source: etherws/trunk/etherws.py @ 155

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