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
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        with self._write_lock:
81            clients = self._clients[:]
82
83            if caller is not self:
84                clients.remove(caller)
85                self._tap.write(message)
86
87            for c in clients:
88                c.write_message(message, True)
89
90    def __call__(self, fd, events):
91        self.write(self, self._tap.read(self._tap.mtu))
92
93
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):
104        self._tap.write(self, message)
105
106    def on_close(self):
107        self._tap.unregister_client(self)
108
109
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
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
174def server_main(args):
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))
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
249    tap = TapHandler(args.device, debug=args.debug)
250    app = tornado.web.Application([
251        (args.path, handler, {'tap': tap, 'debug': args.debug}),
252    ])
253    server = tornado.httpserver.HTTPServer(app, ssl_options=ssl_options)
254    server.listen(args.port, address=args.address)
255
256    ioloop = tornado.ioloop.IOLoop.instance()
257    ioloop.add_handler(tap.fileno(), tap, ioloop.READ)
258
259    if not args.foreground:
260        daemonize()
261
262    ioloop.start()
263
264
265def client_main(args):
266    if args.debug:
267        websocket.enableTrace(True)
268
269    passwd = None
270    if args.user:
271        passwd = getpass.getpass()
272
273    tap = TapHandler(args.device, debug=args.debug)
274    client = EtherWebSocketClient(tap, args.uri, args.user, passwd)
275
276    tap.register_client(client)
277    client.open()
278
279    t = threading.Thread(target=client.run_forever)
280    t.setDaemon(True)
281
282    ioloop = tornado.ioloop.IOLoop.instance()
283    ioloop.add_handler(tap.fileno(), tap, ioloop.READ)
284
285    if not args.foreground:
286        daemonize()
287
288    t.start()
289    ioloop.start()
290
291
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='')
302    parser_server.add_argument('--port', action='store', type=int)
303    parser_server.add_argument('--path', action='store', default='/')
304    parser_server.add_argument('--htpasswd', action='store')
305    parser_server.add_argument('--keyfile', action='store')
306    parser_server.add_argument('--certfile', action='store')
307
308    parser_client = subparsers.add_parser('client')
309    parser_client.add_argument('--uri', action='store', required=True)
310    parser_client.add_argument('--user', action='store')
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.