source: protocols/skype/skyped.py @ 4da4e9b

Last change on this file since 4da4e9b was 4da4e9b, checked in by Miklos Vajna <vmiklos@…>, at 2013-02-11T12:56:04Z

skype: handle socket errors during tls session negotiation in a graceful way

Before this patch, such errors (which happen 1/4 times here) lock
skyped forever, producing traceback (and hanging because threads
in python).
Proper fix would be to see why these happen (might be ssl handling
in the plugin), but that's no excuse not to handle socket errors
without crashing the daemon.

  • Property mode set to 100644
File size: 15.3 KB
RevLine 
[47c590c]1#!/usr/bin/env python2.7
[b56c76c]2#
[cd3022c]3#   skyped.py
[b56c76c]4#
[9ec6b36]5#   Copyright (c) 2007-2013 by Miklos Vajna <vmiklos@vmiklos.hu>
[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.
[b56c76c]11#
[cd3022c]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.
[b56c76c]16#
[cd3022c]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
[b56c76c]19#   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
[cd3022c]20#   USA.
21#
22
[4ddda13]23import sys
[8237df5]24import os
[4ddda13]25import signal
26import time
27import socket
[c15f71a]28import Skype4Py
[8edfc90]29import hashlib
[d891915]30from ConfigParser import ConfigParser, NoOptionError
[eeeb30e]31from traceback import print_exception
[9ce44dd]32from fcntl import fcntl, F_SETFD, FD_CLOEXEC
[c7000bb]33import ssl
[4ddda13]34
[8237df5]35__version__ = "0.1.1"
[4ddda13]36
[d5a66f8]37try:
38        import gobject
39        hasgobject = True
40except ImportError:
41        import select
42        import threading
43        hasgobject = False
44
[eeeb30e]45def eh(type, value, tb):
[a618ea6]46        global options
47
[3a2a0b2]48        if type != KeyboardInterrupt:
49                print_exception(type, value, tb)
[d5a66f8]50        if hasgobject:
51                gobject.MainLoop().quit()
[53eb75c]52        if options.conn:
53                options.conn.close()
[7e5b4bd]54        if not options.dont_start_skype:
55                # shut down client if it's running
56                try:
57                        skype.skype.Client.Shutdown()
58                except NameError:
59                        pass
[3a2a0b2]60        sys.exit("Exiting.")
[eeeb30e]61
62sys.excepthook = eh
63
[d45adcf]64def wait_for_lock(lock, timeout_to_print, timeout, msg):
65        start = time.time()
66        locked = lock.acquire(0)
67        while not(locked):
68                time.sleep(0.5)
69                if timeout_to_print and (time.time() - timeout_to_print > start):
70                        dprint("%s: Waited %f seconds" % \
71                                        (msg, time.time() - start))
72                        timeout_to_print = False
73                if timeout and (time.time() - timeout > start):
74                        dprint("%s: Waited %f seconds, giving up" % \
75                                        (msg, time.time() - start))
76                        return False
77                locked = lock.acquire(0)
78        return True
79
[d5a66f8]80def input_handler(fd, io_condition = None):
[5245e9d]81        global options
[e530abd]82        global skype
[5245e9d]83        if options.buf:
84                for i in options.buf:
85                        skype.send(i.strip())
86                options.buf = None
[d5a66f8]87                if not hasgobject:
88                        return True
[5245e9d]89        else:
[d5a66f8]90                if not hasgobject:
91                        close_socket = False
92                        if wait_for_lock(options.lock, 3, 10, "input_handler"):
93                                try:
94                                                input = fd.recv(1024)
95                                                options.lock.release()
96                                except Exception, s:
97                                        dprint("Warning, receiving 1024 bytes failed (%s)." % s)
98                                        fd.close()
99                                        options.conn = False
[d45adcf]100                                        options.lock.release()
[d5a66f8]101                                        return False
102                                for i in input.split("\n"):
103                                        if i.strip() == "SET USERSTATUS OFFLINE":
104                                                close_socket = True
105                                        skype.send(i.strip())
106                        return not(close_socket)
107                try:
108                        input = fd.recv(1024)
109                except Exception, s:
110                        dprint("Warning, receiving 1024 bytes failed (%s)." % s)
111                        fd.close()
112                        return False
113                for i in input.split("\n"):
114                        skype.send(i.strip())
115                return True
[c15f71a]116
[4b0092e]117def skype_idle_handler(skype):
[3922d44]118        try:
[6af541d]119                c = skype.skype.Command("PING", Block=True)
120                skype.skype.SendCommand(c)
[1a0b734]121        except (Skype4Py.SkypeAPIError, AttributeError), s:
[3922d44]122                dprint("Warning, pinging Skype failed (%s)." % (s))
[1a0b734]123                time.sleep(1)
[94bd28f]124        return True
[4ddda13]125
[8e166ae]126def send(sock, txt, tries=10):
[1130561]127        global options
[d5a66f8]128        if hasgobject:
[5a4f22e]129                if not options.conn: return
130                try:
131                        sock.sendall(txt)
132                except Exception, s:
133                        dprint("Warning, sending '%s' failed (%s)." % (txt, s))
[53eb75c]134                        options.conn.close()
[5a4f22e]135                        options.conn = False
[d5a66f8]136        else:
[8e166ae]137                for attempt in xrange(1, tries+1):
138                        if not options.conn: break
[d5a66f8]139                        if wait_for_lock(options.lock, 3, 10, "socket send"):
140                                try:
[8e166ae]141                                         if options.conn: sock.sendall(txt)
[d5a66f8]142                                         options.lock.release()
143                                except Exception, s:
144                                        options.lock.release()
145                                        dprint("Warning, sending '%s' failed (%s). count=%d" % (txt, s, count))
[8e166ae]146                                        time.sleep(1)
147                                else:
148                                        break
149                else:
[d5a66f8]150                        if options.conn:
151                                options.conn.close()
152                        options.conn = False
153                return done
[a618ea6]154
[4b0092e]155def bitlbee_idle_handler(skype):
[eeab8bc]156        global options
[e530abd]157        done = False
[4b0092e]158        if options.conn:
159                try:
160                        e = "PING"
[e530abd]161                        done = send(options.conn, "%s\n" % e)
[4b0092e]162                except Exception, s:
163                        dprint("Warning, sending '%s' failed (%s)." % (e, s))
[d5a66f8]164                        if hasgobject:
165                                options.conn.close()
166                        else:
167                                if options.conn: options.conn.close()
168                                options.conn = False
169                                done = False
170        if hasgobject:
171                return True
172        else:
173                return done
174        return True
[4b0092e]175
[d5a66f8]176def server(host, port, skype = None):
[c7304b2]177        global options
[05d964c]178        if ":" in host:
179                sock = socket.socket(socket.AF_INET6)
180        else:
181                sock = socket.socket()
[a316c4e]182        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
[9ce44dd]183        fcntl(sock, F_SETFD, FD_CLOEXEC);
[a316c4e]184        sock.bind((host, port))
185        sock.listen(1)
[9ce44dd]186
[d5a66f8]187        if hasgobject:
188                gobject.io_add_watch(sock, gobject.IO_IN, listener)
189        else:
190                dprint("Waiting for connection...")
191                listener(sock, skype)
[a316c4e]192
[eeab8bc]193def listener(sock, skype):
[5245e9d]194        global options
[d5a66f8]195        if not hasgobject:
196                if not(wait_for_lock(options.lock, 3, 10, "listener")): return False
[c7000bb]197        rawsock, addr = sock.accept()
[6ba00ac]198        try:
199                options.conn = ssl.wrap_socket(rawsock,
200                        server_side=True,
201                        certfile=options.config.sslcert,
202                        keyfile=options.config.sslkey,
203                        ssl_version=ssl.PROTOCOL_TLSv1)
[4da4e9b]204        except (ssl.SSLError, socket.error) as err:
205                if isinstance(err, ssl.SSLError):
206                        dprint("Warning, SSL init failed, did you create your certificate?")
207                        return False
208                else:
209                        dprint('Warning, SSL init failed')
210                        return True
[b0d40f5]211        if hasattr(options.conn, 'handshake'):
[5588f7c4]212                try:
213                        options.conn.handshake()
214                except Exception:
[d5a66f8]215                        if not hasgobject:
216                                options.lock.release()
[5588f7c4]217                        dprint("Warning, handshake failed, closing connection.")
218                        return False
[5245e9d]219        ret = 0
[6b9cab1]220        try:
221                line = options.conn.recv(1024)
222                if line.startswith("USERNAME") and line.split(' ')[1].strip() == options.config.username:
223                        ret += 1
224                line = options.conn.recv(1024)
[8edfc90]225                if line.startswith("PASSWORD") and hashlib.sha1(line.split(' ')[1].strip()).hexdigest() == options.config.password:
[6b9cab1]226                        ret += 1
227        except Exception, s:
228                dprint("Warning, receiving 1024 bytes failed (%s)." % s)
229                options.conn.close()
[d5a66f8]230                if not hasgobject:
231                        options.conn = False
232                        options.lock.release()
[6b9cab1]233                return False
[5245e9d]234        if ret == 2:
235                dprint("Username and password OK.")
[c7304b2]236                options.conn.send("PASSWORD OK\n")
[d5a66f8]237                if hasgobject:
238                        gobject.io_add_watch(options.conn, gobject.IO_IN, input_handler)
239                else:
240                        options.lock.release()
241                        serverloop(options, skype)
[5245e9d]242                return True
243        else:
244                dprint("Username and/or password WRONG.")
[c7304b2]245                options.conn.send("PASSWORD KO\n")
[d5a66f8]246                if not hasgobject:
247                        options.conn.close()
248                        options.conn = False
249                        options.lock.release()
[5245e9d]250                return False
[a316c4e]251
252def dprint(msg):
[ffd078a]253        from time import strftime
[8237df5]254        global options
255
[ffd078a]256        now = strftime("%Y-%m-%d %H:%M:%S")
257
[8237df5]258        if options.debug:
[f503585]259                try:
260                        print now + ": " + msg
261                except Exception, s:
262                        try:
263                                sanitized = msg.encode("ascii", "backslashreplace")
264                        except Error, s:
265                                try:
266                                        sanitized = "hex [" + msg.encode("hex") + "]"
267                                except Error, s:
268                                        sanitized = "[unable to print debug message]"
269                        print now + "~=" + sanitized
[a618ea6]270                sys.stdout.flush()
[bcdc24b]271        if options.log:
272                sock = open(options.log, "a")
[ea1d796]273                sock.write("%s: %s\n" % (now, msg))
[bcdc24b]274                sock.close()
[a316c4e]275
[fffabad]276class MockedSkype:
277        """Mock class for Skype4Py.Skype(), in case the -m option is used."""
278        def __init__(self, mock):
279                sock = open(mock)
280                self.lines = sock.readlines()
[b56c76c]281
[fffabad]282        def SendCommand(self, c):
283                pass
284
285        def Command(self, msg, Block):
286                if msg == "PING":
287                        return ["PONG"]
288                line = self.lines[0].strip()
289                if not line.startswith(">> "):
290                        raise Exception("Corrupted mock input")
291                line = line[3:]
292                if line != msg:
293                        raise Exception("'%s' != '%s'" % (line, msg))
294                self.lines = self.lines[1:] # drop the expected incoming line
295                ret = []
296                while True:
297                        # and now send back all the following lines, up to the next expected incoming line
298                        if len(self.lines) == 0:
299                                break
300                        if self.lines[0].startswith(">> "):
301                                break
302                        if not self.lines[0].startswith("<< "):
303                                raise Exception("Corrupted mock input")
304                        ret.append(self.lines[0][3:].strip())
305                        self.lines = self.lines[1:]
306                return ret
307
[944a941]308class SkypeApi:
[fffabad]309        def __init__(self, mock):
[7e5b4bd]310                global options
[fffabad]311                if not mock:
312                        self.skype = Skype4Py.Skype()
313                        self.skype.OnNotify = self.recv
[7e5b4bd]314                        if not options.dont_start_skype:
315                                self.skype.Client.Start()
[fffabad]316                else:
317                        self.skype = MockedSkype(mock)
[94bd28f]318
[5268bd7]319        def recv(self, msg_text):
[5245e9d]320                global options
[d86dfb1]321                if msg_text == "PONG":
322                        return
[c15f71a]323                if "\n" in msg_text:
[7613670]324                        # crappy skype prefixes only the first line for
325                        # multiline messages so we need to do so for the other
326                        # lines, too. this is something like:
327                        # 'CHATMESSAGE id BODY first line\nsecond line' ->
328                        # 'CHATMESSAGE id BODY first line\nCHATMESSAGE id BODY second line'
[c15f71a]329                        prefix = " ".join(msg_text.split(" ")[:3])
330                        msg_text = ["%s %s" % (prefix, i) for i in " ".join(msg_text.split(" ")[3:]).split("\n")]
[7613670]331                else:
[c15f71a]332                        msg_text = [msg_text]
333                for i in msg_text:
[3423be0]334                        try:
335                                # Internally, BitlBee always uses UTF-8 and encodes/decodes as
336                                # necessary to communicate with the IRC client; thus send the
337                                # UTF-8 it expects
338                                e = i.encode('UTF-8')
339                        except:
340                                # Should never happen, but it's better to send difficult to
341                                # read data than crash because some message couldn't be encoded
342                                e = i.encode('ascii', 'backslashreplace')
[5245e9d]343                        if options.conn:
[e530abd]344                                dprint('<< ' + e)
[af8675f]345                                try:
[a618ea6]346                                        send(options.conn, e + "\n")
[80dfdce]347                                except Exception, s:
[a75f2a7]348                                        dprint("Warning, sending '%s' failed (%s)." % (e, s))
[e530abd]349                                        if options.conn: options.conn.close()
350                                        options.conn = False
351                        else:
[53eb75c]352                                dprint('-- ' + e)
[c15f71a]353
354        def send(self, msg_text):
[4b0092e]355                if not len(msg_text) or msg_text == "PONG":
[53eb75c]356                        if msg_text == "PONG":
357                                options.last_bitlbee_pong = time.time()
[c15f71a]358                        return
[885e563e]359                try:
[3423be0]360                        # Internally, BitlBee always uses UTF-8 and encodes/decodes as
361                        # necessary to communicate with the IRC client; thus decode the
362                        # UTF-8 it sent us
[885e563e]363                        e = msg_text.decode('UTF-8')
[3423be0]364                except:
365                        # Should never happen, but it's better to send difficult to read
366                        # data to Skype than to crash
367                        e = msg_text.decode('ascii', 'backslashreplace')
[52d779e]368                dprint('>> ' + e)
[c15f71a]369                try:
[05cf927]370                        c = self.skype.Command(e, Block=True)
371                        self.skype.SendCommand(c)
[fffabad]372                        if hasattr(c, "Reply"):
373                                self.recv(c.Reply) # Skype4Py answer
374                        else:
375                                for i in c: # mock may return multiple iterable answers
376                                        self.recv(i)
[05cf927]377                except Skype4Py.SkypeError:
[c15f71a]378                        pass
[8b3beef]379                except Skype4Py.SkypeAPIError, s:
[a75f2a7]380                        dprint("Warning, sending '%s' failed (%s)." % (e, s))
[4ddda13]381
[8237df5]382
[eeab8bc]383def serverloop(options, skype):
384        timeout = 1; # in seconds
385        skype_ping_period = 5
[e530abd]386        bitlbee_ping_period = 10
387        bitlbee_pong_timeout = 30
388        now = time.time()
389        skype_ping_start_time = now
390        bitlbee_ping_start_time = now
391        options.last_bitlbee_pong = now
392        in_error = []
393        handler_ok = True
394        while (len(in_error) == 0) and handler_ok and options.conn:
[eeab8bc]395                ready_to_read, ready_to_write, in_error = \
[9c51166]396                        select.select([options.conn], [], [options.conn], \
397                                timeout)
[eeab8bc]398                now = time.time()
[9c51166]399                handler_ok = len(in_error) == 0
400                if (len(ready_to_read) == 1) and handler_ok:
[e530abd]401                        handler_ok = input_handler(ready_to_read.pop())
[eeab8bc]402                        # don't ping bitlbee/skype if they already received data
[9c51166]403                        now = time.time() # allow for the input_handler to take some time
[eeab8bc]404                        bitlbee_ping_start_time = now
405                        skype_ping_start_time = now
[9c51166]406                        options.last_bitlbee_pong = now
[e530abd]407                if (now - skype_ping_period > skype_ping_start_time) and handler_ok:
408                        handler_ok = skype_idle_handler(skype)
[eeab8bc]409                        skype_ping_start_time = now
410                if now - bitlbee_ping_period > bitlbee_ping_start_time:
[e530abd]411                        handler_ok = bitlbee_idle_handler(skype)
[eeab8bc]412                        bitlbee_ping_start_time = now
[e530abd]413                        if options.last_bitlbee_pong:
414                                if (now - options.last_bitlbee_pong) > bitlbee_pong_timeout:
415                                        dprint("Bitlbee pong timeout")
416                                        # TODO is following line necessary? Should there be a options.conn.unwrap() somewhere?
417                                        # options.conn.shutdown()
[53eb75c]418                                        if options.conn:
419                                                options.conn.close()
[e530abd]420                                        options.conn = False
421                        else:
422                                options.last_bitlbee_pong = now
[eeab8bc]423
[b56c76c]424
425def main(args=None):
426        global options
427        global skype
428
429        cfgpath = os.path.join(os.environ['HOME'], ".skyped", "skyped.conf")
430        syscfgpath = "/usr/local/etc/skyped/skyped.conf"
431        if not os.path.exists(cfgpath) and os.path.exists(syscfgpath):
432                cfgpath = syscfgpath # fall back to system-wide settings
433        port = 2727
434
435        import argparse
436        parser = argparse.ArgumentParser()
437        parser.add_argument('-c', '--config',
438                metavar='path', default=cfgpath,
439                help='path to configuration file (default: %(default)s)')
440        parser.add_argument('-H', '--host', default='0.0.0.0',
441                help='set the tcp host, supports IPv4 and IPv6 (default: %(default)s)')
442        parser.add_argument('-p', '--port', type=int,
443                help='set the tcp port (default: %(default)s)')
444        parser.add_argument('-l', '--log', metavar='path',
445                help='set the log file in background mode (default: none)')
446        parser.add_argument('-v', '--version', action='store_true', help='display version information')
447        parser.add_argument('-n', '--nofork',
448                action='store_true', help="don't run as daemon in the background")
[7e5b4bd]449        parser.add_argument('-s', '--dont-start-skype', action='store_true',
450                help="assume that skype is running independently, don't try to start/stop it")
[b56c76c]451        parser.add_argument('-m', '--mock', help='fake interactions with skype (only useful for tests)')
452        parser.add_argument('-d', '--debug', action='store_true', help='enable debug messages')
453        options = parser.parse_args(sys.argv[1:] if args is None else args)
454
455        if options.version:
[8237df5]456                print "skyped %s" % __version__
457                sys.exit(0)
[b56c76c]458
459        # well, this is a bit hackish. we store the socket of the last connected client
460        # here and notify it. maybe later notify all connected clients?
461        options.conn = None
462        # this will be read first by the input handler
463        options.buf = None
464
465        if not os.path.exists(options.config):
466                parser.error(( "Can't find configuration file at '%s'."
467                        "Use the -c option to specify an alternate one." )% options.config)
468
469        cfgpath = options.config
[5245e9d]470        options.config = ConfigParser()
[b56c76c]471        options.config.read(cfgpath)
472        options.config.username = options.config.get('skyped', 'username').split('#', 1)[0]
473        options.config.password = options.config.get('skyped', 'password').split('#', 1)[0]
474        options.config.sslkey = os.path.expanduser(options.config.get('skyped', 'key').split('#', 1)[0])
475        options.config.sslcert = os.path.expanduser(options.config.get('skyped', 'cert').split('#', 1)[0])
476
[d891915]477        # hack: we have to parse the parameters first to locate the
478        # config file but the -p option should overwrite the value from
479        # the config file
480        try:
[b56c76c]481                options.config.port = int(options.config.get('skyped', 'port').split('#', 1)[0])
[d891915]482                if not options.port:
483                        options.port = options.config.port
484        except NoOptionError:
485                pass
486        if not options.port:
[b56c76c]487                options.port = port
488        dprint("Parsing config file '%s' done, username is '%s'." % (options.config, options.config.username))
489        if not options.nofork:
[8237df5]490                pid = os.fork()
491                if pid == 0:
[56ae398]492                        nullin = file(os.devnull, 'r')
493                        nullout = file(os.devnull, 'w')
[8237df5]494                        os.dup2(nullin.fileno(), sys.stdin.fileno())
495                        os.dup2(nullout.fileno(), sys.stdout.fileno())
496                        os.dup2(nullout.fileno(), sys.stderr.fileno())
497                else:
498                        print 'skyped is started on port %s, pid: %d' % (options.port, pid)
499                        sys.exit(0)
[d891915]500        else:
501                dprint('skyped is started on port %s' % options.port)
[d5a66f8]502        if hasgobject:
503                server(options.host, options.port)
[c15f71a]504        try:
[fffabad]505                skype = SkypeApi(options.mock)
[8b3beef]506        except Skype4Py.SkypeAPIError, s:
[c15f71a]507                sys.exit("%s. Are you sure you have started Skype?" % s)
[d5a66f8]508        if hasgobject:
509                gobject.timeout_add(2000, skype_idle_handler, skype)
510                gobject.timeout_add(60000, bitlbee_idle_handler, skype)
511                gobject.MainLoop().run()
512        else:
513                while 1:
514                        options.conn = False
515                        options.lock = threading.Lock()
516                        server(options.host, options.port, skype)
[b56c76c]517
518
519if __name__ == '__main__': main()
Note: See TracBrowser for help on using the repository browser.