source: etherws/trunk/etherws.py @ 150

Revision 150, 8.4 KB checked in by atzm, 13 years ago (diff)
  • add basic auth support to server
  • Property svn:keywords set to Id
Line 
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3#
4#              Ethernet over WebSocket tunneling server/client
5#
6# depends on:
7#   - python-2.7.2
8#   - python-pytun-0.2
9#   - websocket-client-0.7.0
10#   - tornado-2.2.1
11#
12# todo:
13#   - authentication support
14#   - servant mode support (like typical p2p software)
15#
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
46import time
47import base64
48import hashlib
49import argparse
50import threading
51
52import pytun
53import websocket
54import tornado.httpserver
55import tornado.ioloop
56import tornado.web
57import tornado.websocket
58
59
60class TapHandler(object):
61    def __init__(self, dev, debug=False):
62        self._debug = debug
63        self._clients = []
64        self._tap = pytun.TunTapDevice(dev, pytun.IFF_TAP | pytun.IFF_NO_PI)
65        self._tap.up()
66        self._write_lock = threading.Lock()
67
68    def fileno(self):
69        return self._tap.fileno()
70
71    def register_client(self, client):
72        self._clients.append(client)
73
74    def unregister_client(self, client):
75        self._clients.remove(client)
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:
82            self._write_lock.acquire()
83
84            clients = self._clients[:]
85
86            if caller is not self:
87                clients.remove(caller)
88                self._tap.write(message)
89
90            for c in clients:
91                c.write_message(message, True)
92
93        finally:
94            self._write_lock.release()
95
96    def __call__(self, fd, events):
97        self.write(self, self._tap.read(self._tap.mtu))
98
99
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):
110        self._tap.write(self, message)
111
112    def on_close(self):
113        self._tap.unregister_client(self)
114
115
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
141def server_main(args):
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))
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
219    tap = TapHandler(args.device, debug=args.debug)
220    app = tornado.web.Application([
221        (args.path, handler, {'tap': tap, 'debug': args.debug}),
222    ])
223    server = tornado.httpserver.HTTPServer(app, ssl_options=ssl_options)
224    server.listen(args.port, address=args.address)
225
226    ioloop = tornado.ioloop.IOLoop.instance()
227    ioloop.add_handler(tap.fileno(), tap, ioloop.READ)
228    ioloop.start()
229
230
231def client_main(args):
232    if args.debug:
233        websocket.enableTrace(True)
234
235    if not args.foreground:
236        daemonize()
237
238    tap = TapHandler(args.device, debug=args.debug)
239    client = websocket.WebSocketApp(args.uri)
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)
243    tap.register_client(client)
244
245    t = threading.Thread(target=client.run_forever)
246    t.setDaemon(True)
247    t.start()
248
249    while not client.sock:
250        time.sleep(0.1)
251
252    ioloop = tornado.ioloop.IOLoop.instance()
253    ioloop.add_handler(tap.fileno(), tap, ioloop.READ)
254    ioloop.start()
255
256
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='')
267    parser_server.add_argument('--port', action='store', type=int)
268    parser_server.add_argument('--path', action='store', default='/')
269    parser_server.add_argument('--htpasswd', action='store')
270    parser_server.add_argument('--keyfile', action='store')
271    parser_server.add_argument('--certfile', action='store')
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.