source: etherws/trunk/etherws.py @ 143

Revision 143, 6.6 KB checked in by atzm, 12 years ago (diff)
  • add SSL support
  • 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#   - authentication support
14#   - servant mode support (like typical p2p software)
15#
16# ===========================================================================
17# Copyright (c) 2012, Atzm WATANABE <atzm@atzm.org>
18# All rights reserved.
19#
20# Redistribution and use in source and binary forms, with or without
21# modification, are permitted provided that the following conditions are met:
22#
23# 1. Redistributions of source code must retain the above copyright notice,
24#    this list of conditions and the following disclaimer.
25# 2. Redistributions in binary form must reproduce the above copyright
26#    notice, this list of conditions and the following disclaimer in the
27#    documentation and/or other materials provided with the distribution.
28#
29# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
30# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
31# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
32# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
33# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
34# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
35# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
36# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
37# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
38# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
39# POSSIBILITY OF SUCH DAMAGE.
40# ===========================================================================
41#
42# $Id$
43
44import os
45import sys
46import time
47import argparse
48import threading
49
50import pytun
51import websocket
52import tornado.httpserver
53import tornado.ioloop
54import tornado.web
55import tornado.websocket
56
57
58class TapHandler(object):
59    def __init__(self, dev, debug=False):
60        self._debug = debug
61        self._clients = []
62        self._tap = pytun.TunTapDevice(dev, pytun.IFF_TAP | pytun.IFF_NO_PI)
63        self._tap.up()
64        self._write_lock = threading.Lock()
65
66    def fileno(self):
67        return self._tap.fileno()
68
69    def register_client(self, client):
70        self._clients.append(client)
71
72    def unregister_client(self, client):
73        self._clients.remove(client)
74
75    def write(self, caller, message):
76        if self._debug:
77            sys.stderr.write('%s: %s\n' % (caller.__class__.__name__,
78                                           message.encode('hex')))
79        try:
80            self._write_lock.acquire()
81
82            clients = self._clients[:]
83
84            if caller is not self:
85                clients.remove(caller)
86                self._tap.write(message)
87
88            for c in clients:
89                c.write_message(message, True)
90
91        finally:
92            self._write_lock.release()
93
94    def __call__(self, fd, events):
95        self.write(self, self._tap.read(self._tap.mtu))
96
97
98class EtherWebSocket(tornado.websocket.WebSocketHandler):
99    def __init__(self, app, req, tap, debug=False):
100        super(EtherWebSocket, self).__init__(app, req)
101        self._tap = tap
102        self._debug = debug
103
104    def open(self):
105        self._tap.register_client(self)
106
107    def on_message(self, message):
108        self._tap.write(self, message)
109
110    def on_close(self):
111        self._tap.unregister_client(self)
112
113
114def daemonize(nochdir=False, noclose=False):
115    if os.fork() > 0:
116        sys.exit(0)
117
118    os.setsid()
119
120    if os.fork() > 0:
121        sys.exit(0)
122
123    if not nochdir:
124        os.chdir('/')
125
126    if not noclose:
127        os.umask(0)
128        sys.stdin.close()
129        sys.stdout.close()
130        sys.stderr.close()
131        os.close(0)
132        os.close(1)
133        os.close(2)
134        sys.stdin = open(os.devnull)
135        sys.stdout = open(os.devnull, 'a')
136        sys.stderr = open(os.devnull, 'a')
137
138
139def server_main(args):
140    ssl_options = {}
141
142    for k in ['keyfile', 'certfile']:
143        v = getattr(args, k, None)
144        if v:
145            v = os.path.realpath(v)
146            ssl_options[k] = v
147            open(v).close()  # readable test
148
149    if len(ssl_options) == 1:
150        raise ValueError('both keyfile and certfile are required')
151    elif not ssl_options:
152        ssl_options = None
153
154    if not args.port:
155        if ssl_options:
156            args.port = 443
157        else:
158            args.port = 80
159
160    if not args.foreground:
161        daemonize()
162
163    tap = TapHandler(args.device, debug=args.debug)
164    app = tornado.web.Application([
165        (args.path, EtherWebSocket, {'tap': tap, 'debug': args.debug}),
166    ])
167    server = tornado.httpserver.HTTPServer(app, ssl_options=ssl_options)
168    server.listen(args.port, address=args.address)
169
170    ioloop = tornado.ioloop.IOLoop.instance()
171    ioloop.add_handler(tap.fileno(), tap, ioloop.READ)
172    ioloop.start()
173
174
175def client_main(args):
176    if args.debug:
177        websocket.enableTrace(True)
178
179    if not args.foreground:
180        daemonize()
181
182    tap = TapHandler(args.device, debug=args.debug)
183    client = websocket.WebSocketApp(args.uri)
184    client.on_message = lambda s, m: tap.write(client, m)
185    client.write_message = \
186        lambda m, b: client.sock.send(m, websocket.ABNF.OPCODE_BINARY)
187    tap.register_client(client)
188
189    t = threading.Thread(target=client.run_forever)
190    t.setDaemon(True)
191    t.start()
192
193    while not client.sock:
194        time.sleep(0.1)
195
196    ioloop = tornado.ioloop.IOLoop.instance()
197    ioloop.add_handler(tap.fileno(), tap, ioloop.READ)
198    ioloop.start()
199
200
201def main():
202    parser = argparse.ArgumentParser()
203    parser.add_argument('--device', action='store', default='ethws%d')
204    parser.add_argument('--foreground', action='store_true', default=False)
205    parser.add_argument('--debug', action='store_true', default=False)
206
207    subparsers = parser.add_subparsers(dest='subcommand')
208
209    parser_server = subparsers.add_parser('server')
210    parser_server.add_argument('--address', action='store', default='')
211    parser_server.add_argument('--port', action='store', type=int)
212    parser_server.add_argument('--path', action='store', default='/')
213    parser_server.add_argument('--keyfile', action='store')
214    parser_server.add_argument('--certfile', action='store')
215
216    parser_client = subparsers.add_parser('client')
217    parser_client.add_argument('--uri', action='store', required=True)
218
219    args = parser.parse_args()
220
221    if args.subcommand == 'server':
222        server_main(args)
223    elif args.subcommand == 'client':
224        client_main(args)
225
226
227if __name__ == '__main__':
228    main()
Note: See TracBrowser for help on using the repository browser.