source: etherws/trunk/etherws.py @ 151

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