source: etherws/trunk/etherws.py @ 151

Revision 151, 9.4 KB checked in by atzm, 13 years ago (diff)
  • add basic auth support to client
  • 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#   - servant mode support (like typical p2p software)
14#
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
45import base64
46import hashlib
47import getpass
48import argparse
49import threading
50
51import pytun
52import websocket
53import tornado.httpserver
54import tornado.ioloop
55import tornado.web
56import tornado.websocket
57
58
59class TapHandler(object):
60    def __init__(self, dev, debug=False):
61        self._debug = debug
62        self._clients = []
63        self._tap = pytun.TunTapDevice(dev, pytun.IFF_TAP | pytun.IFF_NO_PI)
64        self._tap.up()
65        self._write_lock = threading.Lock()
66
67    def fileno(self):
68        return self._tap.fileno()
69
70    def register_client(self, client):
71        self._clients.append(client)
72
73    def unregister_client(self, client):
74        self._clients.remove(client)
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:
81            self._write_lock.acquire()
82
83            clients = self._clients[:]
84
85            if caller is not self:
86                clients.remove(caller)
87                self._tap.write(message)
88
89            for c in clients:
90                c.write_message(message, True)
91
92        finally:
93            self._write_lock.release()
94
95    def __call__(self, fd, events):
96        self.write(self, self._tap.read(self._tap.mtu))
97
98
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):
109        self._tap.write(self, message)
110
111    def on_close(self):
112        self._tap.unregister_client(self)
113
114
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
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
179def server_main(args):
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))
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
254    tap = TapHandler(args.device, debug=args.debug)
255    app = tornado.web.Application([
256        (args.path, handler, {'tap': tap, 'debug': args.debug}),
257    ])
258    server = tornado.httpserver.HTTPServer(app, ssl_options=ssl_options)
259    server.listen(args.port, address=args.address)
260
261    ioloop = tornado.ioloop.IOLoop.instance()
262    ioloop.add_handler(tap.fileno(), tap, ioloop.READ)
263
264    if not args.foreground:
265        daemonize()
266
267    ioloop.start()
268
269
270def client_main(args):
271    if args.debug:
272        websocket.enableTrace(True)
273
274    passwd = None
275    if args.user:
276        passwd = getpass.getpass()
277
278    tap = TapHandler(args.device, debug=args.debug)
279    client = EtherWebSocketClient(tap, args.uri, args.user, passwd)
280
281    tap.register_client(client)
282    client.open()
283
284    t = threading.Thread(target=client.run_forever)
285    t.setDaemon(True)
286
287    ioloop = tornado.ioloop.IOLoop.instance()
288    ioloop.add_handler(tap.fileno(), tap, ioloop.READ)
289
290    if not args.foreground:
291        daemonize()
292
293    t.start()
294    ioloop.start()
295
296
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='')
307    parser_server.add_argument('--port', action='store', type=int)
308    parser_server.add_argument('--path', action='store', default='/')
309    parser_server.add_argument('--htpasswd', action='store')
310    parser_server.add_argument('--keyfile', action='store')
311    parser_server.add_argument('--certfile', action='store')
312
313    parser_client = subparsers.add_parser('client')
314    parser_client.add_argument('--uri', action='store', required=True)
315    parser_client.add_argument('--user', action='store')
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.