source: protocols/skype/skyped.py @ 7a2a486

Last change on this file since 7a2a486 was 9ce44dd, checked in by Miklos Vajna <vmiklos@…>, at 2012-04-22T21:48:09Z

skyped: set FD_CLOEXEC on listening socket

Skype4Py uses os.execlp() to spawn skype if it is not yet started, this leaks
our listening FD to skype process and can't get it back even if we ourself exit
meanwhile.

and we can't startup again:
error: [Errno 98] Address already in use

Patch-by: Elan Ruusamäe <glen@…>

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