source: etherws/tags/release-0.5/etherws.py @ 161

Revision 161, 11.0 KB checked in by atzm, 12 years ago (diff)
  • add tag release-0.5
  • 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):
[133]69    def __init__(self, dev, debug=False):
70        self._debug = debug
[138]71        self._clients = []
[133]72        self._tap = pytun.TunTapDevice(dev, pytun.IFF_TAP | pytun.IFF_NO_PI)
[138]73        self._tap.up()
[160]74        self._glock = threading.Lock()
[133]75
[138]76    def fileno(self):
[160]77        with self._glock:
78            return self._tap.fileno()
[133]79
80    def register_client(self, client):
[160]81        with self._glock:
82            self._clients.append(client)
[133]83
84    def unregister_client(self, client):
[160]85        with self._glock:
86            self._clients.remove(client)
[133]87
88    def write(self, caller, message):
[160]89        with self._glock:
[133]90            clients = self._clients[:]
91
[137]92            if caller is not self:
93                clients.remove(caller)
[138]94                self._tap.write(message)
[133]95
[137]96            for c in clients:
[139]97                c.write_message(message, True)
[133]98
[138]99    def __call__(self, fd, events):
[160]100        with self._glock:
101            data = self._tap.read(self._tap.mtu)
102        self.write(self, data)
[133]103
[135]104
[160]105class EtherWebSocketHandler(tornado.websocket.WebSocketHandler, DebugMixIn):
[133]106    def __init__(self, app, req, tap, debug=False):
[160]107        super(EtherWebSocketHandler, self).__init__(app, req)
[133]108        self._tap = tap
109        self._debug = debug
110
111    def open(self):
112        self._tap.register_client(self)
[160]113        self.dprintf('connected: %s\n', self.request.remote_ip)
[133]114
115    def on_message(self, message):
[139]116        self._tap.write(self, message)
[160]117        self.dprintf('received: %s %s\n',
118                     self.request.remote_ip, message.encode('hex'))
[133]119
120    def on_close(self):
121        self._tap.unregister_client(self)
[160]122        self.dprintf('disconnected: %s\n', self.request.remote_ip)
[133]123
124
[160]125class EtherWebSocketClient(DebugMixIn):
126    def __init__(self, tap, url, user=None, passwd=None, debug=False):
[151]127        self._sock = None
128        self._tap = tap
129        self._url = url
[160]130        self._debug = debug
[151]131        self._options = {}
132
133        if user and passwd:
134            token = base64.b64encode('%s:%s' % (user, passwd))
135            auth = ['Authorization: Basic %s' % token]
136            self._options['header'] = auth
137
[160]138    @property
139    def closed(self):
140        return not self._sock
141
[151]142    def open(self):
[160]143        if not self.closed:
144            raise websocket.WebSocketException('already opened')
[151]145        self._sock = websocket.WebSocket()
146        self._sock.connect(self._url, **self._options)
[160]147        self.dprintf('connected: %s\n', self._url)
[151]148
149    def close(self):
[160]150        if self.closed:
151            raise websocket.WebSocketException('already closed')
[151]152        self._sock.close()
153        self._sock = None
[160]154        self.dprintf('disconnected: %s\n', self._url)
[151]155
156    def write_message(self, message, binary=False):
[160]157        if self.closed:
158            raise websocket.WebSocketException('closed socket')
[151]159        if binary:
160            flag = websocket.ABNF.OPCODE_BINARY
[160]161        else:
162            flag = websocket.ABNF.OPCODE_TEXT
[151]163        self._sock.send(message, flag)
[160]164        self.dprintf('sent: %s %s\n', self._url, message.encode('hex'))
[151]165
166    def run_forever(self):
167        try:
[160]168            if self.closed:
[151]169                self.open()
170            while True:
171                data = self._sock.recv()
172                if data is None:
173                    break
174                self._tap.write(self, data)
175        finally:
176            self.close()
177
178
[134]179def daemonize(nochdir=False, noclose=False):
180    if os.fork() > 0:
181        sys.exit(0)
182
183    os.setsid()
184
185    if os.fork() > 0:
186        sys.exit(0)
187
188    if not nochdir:
189        os.chdir('/')
190
191    if not noclose:
192        os.umask(0)
193        sys.stdin.close()
194        sys.stdout.close()
195        sys.stderr.close()
196        os.close(0)
197        os.close(1)
198        os.close(2)
199        sys.stdin = open(os.devnull)
200        sys.stdout = open(os.devnull, 'a')
201        sys.stderr = open(os.devnull, 'a')
202
203
[160]204def realpath(ns, *keys):
205    for k in keys:
206        v = getattr(ns, k, None)
207        if v is not None:
208            v = os.path.realpath(v)
209            setattr(ns, k, v)
210            open(v).close()  # check readable
211    return ns
212
213
[133]214def server_main(args):
[160]215    def wrap_basic_auth(cls, users):
216        o_exec = cls._execute
217
[150]218        if not users:
219            return cls
220
[160]221        def execute(self, transforms, *args, **kwargs):
[150]222            def auth_required():
223                self.stream.write(tornado.escape.utf8(
224                    'HTTP/1.1 401 Authorization Required\r\n'
225                    'WWW-Authenticate: Basic realm=etherws\r\n\r\n'
226                ))
227                self.stream.close()
228
[160]229            creds = self.request.headers.get('Authorization')
[150]230
[160]231            if not creds or not creds.startswith('Basic '):
232                return auth_required()
[150]233
[160]234            try:
235                name, passwd = base64.b64decode(creds[6:]).split(':', 1)
[150]236                passwd = base64.b64encode(hashlib.sha1(passwd).digest())
237
238                if name not in users or users[name] != passwd:
239                    return auth_required()
240
[160]241                return o_exec(self, transforms, *args, **kwargs)
[150]242
243            except:
244                return auth_required()
245
[160]246        cls._execute = execute
[150]247        return cls
248
249    def load_htpasswd(path):
250        users = {}
251        try:
252            with open(path) as fp:
253                for line in fp:
254                    line = line.strip()
255                    if 0 <= line.find(':'):
[160]256                        name, passwd = line.split(':', 1)
[150]257                        if passwd.startswith('{SHA}'):
258                            users[name] = passwd[5:]
[156]259            if not users:
[160]260                raise ValueError('no valid users found')
[150]261        except TypeError:
262            pass
263        return users
264
[160]265    realpath(args, 'keyfile', 'certfile', 'htpasswd')
[143]266
[160]267    if args.keyfile and args.certfile:
268        ssl_options = {'keyfile': args.keyfile, 'certfile': args.certfile}
269    elif args.keyfile or args.certfile:
[143]270        raise ValueError('both keyfile and certfile are required')
[160]271    else:
[143]272        ssl_options = None
273
[160]274    if args.port is None:
[143]275        if ssl_options:
276            args.port = 443
277        else:
278            args.port = 80
[160]279    elif not (0 <= args.port <= 65535):
280        raise ValueError('invalid port: %s' % args.port)
[143]281
[160]282    handler = wrap_basic_auth(EtherWebSocketHandler,
283                              load_htpasswd(args.htpasswd))
284
[138]285    tap = TapHandler(args.device, debug=args.debug)
[133]286    app = tornado.web.Application([
[150]287        (args.path, handler, {'tap': tap, 'debug': args.debug}),
[133]288    ])
[143]289    server = tornado.httpserver.HTTPServer(app, ssl_options=ssl_options)
[133]290    server.listen(args.port, address=args.address)
291
[138]292    ioloop = tornado.ioloop.IOLoop.instance()
293    ioloop.add_handler(tap.fileno(), tap, ioloop.READ)
[151]294
295    if not args.foreground:
296        daemonize()
297
[138]298    ioloop.start()
[133]299
300
301def client_main(args):
[160]302    realpath(args, 'cacerts')
303
[133]304    if args.debug:
305        websocket.enableTrace(True)
306
[156]307    if not args.insecure:
308        websocket._SSLSocketWrapper = \
309            lambda s: ssl.wrap_socket(s, cert_reqs=ssl.CERT_REQUIRED,
310                                      ca_certs=args.cacerts)
311
[160]312    if args.user and args.passwd is None:
313        args.passwd = getpass.getpass()
[143]314
[138]315    tap = TapHandler(args.device, debug=args.debug)
[160]316    client = EtherWebSocketClient(tap, args.uri,
317                                  args.user, args.passwd, args.debug)
[151]318
[133]319    tap.register_client(client)
[151]320    client.open()
[133]321
[138]322    ioloop = tornado.ioloop.IOLoop.instance()
323    ioloop.add_handler(tap.fileno(), tap, ioloop.READ)
[151]324
[160]325    t = threading.Thread(target=ioloop.start)
326    t.setDaemon(True)
327
[151]328    if not args.foreground:
329        daemonize()
330
331    t.start()
[160]332    client.run_forever()
[133]333
[138]334
[133]335def main():
336    parser = argparse.ArgumentParser()
337    parser.add_argument('--device', action='store', default='ethws%d')
338    parser.add_argument('--foreground', action='store_true', default=False)
339    parser.add_argument('--debug', action='store_true', default=False)
340
341    subparsers = parser.add_subparsers(dest='subcommand')
342
[158]343    parser_s = subparsers.add_parser('server')
344    parser_s.add_argument('--address', action='store', default='')
345    parser_s.add_argument('--port', action='store', type=int)
346    parser_s.add_argument('--path', action='store', default='/')
347    parser_s.add_argument('--htpasswd', action='store')
348    parser_s.add_argument('--keyfile', action='store')
349    parser_s.add_argument('--certfile', action='store')
[133]350
[158]351    parser_c = subparsers.add_parser('client')
352    parser_c.add_argument('--uri', action='store', required=True)
353    parser_c.add_argument('--insecure', action='store_true', default=False)
354    parser_c.add_argument('--cacerts', action='store')
355    parser_c.add_argument('--user', action='store')
[160]356    parser_c.add_argument('--passwd', action='store')
[133]357
358    args = parser.parse_args()
359
360    if args.subcommand == 'server':
361        server_main(args)
362    elif args.subcommand == 'client':
363        client_main(args)
364
365
366if __name__ == '__main__':
367    main()
Note: See TracBrowser for help on using the repository browser.