source: etherws/trunk/etherws.py @ 150

Revision 150, 8.4 KB checked in by atzm, 12 years ago (diff)
  • add basic auth support to server
  • 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#   - authentication support
14#   - servant mode support (like typical p2p software)
[140]15#
[133]16# ===========================================================================
17# Copyright (c) 2012, Atzm WATANABE <atzm@atzm.org>
18# All rights reserved.
19#
20# Redistribution and use in source and binary forms, with or without
21# modification, are permitted provided that the following conditions are met:
22#
23# 1. Redistributions of source code must retain the above copyright notice,
24#    this list of conditions and the following disclaimer.
25# 2. Redistributions in binary form must reproduce the above copyright
26#    notice, this list of conditions and the following disclaimer in the
27#    documentation and/or other materials provided with the distribution.
28#
29# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
30# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
31# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
32# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
33# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
34# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
35# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
36# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
37# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
38# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
39# POSSIBILITY OF SUCH DAMAGE.
40# ===========================================================================
41#
42# $Id$
43
44import os
45import sys
[143]46import time
[150]47import base64
48import hashlib
[133]49import argparse
50import threading
51
52import pytun
53import websocket
54import tornado.httpserver
55import tornado.ioloop
56import tornado.web
57import tornado.websocket
58
59
[138]60class TapHandler(object):
[133]61    def __init__(self, dev, debug=False):
62        self._debug = debug
[138]63        self._clients = []
[133]64        self._tap = pytun.TunTapDevice(dev, pytun.IFF_TAP | pytun.IFF_NO_PI)
[138]65        self._tap.up()
66        self._write_lock = threading.Lock()
[133]67
[138]68    def fileno(self):
69        return self._tap.fileno()
[133]70
71    def register_client(self, client):
[138]72        self._clients.append(client)
[133]73
74    def unregister_client(self, client):
[138]75        self._clients.remove(client)
[133]76
77    def write(self, caller, message):
78        if self._debug:
79            sys.stderr.write('%s: %s\n' % (caller.__class__.__name__,
80                                           message.encode('hex')))
81        try:
[138]82            self._write_lock.acquire()
[137]83
[133]84            clients = self._clients[:]
85
[137]86            if caller is not self:
87                clients.remove(caller)
[138]88                self._tap.write(message)
[133]89
[137]90            for c in clients:
[139]91                c.write_message(message, True)
[133]92
[137]93        finally:
[138]94            self._write_lock.release()
[137]95
[138]96    def __call__(self, fd, events):
97        self.write(self, self._tap.read(self._tap.mtu))
[133]98
[135]99
[133]100class EtherWebSocket(tornado.websocket.WebSocketHandler):
101    def __init__(self, app, req, tap, debug=False):
102        super(EtherWebSocket, self).__init__(app, req)
103        self._tap = tap
104        self._debug = debug
105
106    def open(self):
107        self._tap.register_client(self)
108
109    def on_message(self, message):
[139]110        self._tap.write(self, message)
[133]111
112    def on_close(self):
113        self._tap.unregister_client(self)
114
115
[134]116def daemonize(nochdir=False, noclose=False):
117    if os.fork() > 0:
118        sys.exit(0)
119
120    os.setsid()
121
122    if os.fork() > 0:
123        sys.exit(0)
124
125    if not nochdir:
126        os.chdir('/')
127
128    if not noclose:
129        os.umask(0)
130        sys.stdin.close()
131        sys.stdout.close()
132        sys.stderr.close()
133        os.close(0)
134        os.close(1)
135        os.close(2)
136        sys.stdin = open(os.devnull)
137        sys.stdout = open(os.devnull, 'a')
138        sys.stderr = open(os.devnull, 'a')
139
140
[133]141def server_main(args):
[150]142    def may_auth_required(cls, users):
143        if not users:
144            return cls
145
146        orig_execute = cls._execute
147
148        def _execute(self, transforms, *args, **kwargs):
149            def auth_required():
150                self.stream.write(tornado.escape.utf8(
151                    'HTTP/1.1 401 Authorization Required\r\n'
152                    'WWW-Authenticate: Basic realm=etherws\r\n\r\n'
153                ))
154                self.stream.close()
155
156            try:
157                creds = self.request.headers.get('Authorization')
158
159                if not creds or not creds.startswith('Basic '):
160                    return auth_required()
161
162                creds = base64.b64decode(creds[6:])
163
164                if creds.find(':') < 0:
165                    return auth_required()
166
167                name, passwd = creds.split(':', 2)
168                passwd = base64.b64encode(hashlib.sha1(passwd).digest())
169
170                if name not in users or users[name] != passwd:
171                    return auth_required()
172
173                return orig_execute(self, transforms, *args, **kwargs)
174
175            except:
176                return auth_required()
177
178        cls._execute = _execute
179        return cls
180
181    def load_htpasswd(path):
182        users = {}
183        try:
184            with open(path) as fp:
185                for line in fp:
186                    line = line.strip()
187                    if 0 <= line.find(':'):
188                        name, passwd = line.split(':', 2)
189                        if passwd.startswith('{SHA}'):
190                            users[name] = passwd[5:]
191        except TypeError:
192            pass
193        return users
194
195    handler = may_auth_required(EtherWebSocket, load_htpasswd(args.htpasswd))
[143]196    ssl_options = {}
197
198    for k in ['keyfile', 'certfile']:
199        v = getattr(args, k, None)
200        if v:
201            v = os.path.realpath(v)
202            ssl_options[k] = v
203            open(v).close()  # readable test
204
205    if len(ssl_options) == 1:
206        raise ValueError('both keyfile and certfile are required')
207    elif not ssl_options:
208        ssl_options = None
209
210    if not args.port:
211        if ssl_options:
212            args.port = 443
213        else:
214            args.port = 80
215
216    if not args.foreground:
217        daemonize()
218
[138]219    tap = TapHandler(args.device, debug=args.debug)
[133]220    app = tornado.web.Application([
[150]221        (args.path, handler, {'tap': tap, 'debug': args.debug}),
[133]222    ])
[143]223    server = tornado.httpserver.HTTPServer(app, ssl_options=ssl_options)
[133]224    server.listen(args.port, address=args.address)
225
[138]226    ioloop = tornado.ioloop.IOLoop.instance()
227    ioloop.add_handler(tap.fileno(), tap, ioloop.READ)
228    ioloop.start()
[133]229
230
231def client_main(args):
232    if args.debug:
233        websocket.enableTrace(True)
234
[143]235    if not args.foreground:
236        daemonize()
237
[138]238    tap = TapHandler(args.device, debug=args.debug)
[133]239    client = websocket.WebSocketApp(args.uri)
[139]240    client.on_message = lambda s, m: tap.write(client, m)
241    client.write_message = \
242        lambda m, b: client.sock.send(m, websocket.ABNF.OPCODE_BINARY)
[133]243    tap.register_client(client)
244
[138]245    t = threading.Thread(target=client.run_forever)
246    t.setDaemon(True)
247    t.start()
[133]248
[143]249    while not client.sock:
250        time.sleep(0.1)
251
[138]252    ioloop = tornado.ioloop.IOLoop.instance()
253    ioloop.add_handler(tap.fileno(), tap, ioloop.READ)
254    ioloop.start()
[133]255
[138]256
[133]257def main():
258    parser = argparse.ArgumentParser()
259    parser.add_argument('--device', action='store', default='ethws%d')
260    parser.add_argument('--foreground', action='store_true', default=False)
261    parser.add_argument('--debug', action='store_true', default=False)
262
263    subparsers = parser.add_subparsers(dest='subcommand')
264
265    parser_server = subparsers.add_parser('server')
266    parser_server.add_argument('--address', action='store', default='')
[143]267    parser_server.add_argument('--port', action='store', type=int)
[133]268    parser_server.add_argument('--path', action='store', default='/')
[150]269    parser_server.add_argument('--htpasswd', action='store')
[143]270    parser_server.add_argument('--keyfile', action='store')
271    parser_server.add_argument('--certfile', action='store')
[133]272
273    parser_client = subparsers.add_parser('client')
274    parser_client.add_argument('--uri', action='store', required=True)
275
276    args = parser.parse_args()
277
278    if args.subcommand == 'server':
279        server_main(args)
280    elif args.subcommand == 'client':
281        client_main(args)
282
283
284if __name__ == '__main__':
285    main()
Note: See TracBrowser for help on using the repository browser.