source: protocols/skype/skyped.py @ 8e166ae

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

skype: cleanup of the send() code

Use socket.sendall(), as send() is not guaranteed to send all
the data passed (though it should generally work that way with
blocking sockets).
Use more robust, obvious and idiomatic loop conditions.
Cleanup redundant imports and variables.

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