source: etherws/trunk/etherws.py @ 137

Revision 137, 7.1 KB checked in by atzm, 12 years ago (diff)
  • change lock range
  • Property svn:keywords set to Id
Line 
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3#
4#               EtherWebSocket 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#   - direct binary transmission support (to improve performance)
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 select
47import argparse
48import threading
49
50import pytun
51import websocket
52import tornado.httpserver
53import tornado.ioloop
54import tornado.web
55import tornado.websocket
56
57
58class TapListener(threading.Thread):
59    daemon = True
60
61    def __init__(self, dev, debug=False):
62        super(TapListener, self).__init__()
63
64        self._debug = debug
65        self._tap = pytun.TunTapDevice(dev, pytun.IFF_TAP | pytun.IFF_NO_PI)
66        self._tap_lock = threading.Lock()
67
68        self._clients = []
69        self._clients_lock = threading.Lock()
70
71        try:
72            self._tap_lock.acquire()
73            self._tap.up()
74        finally:
75            self._tap_lock.release()
76
77    def register_client(self, client):
78        try:
79            self._clients_lock.acquire()
80            self._clients.append(client)
81        finally:
82            self._clients_lock.release()
83
84    def unregister_client(self, client):
85        try:
86            self._clients_lock.acquire()
87            self._clients.remove(client)
88        except ValueError:
89            pass
90        finally:
91            self._clients_lock.release()
92
93    def write(self, caller, message):
94        if self._debug:
95            sys.stderr.write('%s: %s\n' % (caller.__class__.__name__,
96                                           message.encode('hex')))
97
98        try:
99            self._clients_lock.acquire()
100
101            clients = self._clients[:]
102
103            if caller is not self:
104                clients.remove(caller)
105                try:
106                    self._tap_lock.acquire()
107                    self._tap.write(message)
108                finally:
109                    self._tap_lock.release()
110
111            message = base64.b64encode(message)
112
113            for c in clients:
114                c.write_message(message)
115
116        finally:
117            self._clients_lock.release()
118
119    def run(self):
120        epoll = select.epoll()
121
122        try:
123            self._tap_lock.acquire()
124            epoll.register(self._tap.fileno(), select.EPOLLIN)
125        finally:
126            self._tap_lock.release()
127
128        while True:
129            evts = epoll.poll(1)
130            for fileno, evt in evts:
131                try:
132                    self._tap_lock.acquire()
133                    data = self._tap.read(self._tap.mtu)
134                finally:
135                    self._tap_lock.release()
136                self.write(self, data)
137
138
139class EtherWebSocket(tornado.websocket.WebSocketHandler):
140    def __init__(self, app, req, tap, debug=False):
141        super(EtherWebSocket, self).__init__(app, req)
142        self._tap = tap
143        self._debug = debug
144
145    def open(self):
146        if self._debug:
147            sys.stderr.write('[%s] opened\n' % self.request.remote_ip)
148        self._tap.register_client(self)
149
150    def on_message(self, message):
151        if self._debug:
152            sys.stderr.write('[%s] received\n' % self.request.remote_ip)
153        self._tap.write(self, base64.b64decode(message))
154
155    def on_close(self):
156        if self._debug:
157            sys.stderr.write('[%s] closed\n' % self.request.remote_ip)
158        self._tap.unregister_client(self)
159
160
161def daemonize(nochdir=False, noclose=False):
162    if os.fork() > 0:
163        sys.exit(0)
164
165    os.setsid()
166
167    if os.fork() > 0:
168        sys.exit(0)
169
170    if not nochdir:
171        os.chdir('/')
172
173    if not noclose:
174        os.umask(0)
175        sys.stdin.close()
176        sys.stdout.close()
177        sys.stderr.close()
178        os.close(0)
179        os.close(1)
180        os.close(2)
181        sys.stdin = open(os.devnull)
182        sys.stdout = open(os.devnull, 'a')
183        sys.stderr = open(os.devnull, 'a')
184
185
186def server_main(args):
187    tap = TapListener(args.device, debug=args.debug)
188    tap.start()
189
190    app = tornado.web.Application([
191        (args.path, EtherWebSocket, {'tap': tap, 'debug': args.debug}),
192    ])
193    server = tornado.httpserver.HTTPServer(app)
194    server.listen(args.port, address=args.address)
195
196    tornado.ioloop.IOLoop.instance().start()
197
198
199def client_main(args):
200    if args.debug:
201        websocket.enableTrace(True)
202
203    tap = TapListener(args.device, debug=args.debug)
204    client = websocket.WebSocketApp(args.uri)
205    client.write_message = client.send
206    client.on_message = lambda s, m: tap.write(client, base64.b64decode(m))
207
208    if args.debug:
209        client.on_error = lambda s, e: sys.stderr.write(str(e) + '\n')
210        client.on_close = lambda s: sys.stderr.write('closed\n')
211
212    tap.register_client(client)
213    tap.start()
214
215    client.run_forever()
216
217
218def main():
219    parser = argparse.ArgumentParser()
220    parser.add_argument('--device', action='store', default='ethws%d')
221    parser.add_argument('--foreground', action='store_true', default=False)
222    parser.add_argument('--debug', action='store_true', default=False)
223
224    subparsers = parser.add_subparsers(dest='subcommand')
225
226    parser_server = subparsers.add_parser('server')
227    parser_server.add_argument('--address', action='store', default='')
228    parser_server.add_argument('--port', action='store', type=int, default=80)
229    parser_server.add_argument('--path', action='store', default='/')
230
231    parser_client = subparsers.add_parser('client')
232    parser_client.add_argument('--uri', action='store', required=True)
233
234    args = parser.parse_args()
235
236    if not args.foreground:
237        daemonize()
238
239    if args.subcommand == 'server':
240        server_main(args)
241    elif args.subcommand == 'client':
242        client_main(args)
243
244
245if __name__ == '__main__':
246    main()
Note: See TracBrowser for help on using the repository browser.