source: etherws/trunk/etherws.py @ 158

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