source: etherws/trunk/etherws.py @ 162

Revision 162, 11.1 KB checked in by atzm, 12 years ago (diff)
  • improve performance
  • Property svn:keywords set to Id
RevLine 
[133]1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3#
[141]4#              Ethernet over WebSocket tunneling server/client
[133]5#
6# depends on:
7#   - python-2.7.2
8#   - python-pytun-0.2
[136]9#   - websocket-client-0.7.0
10#   - tornado-2.2.1
[133]11#
[140]12# todo:
[143]13#   - servant mode support (like typical p2p software)
[140]14#
[133]15# ===========================================================================
16# Copyright (c) 2012, Atzm WATANABE <atzm@atzm.org>
17# All rights reserved.
18#
19# Redistribution and use in source and binary forms, with or without
20# modification, are permitted provided that the following conditions are met:
21#
22# 1. Redistributions of source code must retain the above copyright notice,
23#    this list of conditions and the following disclaimer.
24# 2. Redistributions in binary form must reproduce the above copyright
25#    notice, this list of conditions and the following disclaimer in the
26#    documentation and/or other materials provided with the distribution.
27#
28# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
29# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
30# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
31# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
32# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
33# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
34# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
35# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
36# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
37# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
38# POSSIBILITY OF SUCH DAMAGE.
39# ===========================================================================
40#
41# $Id$
42
43import os
44import sys
[156]45import ssl
[160]46import time
[150]47import base64
48import hashlib
[151]49import getpass
[133]50import argparse
51import threading
52
53import pytun
54import websocket
[160]55import tornado.web
[133]56import tornado.ioloop
57import tornado.websocket
[160]58import tornado.httpserver
[133]59
60
[160]61class DebugMixIn(object):
62    def dprintf(self, msg, *args):
63        if self._debug:
64            prefix = '[%s] %s - ' % (time.asctime(), self.__class__.__name__)
65            sys.stderr.write(prefix + (msg % args))
66
67
68class TapHandler(DebugMixIn):
[162]69    READ_SIZE = 65535
70
[133]71    def __init__(self, dev, debug=False):
72        self._debug = debug
[138]73        self._clients = []
[133]74        self._tap = pytun.TunTapDevice(dev, pytun.IFF_TAP | pytun.IFF_NO_PI)
[138]75        self._tap.up()
[162]76        self._taplock = threading.Lock()
[133]77
[138]78    def fileno(self):
[162]79        with self._taplock:
[160]80            return self._tap.fileno()
[133]81
82    def register_client(self, client):
[162]83        self._clients.append(client)
[133]84
85    def unregister_client(self, client):
[162]86        self._clients.remove(client)
[133]87
88    def write(self, caller, message):
[162]89        clients = self._clients[:]
[133]90
[162]91        if caller is not self:
92            clients.remove(caller)
93
94            with self._taplock:
[138]95                self._tap.write(message)
[133]96
[162]97        for c in clients:
98            c.write_message(message, True)
[133]99
[138]100    def __call__(self, fd, events):
[162]101        buf = []
[133]102
[162]103        while True:
104            with self._taplock:
105                data = self._tap.read(self.READ_SIZE)
[135]106
[162]107            if data:
108                buf.append(data)
109
110            if len(data) < self.READ_SIZE:
111                break
112
113        self.write(self, ''.join(buf))
114
115
[160]116class EtherWebSocketHandler(tornado.websocket.WebSocketHandler, DebugMixIn):
[133]117    def __init__(self, app, req, tap, debug=False):
[160]118        super(EtherWebSocketHandler, self).__init__(app, req)
[133]119        self._tap = tap
120        self._debug = debug
121
122    def open(self):
123        self._tap.register_client(self)
[160]124        self.dprintf('connected: %s\n', self.request.remote_ip)
[133]125
126    def on_message(self, message):
[139]127        self._tap.write(self, message)
[160]128        self.dprintf('received: %s %s\n',
129                     self.request.remote_ip, message.encode('hex'))
[133]130
131    def on_close(self):
132        self._tap.unregister_client(self)
[160]133        self.dprintf('disconnected: %s\n', self.request.remote_ip)
[133]134
135
[160]136class EtherWebSocketClient(DebugMixIn):
137    def __init__(self, tap, url, user=None, passwd=None, debug=False):
[151]138        self._sock = None
139        self._tap = tap
140        self._url = url
[160]141        self._debug = debug
[151]142        self._options = {}
143
144        if user and passwd:
145            token = base64.b64encode('%s:%s' % (user, passwd))
146            auth = ['Authorization: Basic %s' % token]
147            self._options['header'] = auth
148
[160]149    @property
150    def closed(self):
151        return not self._sock
152
[151]153    def open(self):
[160]154        if not self.closed:
155            raise websocket.WebSocketException('already opened')
[151]156        self._sock = websocket.WebSocket()
157        self._sock.connect(self._url, **self._options)
[160]158        self.dprintf('connected: %s\n', self._url)
[151]159
160    def close(self):
[160]161        if self.closed:
162            raise websocket.WebSocketException('already closed')
[151]163        self._sock.close()
164        self._sock = None
[160]165        self.dprintf('disconnected: %s\n', self._url)
[151]166
167    def write_message(self, message, binary=False):
[160]168        if self.closed:
169            raise websocket.WebSocketException('closed socket')
[151]170        if binary:
171            flag = websocket.ABNF.OPCODE_BINARY
[160]172        else:
173            flag = websocket.ABNF.OPCODE_TEXT
[151]174        self._sock.send(message, flag)
[160]175        self.dprintf('sent: %s %s\n', self._url, message.encode('hex'))
[151]176
177    def run_forever(self):
178        try:
[160]179            if self.closed:
[151]180                self.open()
181            while True:
182                data = self._sock.recv()
183                if data is None:
184                    break
185                self._tap.write(self, data)
186        finally:
187            self.close()
188
189
[134]190def daemonize(nochdir=False, noclose=False):
191    if os.fork() > 0:
192        sys.exit(0)
193
194    os.setsid()
195
196    if os.fork() > 0:
197        sys.exit(0)
198
199    if not nochdir:
200        os.chdir('/')
201
202    if not noclose:
203        os.umask(0)
204        sys.stdin.close()
205        sys.stdout.close()
206        sys.stderr.close()
207        os.close(0)
208        os.close(1)
209        os.close(2)
210        sys.stdin = open(os.devnull)
211        sys.stdout = open(os.devnull, 'a')
212        sys.stderr = open(os.devnull, 'a')
213
214
[160]215def realpath(ns, *keys):
216    for k in keys:
217        v = getattr(ns, k, None)
218        if v is not None:
219            v = os.path.realpath(v)
220            setattr(ns, k, v)
221            open(v).close()  # check readable
222    return ns
223
224
[133]225def server_main(args):
[160]226    def wrap_basic_auth(cls, users):
227        o_exec = cls._execute
228
[150]229        if not users:
230            return cls
231
[160]232        def execute(self, transforms, *args, **kwargs):
[150]233            def auth_required():
234                self.stream.write(tornado.escape.utf8(
235                    'HTTP/1.1 401 Authorization Required\r\n'
236                    'WWW-Authenticate: Basic realm=etherws\r\n\r\n'
237                ))
238                self.stream.close()
239
[160]240            creds = self.request.headers.get('Authorization')
[150]241
[160]242            if not creds or not creds.startswith('Basic '):
243                return auth_required()
[150]244
[160]245            try:
246                name, passwd = base64.b64decode(creds[6:]).split(':', 1)
[150]247                passwd = base64.b64encode(hashlib.sha1(passwd).digest())
248
249                if name not in users or users[name] != passwd:
250                    return auth_required()
251
[160]252                return o_exec(self, transforms, *args, **kwargs)
[150]253
254            except:
255                return auth_required()
256
[160]257        cls._execute = execute
[150]258        return cls
259
260    def load_htpasswd(path):
261        users = {}
262        try:
263            with open(path) as fp:
264                for line in fp:
265                    line = line.strip()
266                    if 0 <= line.find(':'):
[160]267                        name, passwd = line.split(':', 1)
[150]268                        if passwd.startswith('{SHA}'):
269                            users[name] = passwd[5:]
[156]270            if not users:
[160]271                raise ValueError('no valid users found')
[150]272        except TypeError:
273            pass
274        return users
275
[160]276    realpath(args, 'keyfile', 'certfile', 'htpasswd')
[143]277
[160]278    if args.keyfile and args.certfile:
279        ssl_options = {'keyfile': args.keyfile, 'certfile': args.certfile}
280    elif args.keyfile or args.certfile:
[143]281        raise ValueError('both keyfile and certfile are required')
[160]282    else:
[143]283        ssl_options = None
284
[160]285    if args.port is None:
[143]286        if ssl_options:
287            args.port = 443
288        else:
289            args.port = 80
[160]290    elif not (0 <= args.port <= 65535):
291        raise ValueError('invalid port: %s' % args.port)
[143]292
[160]293    handler = wrap_basic_auth(EtherWebSocketHandler,
294                              load_htpasswd(args.htpasswd))
295
[138]296    tap = TapHandler(args.device, debug=args.debug)
[133]297    app = tornado.web.Application([
[150]298        (args.path, handler, {'tap': tap, 'debug': args.debug}),
[133]299    ])
[143]300    server = tornado.httpserver.HTTPServer(app, ssl_options=ssl_options)
[133]301    server.listen(args.port, address=args.address)
302
[138]303    ioloop = tornado.ioloop.IOLoop.instance()
304    ioloop.add_handler(tap.fileno(), tap, ioloop.READ)
[151]305
306    if not args.foreground:
307        daemonize()
308
[138]309    ioloop.start()
[133]310
311
312def client_main(args):
[160]313    realpath(args, 'cacerts')
314
[133]315    if args.debug:
316        websocket.enableTrace(True)
317
[156]318    if not args.insecure:
319        websocket._SSLSocketWrapper = \
320            lambda s: ssl.wrap_socket(s, cert_reqs=ssl.CERT_REQUIRED,
321                                      ca_certs=args.cacerts)
322
[160]323    if args.user and args.passwd is None:
324        args.passwd = getpass.getpass()
[143]325
[138]326    tap = TapHandler(args.device, debug=args.debug)
[160]327    client = EtherWebSocketClient(tap, args.uri,
328                                  args.user, args.passwd, args.debug)
[151]329
[133]330    tap.register_client(client)
[151]331    client.open()
[133]332
[138]333    ioloop = tornado.ioloop.IOLoop.instance()
334    ioloop.add_handler(tap.fileno(), tap, ioloop.READ)
[151]335
[160]336    t = threading.Thread(target=ioloop.start)
337    t.setDaemon(True)
338
[151]339    if not args.foreground:
340        daemonize()
341
342    t.start()
[160]343    client.run_forever()
[133]344
[138]345
[133]346def main():
347    parser = argparse.ArgumentParser()
348    parser.add_argument('--device', action='store', default='ethws%d')
349    parser.add_argument('--foreground', action='store_true', default=False)
350    parser.add_argument('--debug', action='store_true', default=False)
351
352    subparsers = parser.add_subparsers(dest='subcommand')
353
[158]354    parser_s = subparsers.add_parser('server')
355    parser_s.add_argument('--address', action='store', default='')
356    parser_s.add_argument('--port', action='store', type=int)
357    parser_s.add_argument('--path', action='store', default='/')
358    parser_s.add_argument('--htpasswd', action='store')
359    parser_s.add_argument('--keyfile', action='store')
360    parser_s.add_argument('--certfile', action='store')
[133]361
[158]362    parser_c = subparsers.add_parser('client')
363    parser_c.add_argument('--uri', action='store', required=True)
364    parser_c.add_argument('--insecure', action='store_true', default=False)
365    parser_c.add_argument('--cacerts', action='store')
366    parser_c.add_argument('--user', action='store')
[160]367    parser_c.add_argument('--passwd', action='store')
[133]368
369    args = parser.parse_args()
370
371    if args.subcommand == 'server':
372        server_main(args)
373    elif args.subcommand == 'client':
374        client_main(args)
375
376
377if __name__ == '__main__':
378    main()
Note: See TracBrowser for help on using the repository browser.