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
RevLine 
[47c590c]1#!/usr/bin/env python2.7
[cd3022c]2#
3#   skyped.py
4
[7cf146f]5#   Copyright (c) 2007, 2008, 2009, 2010 by Miklos Vajna <vmiklos@frugalware.org>
[cd3022c]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
[4ddda13]23import sys
[8237df5]24import os
[4ddda13]25import signal
26import locale
27import time
28import socket
[8237df5]29import getopt
[c15f71a]30import Skype4Py
[8edfc90]31import hashlib
[d891915]32from ConfigParser import ConfigParser, NoOptionError
[eeeb30e]33from traceback import print_exception
[c7000bb]34import ssl
[eeab8bc]35import select
[d45adcf]36import threading
[4ddda13]37
[8237df5]38__version__ = "0.1.1"
[4ddda13]39
[eeeb30e]40def eh(type, value, tb):
[a618ea6]41        global options
42
[3a2a0b2]43        if type != KeyboardInterrupt:
44                print_exception(type, value, tb)
[e530abd]45        if options.conn: options.conn.close()
[7415989]46        # shut down client if it's running
47        try:
48                skype.skype.Client.Shutdown()
49        except NameError:
50                pass
[3a2a0b2]51        sys.exit("Exiting.")
[eeeb30e]52
53sys.excepthook = eh
54
[d45adcf]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
[e530abd]71def input_handler(fd):
[5245e9d]72        global options
[e530abd]73        global skype
[5245e9d]74        if options.buf:
75                for i in options.buf:
76                        skype.send(i.strip())
77                options.buf = None
[e530abd]78                return True
[5245e9d]79        else:
[f85837a]80                close_socket = False
[d45adcf]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"):
[f85837a]92                                if i.strip() == "SET USERSTATUS OFFLINE":
93                                        close_socket = True
[d45adcf]94                                skype.send(i.strip())
[f85837a]95                return not(close_socket)
[c15f71a]96
[4b0092e]97def skype_idle_handler(skype):
[3922d44]98        try:
[6af541d]99                c = skype.skype.Command("PING", Block=True)
100                skype.skype.SendCommand(c)
[5a54ec8]101                dprint("... skype pinged")
[3922d44]102        except Skype4Py.SkypeAPIError, s:
103                dprint("Warning, pinging Skype failed (%s)." % (s))
[94bd28f]104        return True
[4ddda13]105
[a618ea6]106def send(sock, txt):
[1130561]107        global options
[a618ea6]108        count = 1
109        done = False
[1130561]110        while (not done) and (count < 10) and options.conn:
[d45adcf]111                if wait_for_lock(options.lock, 3, 10, "socket send"):
112                        try:
[1130561]113                                if options.conn: sock.send(txt)
[d45adcf]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)
[a618ea6]121        if not done:
[e530abd]122                if options.conn: options.conn.close()
123                options.conn = False
124        return done
[a618ea6]125
[4b0092e]126def bitlbee_idle_handler(skype):
[eeab8bc]127        global options
[e530abd]128        done = False
[4b0092e]129        if options.conn:
130                try:
131                        e = "PING"
[e530abd]132                        done = send(options.conn, "%s\n" % e)
[5a54ec8]133                        dprint("... pinged Bitlbee")
[4b0092e]134                except Exception, s:
135                        dprint("Warning, sending '%s' failed (%s)." % (e, s))
[e530abd]136                        if options.conn: options.conn.close()
137                        options.conn = False
138                        done = False
139        return done
[4b0092e]140
[eeab8bc]141def server(host, port, skype):
[c7304b2]142        global options
[c7000bb]143        sock = socket.socket()
[a316c4e]144        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
145        sock.bind((host, port))
146        sock.listen(1)
[eeab8bc]147        dprint("Waiting for connection...")
148        listener(sock, skype)
[a316c4e]149
[eeab8bc]150def listener(sock, skype):
[5245e9d]151        global options
[d45adcf]152        if not(wait_for_lock(options.lock, 3, 10, "listener")): return False
[c7000bb]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)
[b0d40f5]159        if hasattr(options.conn, 'handshake'):
[5588f7c4]160                try:
161                        options.conn.handshake()
162                except Exception:
[d45adcf]163                        options.lock.release()
[5588f7c4]164                        dprint("Warning, handshake failed, closing connection.")
165                        return False
[5245e9d]166        ret = 0
[6b9cab1]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)
[8edfc90]172                if line.startswith("PASSWORD") and hashlib.sha1(line.split(' ')[1].strip()).hexdigest() == options.config.password:
[6b9cab1]173                        ret += 1
174        except Exception, s:
175                dprint("Warning, receiving 1024 bytes failed (%s)." % s)
176                options.conn.close()
[e530abd]177                options.conn = False
[d45adcf]178                options.lock.release()
[6b9cab1]179                return False
[5245e9d]180        if ret == 2:
181                dprint("Username and password OK.")
[c7304b2]182                options.conn.send("PASSWORD OK\n")
[d45adcf]183                options.lock.release()
[eeab8bc]184                serverloop(options, skype)
[5245e9d]185                return True
186        else:
187                dprint("Username and/or password WRONG.")
[c7304b2]188                options.conn.send("PASSWORD KO\n")
[e530abd]189                options.conn.close()
190                options.conn = False
[d45adcf]191                options.lock.release()
[5245e9d]192                return False
[a316c4e]193
194def dprint(msg):
[ffd078a]195        from time import strftime
[8237df5]196        global options
197
[ffd078a]198        now = strftime("%Y-%m-%d %H:%M:%S")
199
[8237df5]200        if options.debug:
[ffd078a]201                print now + ": " + msg
[a618ea6]202                sys.stdout.flush()
[bcdc24b]203        if options.log:
204                sock = open(options.log, "a")
[ea1d796]205                sock.write("%s: %s\n" % (now, msg))
[bcdc24b]206                sock.close()
[a316c4e]207
[944a941]208class SkypeApi:
[94bd28f]209        def __init__(self):
[c15f71a]210                self.skype = Skype4Py.Skype()
[5268bd7]211                self.skype.OnNotify = self.recv
[6af541d]212                self.skype.Client.Start()
[94bd28f]213
[5268bd7]214        def recv(self, msg_text):
[5245e9d]215                global options
[d86dfb1]216                if msg_text == "PONG":
217                        return
[c15f71a]218                if "\n" in msg_text:
[7613670]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'
[c15f71a]224                        prefix = " ".join(msg_text.split(" ")[:3])
225                        msg_text = ["%s %s" % (prefix, i) for i in " ".join(msg_text.split(" ")[3:]).split("\n")]
[7613670]226                else:
[c15f71a]227                        msg_text = [msg_text]
228                for i in msg_text:
[a75f2a7]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')
[5245e9d]235                        if options.conn:
[e530abd]236                                dprint('<< ' + e)
[af8675f]237                                try:
[eeab8bc]238                                        # I called the send function really_send
[a618ea6]239                                        send(options.conn, e + "\n")
[80dfdce]240                                except Exception, s:
[a75f2a7]241                                        dprint("Warning, sending '%s' failed (%s)." % (e, s))
[e530abd]242                                        if options.conn: options.conn.close()
243                                        options.conn = False
244                        else:
245                                dprint('---' + e)
[c15f71a]246
247        def send(self, msg_text):
[4b0092e]248                if not len(msg_text) or msg_text == "PONG":
[e530abd]249                        if msg_text == "PONG": options.last_bitlbee_pong = time.time()
[c15f71a]250                        return
[885e563e]251                try:
[072c0fe]252                        encoding = locale.getdefaultlocale()[1]
253                        if not encoding:
254                                raise ValueError
255                        e = msg_text.decode(encoding)
[885e563e]256                except ValueError:
257                        e = msg_text.decode('UTF-8')
[52d779e]258                dprint('>> ' + e)
[c15f71a]259                try:
[05cf927]260                        c = self.skype.Command(e, Block=True)
261                        self.skype.SendCommand(c)
262                        self.recv(c.Reply)
263                except Skype4Py.SkypeError:
[c15f71a]264                        pass
[8b3beef]265                except Skype4Py.SkypeAPIError, s:
[a75f2a7]266                        dprint("Warning, sending '%s' failed (%s)." % (e, s))
[4ddda13]267
[8237df5]268class Options:
269        def __init__(self):
[1b48afb]270                self.cfgpath = os.path.join(os.environ['HOME'], ".skyped", "skyped.conf")
[1a575f69]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
[8237df5]275                self.daemon = True
276                self.debug = False
277                self.help = False
[7e450c3]278                self.host = "0.0.0.0"
[bcdc24b]279                self.log = None
[d891915]280                self.port = None
[8237df5]281                self.version = False
[5245e9d]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
[8237df5]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:
[5245e9d]295        -c      --config        path to configuration file (default: %s)
[8237df5]296        -d      --debug         enable debug messages
297        -h      --help          this help
[7e450c3]298        -H      --host          set the tcp host (default: %s)
[bcdc24b]299        -l      --log           set the log file in background mode (default: none)
[8237df5]300        -n      --nofork        don't run as daemon in the background
[a349932]301        -p      --port          set the tcp port (default: %s)
[5245e9d]302        -v      --version       display version information""" % (self.cfgpath, self.host, self.port)
[8237df5]303                sys.exit(ret)
304
[eeab8bc]305def serverloop(options, skype):
306        timeout = 1; # in seconds
307        skype_ping_period = 5
[e530abd]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:
[eeab8bc]317                ready_to_read, ready_to_write, in_error = \
[9c51166]318                        select.select([options.conn], [], [options.conn], \
319                                timeout)
[eeab8bc]320                now = time.time()
[9c51166]321                handler_ok = len(in_error) == 0
322                if (len(ready_to_read) == 1) and handler_ok:
[e530abd]323                        handler_ok = input_handler(ready_to_read.pop())
[eeab8bc]324                        # don't ping bitlbee/skype if they already received data
[9c51166]325                        now = time.time() # allow for the input_handler to take some time
[eeab8bc]326                        bitlbee_ping_start_time = now
327                        skype_ping_start_time = now
[9c51166]328                        options.last_bitlbee_pong = now
[e530abd]329                if (now - skype_ping_period > skype_ping_start_time) and handler_ok:
330                        handler_ok = skype_idle_handler(skype)
[eeab8bc]331                        skype_ping_start_time = now
332                if now - bitlbee_ping_period > bitlbee_ping_start_time:
[e530abd]333                        handler_ok = bitlbee_idle_handler(skype)
[eeab8bc]334                        bitlbee_ping_start_time = now
[e530abd]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")
[eeab8bc]347
[4ddda13]348if __name__=='__main__':
[8237df5]349        options = Options()
350        try:
[a985369]351                opts, args = getopt.getopt(sys.argv[1:], "c:dhH:l:np:v", ["config=", "debug", "help", "host=", "log=", "nofork", "port=", "version"])
[8237df5]352        except getopt.GetoptError:
353                options.usage(1)
354        for opt, arg in opts:
[5245e9d]355                if opt in ("-c", "--config"):
356                        options.cfgpath = arg
357                elif opt in ("-d", "--debug"):
[8237df5]358                        options.debug = True
359                elif opt in ("-h", "--help"):
360                        options.help = True
[7e450c3]361                elif opt in ("-H", "--host"):
362                        options.host = arg
[bcdc24b]363                elif opt in ("-l", "--log"):
364                        options.log = arg
[8237df5]365                elif opt in ("-n", "--nofork"):
366                        options.daemon = False
367                elif opt in ("-p", "--port"):
[d891915]368                        options.port = int(arg)
[8237df5]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)
[5245e9d]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]
[7cc2c1e]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])
[d891915]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
[5245e9d]398        dprint("Parsing config file '%s' done, username is '%s'." % (options.cfgpath, options.config.username))
399        if options.daemon:
[8237df5]400                pid = os.fork()
401                if pid == 0:
[56ae398]402                        nullin = file(os.devnull, 'r')
403                        nullout = file(os.devnull, 'w')
[8237df5]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)
[d891915]410        else:
411                dprint('skyped is started on port %s' % options.port)
[c15f71a]412        try:
[3953172]413                skype = SkypeApi()
[8b3beef]414        except Skype4Py.SkypeAPIError, s:
[c15f71a]415                sys.exit("%s. Are you sure you have started Skype?" % s)
[e530abd]416        while 1:
417                options.conn = False
[d45adcf]418                options.lock = threading.Lock()
[e530abd]419                server(options.host, options.port, skype)
Note: See TracBrowser for help on using the repository browser.