source: skype/skyped.py @ ae8cc50

Last change on this file since ae8cc50 was f85837a, checked in by Philippe Crama <pcfeb0009@…>, at 2010-12-28T13:06:16Z

Close connection when going offline

When the user disconnects from bitlbee (either by quitting or by
logging out "account skype off"), skyped should close its connection
and wait for a new connection.

This change is useful to make the test cases pass as otherwise,
skyped would not notice that the first test was done and that the
second test was trying to connect, thus leading to a failure in
the second test.

  • Property mode set to 100644
File size: 12.5 KB
Line 
1#!/usr/bin/env python2.7
2#
3#   skyped.py
4
5#   Copyright (c) 2007, 2008, 2009, 2010 by Miklos Vajna <vmiklos@frugalware.org>
6#
7#   This program is free software; you can redistribute it and/or modify
8#   it under the terms of the GNU General Public License as published by
9#   the Free Software Foundation; either version 2 of the License, or
10#   (at your option) any later version.
11#
12#   This program is distributed in the hope that it will be useful,
13#   but WITHOUT ANY WARRANTY; without even the implied warranty of
14#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15#   GNU General Public License for more details.
16
17#   You should have received a copy of the GNU General Public License
18#   along with this program; if not, write to the Free Software
19#   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
20#   USA.
21#
22
23import sys
24import os
25import signal
26import locale
27import time
28import socket
29import getopt
30import Skype4Py
31import hashlib
32from ConfigParser import ConfigParser, NoOptionError
33from traceback import print_exception
34import ssl
35import select
36import threading
37
38__version__ = "0.1.1"
39
40def eh(type, value, tb):
41        global options
42
43        if type != KeyboardInterrupt:
44                print_exception(type, value, tb)
45        if options.conn: options.conn.close()
46        # shut down client if it's running
47        try:
48                skype.skype.Client.Shutdown()
49        except NameError:
50                pass
51        sys.exit("Exiting.")
52
53sys.excepthook = eh
54
55def wait_for_lock(lock, timeout_to_print, timeout, msg):
56        start = time.time()
57        locked = lock.acquire(0)
58        while not(locked):
59                time.sleep(0.5)
60                if timeout_to_print and (time.time() - timeout_to_print > start):
61                        dprint("%s: Waited %f seconds" % \
62                                        (msg, time.time() - start))
63                        timeout_to_print = False
64                if timeout and (time.time() - timeout > start):
65                        dprint("%s: Waited %f seconds, giving up" % \
66                                        (msg, time.time() - start))
67                        return False
68                locked = lock.acquire(0)
69        return True
70
71def input_handler(fd):
72        global options
73        global skype
74        if options.buf:
75                for i in options.buf:
76                        skype.send(i.strip())
77                options.buf = None
78                return True
79        else:
80                close_socket = False
81                if wait_for_lock(options.lock, 3, 10, "input_handler"):
82                        try:
83                                        input = fd.recv(1024)
84                                        options.lock.release()
85                        except Exception, s:
86                                dprint("Warning, receiving 1024 bytes failed (%s)." % s)
87                                fd.close()
88                                options.conn = False
89                                options.lock.release()
90                                return False
91                        for i in input.split("\n"):
92                                if i.strip() == "SET USERSTATUS OFFLINE":
93                                        close_socket = True
94                                skype.send(i.strip())
95                return not(close_socket)
96
97def skype_idle_handler(skype):
98        try:
99                c = skype.skype.Command("PING", Block=True)
100                skype.skype.SendCommand(c)
101                dprint("... skype pinged")
102        except Skype4Py.SkypeAPIError, s:
103                dprint("Warning, pinging Skype failed (%s)." % (s))
104        return True
105
106def send(sock, txt):
107        global options
108        count = 1
109        done = False
110        while (not done) and (count < 10) and options.conn:
111                if wait_for_lock(options.lock, 3, 10, "socket send"):
112                        try:
113                                if options.conn: sock.send(txt)
114                                options.lock.release()
115                                done = True
116                        except Exception, s:
117                                options.lock.release()
118                                count += 1
119                                dprint("Warning, sending '%s' failed (%s). count=%d" % (txt, s, count))
120                                time.sleep(1)
121        if not done:
122                if options.conn: options.conn.close()
123                options.conn = False
124        return done
125
126def bitlbee_idle_handler(skype):
127        global options
128        done = False
129        if options.conn:
130                try:
131                        e = "PING"
132                        done = send(options.conn, "%s\n" % e)
133                        dprint("... pinged Bitlbee")
134                except Exception, s:
135                        dprint("Warning, sending '%s' failed (%s)." % (e, s))
136                        if options.conn: options.conn.close()
137                        options.conn = False
138                        done = False
139        return done
140
141def server(host, port, skype):
142        global options
143        sock = socket.socket()
144        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
145        sock.bind((host, port))
146        sock.listen(1)
147        dprint("Waiting for connection...")
148        listener(sock, skype)
149
150def listener(sock, skype):
151        global options
152        if not(wait_for_lock(options.lock, 3, 10, "listener")): return False
153        rawsock, addr = sock.accept()
154        options.conn = ssl.wrap_socket(rawsock,
155                server_side=True,
156                certfile=options.config.sslcert,
157                keyfile=options.config.sslkey,
158                ssl_version=ssl.PROTOCOL_TLSv1)
159        if hasattr(options.conn, 'handshake'):
160                try:
161                        options.conn.handshake()
162                except Exception:
163                        options.lock.release()
164                        dprint("Warning, handshake failed, closing connection.")
165                        return False
166        ret = 0
167        try:
168                line = options.conn.recv(1024)
169                if line.startswith("USERNAME") and line.split(' ')[1].strip() == options.config.username:
170                        ret += 1
171                line = options.conn.recv(1024)
172                if line.startswith("PASSWORD") and hashlib.sha1(line.split(' ')[1].strip()).hexdigest() == options.config.password:
173                        ret += 1
174        except Exception, s:
175                dprint("Warning, receiving 1024 bytes failed (%s)." % s)
176                options.conn.close()
177                options.conn = False
178                options.lock.release()
179                return False
180        if ret == 2:
181                dprint("Username and password OK.")
182                options.conn.send("PASSWORD OK\n")
183                options.lock.release()
184                serverloop(options, skype)
185                return True
186        else:
187                dprint("Username and/or password WRONG.")
188                options.conn.send("PASSWORD KO\n")
189                options.conn.close()
190                options.conn = False
191                options.lock.release()
192                return False
193
194def dprint(msg):
195        from time import strftime
196        global options
197
198        now = strftime("%Y-%m-%d %H:%M:%S")
199
200        if options.debug:
201                print now + ": " + msg
202                sys.stdout.flush()
203        if options.log:
204                sock = open(options.log, "a")
205                sock.write("%s: %s\n" % (now, msg))
206                sock.close()
207
208class SkypeApi:
209        def __init__(self):
210                self.skype = Skype4Py.Skype()
211                self.skype.OnNotify = self.recv
212                self.skype.Client.Start()
213
214        def recv(self, msg_text):
215                global options
216                if msg_text == "PONG":
217                        return
218                if "\n" in msg_text:
219                        # crappy skype prefixes only the first line for
220                        # multiline messages so we need to do so for the other
221                        # lines, too. this is something like:
222                        # 'CHATMESSAGE id BODY first line\nsecond line' ->
223                        # 'CHATMESSAGE id BODY first line\nCHATMESSAGE id BODY second line'
224                        prefix = " ".join(msg_text.split(" ")[:3])
225                        msg_text = ["%s %s" % (prefix, i) for i in " ".join(msg_text.split(" ")[3:]).split("\n")]
226                else:
227                        msg_text = [msg_text]
228                for i in msg_text:
229                        # use utf-8 here to solve the following problem:
230                        # people use env vars like LC_ALL=en_US (latin1) then
231                        # they complain about why can't they receive latin2
232                        # messages.. so here it is: always use utf-8 then
233                        # everybody will be happy
234                        e = i.encode('UTF-8')
235                        if options.conn:
236                                dprint('<< ' + e)
237                                try:
238                                        # I called the send function really_send
239                                        send(options.conn, e + "\n")
240                                except Exception, s:
241                                        dprint("Warning, sending '%s' failed (%s)." % (e, s))
242                                        if options.conn: options.conn.close()
243                                        options.conn = False
244                        else:
245                                dprint('---' + e)
246
247        def send(self, msg_text):
248                if not len(msg_text) or msg_text == "PONG":
249                        if msg_text == "PONG": options.last_bitlbee_pong = time.time()
250                        return
251                try:
252                        encoding = locale.getdefaultlocale()[1]
253                        if not encoding:
254                                raise ValueError
255                        e = msg_text.decode(encoding)
256                except ValueError:
257                        e = msg_text.decode('UTF-8')
258                dprint('>> ' + e)
259                try:
260                        c = self.skype.Command(e, Block=True)
261                        self.skype.SendCommand(c)
262                        self.recv(c.Reply)
263                except Skype4Py.SkypeError:
264                        pass
265                except Skype4Py.SkypeAPIError, s:
266                        dprint("Warning, sending '%s' failed (%s)." % (e, s))
267
268class Options:
269        def __init__(self):
270                self.cfgpath = os.path.join(os.environ['HOME'], ".skyped", "skyped.conf")
271                # for backwards compatibility
272                self.syscfgpath = "/usr/local/etc/skyped/skyped.conf"
273                if os.path.exists(self.syscfgpath):
274                        self.cfgpath = self.syscfgpath
275                self.daemon = True
276                self.debug = False
277                self.help = False
278                self.host = "0.0.0.0"
279                self.log = None
280                self.port = None
281                self.version = False
282                # well, this is a bit hackish. we store the socket of the last connected client
283                # here and notify it. maybe later notify all connected clients?
284                self.conn = None
285                # this will be read first by the input handler
286                self.buf = None
287
288
289        def usage(self, ret):
290                print """Usage: skyped [OPTION]...
291
292skyped is a daemon that acts as a tcp server on top of a Skype instance.
293
294Options:
295        -c      --config        path to configuration file (default: %s)
296        -d      --debug         enable debug messages
297        -h      --help          this help
298        -H      --host          set the tcp host (default: %s)
299        -l      --log           set the log file in background mode (default: none)
300        -n      --nofork        don't run as daemon in the background
301        -p      --port          set the tcp port (default: %s)
302        -v      --version       display version information""" % (self.cfgpath, self.host, self.port)
303                sys.exit(ret)
304
305def serverloop(options, skype):
306        timeout = 1; # in seconds
307        skype_ping_period = 5
308        bitlbee_ping_period = 10
309        bitlbee_pong_timeout = 30
310        now = time.time()
311        skype_ping_start_time = now
312        bitlbee_ping_start_time = now
313        options.last_bitlbee_pong = now
314        in_error = []
315        handler_ok = True
316        while (len(in_error) == 0) and handler_ok and options.conn:
317                ready_to_read, ready_to_write, in_error = \
318                        select.select([options.conn], [], [options.conn], \
319                                timeout)
320                now = time.time()
321                handler_ok = len(in_error) == 0
322                if (len(ready_to_read) == 1) and handler_ok:
323                        handler_ok = input_handler(ready_to_read.pop())
324                        # don't ping bitlbee/skype if they already received data
325                        now = time.time() # allow for the input_handler to take some time
326                        bitlbee_ping_start_time = now
327                        skype_ping_start_time = now
328                        options.last_bitlbee_pong = now
329                if (now - skype_ping_period > skype_ping_start_time) and handler_ok:
330                        handler_ok = skype_idle_handler(skype)
331                        skype_ping_start_time = now
332                if now - bitlbee_ping_period > bitlbee_ping_start_time:
333                        handler_ok = bitlbee_idle_handler(skype)
334                        bitlbee_ping_start_time = now
335                        if options.last_bitlbee_pong:
336                                if (now - options.last_bitlbee_pong) > bitlbee_pong_timeout:
337                                        dprint("Bitlbee pong timeout")
338                                        # TODO is following line necessary? Should there be a options.conn.unwrap() somewhere?
339                                        # options.conn.shutdown()
340                                        if options.conn: options.conn.close()
341                                        options.conn = False
342                                else:
343                                        dprint("%f seconds since last PONG" % (now - options.last_bitlbee_pong))
344                        else:
345                                options.last_bitlbee_pong = now
346        dprint("Serverloop done")
347
348if __name__=='__main__':
349        options = Options()
350        try:
351                opts, args = getopt.getopt(sys.argv[1:], "c:dhH:l:np:v", ["config=", "debug", "help", "host=", "log=", "nofork", "port=", "version"])
352        except getopt.GetoptError:
353                options.usage(1)
354        for opt, arg in opts:
355                if opt in ("-c", "--config"):
356                        options.cfgpath = arg
357                elif opt in ("-d", "--debug"):
358                        options.debug = True
359                elif opt in ("-h", "--help"):
360                        options.help = True
361                elif opt in ("-H", "--host"):
362                        options.host = arg
363                elif opt in ("-l", "--log"):
364                        options.log = arg
365                elif opt in ("-n", "--nofork"):
366                        options.daemon = False
367                elif opt in ("-p", "--port"):
368                        options.port = int(arg)
369                elif opt in ("-v", "--version"):
370                        options.version = True
371        if options.help:
372                options.usage(0)
373        elif options.version:
374                print "skyped %s" % __version__
375                sys.exit(0)
376        # parse our config
377        if not os.path.exists(options.cfgpath):
378                print "Can't find configuration file at '%s'." % options.cfgpath
379                print "Use the -c option to specify an alternate one."
380                sys.exit(1)
381        options.config = ConfigParser()
382        options.config.read(options.cfgpath)
383        options.config.username = options.config.get('skyped', 'username').split('#')[0]
384        options.config.password = options.config.get('skyped', 'password').split('#')[0]
385        options.config.sslkey = os.path.expanduser(options.config.get('skyped', 'key').split('#')[0])
386        options.config.sslcert = os.path.expanduser(options.config.get('skyped', 'cert').split('#')[0])
387        # hack: we have to parse the parameters first to locate the
388        # config file but the -p option should overwrite the value from
389        # the config file
390        try:
391                options.config.port = int(options.config.get('skyped', 'port').split('#')[0])
392                if not options.port:
393                        options.port = options.config.port
394        except NoOptionError:
395                pass
396        if not options.port:
397                options.port = 2727
398        dprint("Parsing config file '%s' done, username is '%s'." % (options.cfgpath, options.config.username))
399        if options.daemon:
400                pid = os.fork()
401                if pid == 0:
402                        nullin = file(os.devnull, 'r')
403                        nullout = file(os.devnull, 'w')
404                        os.dup2(nullin.fileno(), sys.stdin.fileno())
405                        os.dup2(nullout.fileno(), sys.stdout.fileno())
406                        os.dup2(nullout.fileno(), sys.stderr.fileno())
407                else:
408                        print 'skyped is started on port %s, pid: %d' % (options.port, pid)
409                        sys.exit(0)
410        else:
411                dprint('skyped is started on port %s' % options.port)
412        try:
413                skype = SkypeApi()
414        except Skype4Py.SkypeAPIError, s:
415                sys.exit("%s. Are you sure you have started Skype?" % s)
416        while 1:
417                options.conn = False
418                options.lock = threading.Lock()
419                server(options.host, options.port, skype)
Note: See TracBrowser for help on using the repository browser.