Changeset 160 for etherws


Ignore:
Timestamp:
05/20/12 02:39:34 (13 years ago)
Author:
atzm
Message:
  • fixed some bugs
Location:
etherws/trunk
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • etherws/trunk/README.rst

    r158 r160  
    110110It will return *401 Authorization Required*. 
    111111 
    112 On client side, etherws requires username as option, and password from 
    113 stdin:: 
     112On client side, etherws requires username from option, and password from 
     113option or stdin:: 
    114114 
     115  # etherws client --uri ws://<address>/ --user username --passwd password 
    115116  # etherws client --uri ws://<address>/ --user username 
    116117  Password:  
     
    123124History 
    124125======= 
     1260.5 (2012-05-20 JST) 
     127  - added passwd option to client mode 
     128  - fixed bug: basic authentication password cannot contain colon 
     129  - fixed bug: client loops meaninglessly even if server stops 
     130 
    1251310.4 (2012-05-19 JST) 
    126132  - server certificate verification support 
  • etherws/trunk/etherws.py

    r158 r160  
    4444import sys 
    4545import ssl 
     46import time 
    4647import base64 
    4748import hashlib 
     
    5253import pytun 
    5354import websocket 
     55import tornado.web 
     56import tornado.ioloop 
     57import tornado.websocket 
    5458import tornado.httpserver 
    55 import tornado.ioloop 
    56 import tornado.web 
    57 import tornado.websocket 
    58  
    59  
    60 class TapHandler(object): 
     59 
     60 
     61class DebugMixIn(object): 
     62    def dprintf(self, msg, *args): 
     63        if self._debug: 
     64            prefix = '[%s] %s - ' % (time.asctime(), self.__class__.__name__) 
     65            sys.stderr.write(prefix + (msg % args)) 
     66 
     67 
     68class TapHandler(DebugMixIn): 
    6169    def __init__(self, dev, debug=False): 
    6270        self._debug = debug 
     
    6472        self._tap = pytun.TunTapDevice(dev, pytun.IFF_TAP | pytun.IFF_NO_PI) 
    6573        self._tap.up() 
    66         self._write_lock = threading.Lock() 
     74        self._glock = threading.Lock() 
    6775 
    6876    def fileno(self): 
    69         return self._tap.fileno() 
     77        with self._glock: 
     78            return self._tap.fileno() 
    7079 
    7180    def register_client(self, client): 
    72         self._clients.append(client) 
     81        with self._glock: 
     82            self._clients.append(client) 
    7383 
    7484    def unregister_client(self, client): 
    75         self._clients.remove(client) 
     85        with self._glock: 
     86            self._clients.remove(client) 
    7687 
    7788    def write(self, caller, message): 
    78         if self._debug: 
    79             sys.stderr.write('%s: %s\n' % (caller.__class__.__name__, 
    80                                            message.encode('hex'))) 
    81         with self._write_lock: 
     89        with self._glock: 
    8290            clients = self._clients[:] 
    8391 
     
    9098 
    9199    def __call__(self, fd, events): 
    92         self.write(self, self._tap.read(self._tap.mtu)) 
    93  
    94  
    95 class EtherWebSocket(tornado.websocket.WebSocketHandler): 
     100        with self._glock: 
     101            data = self._tap.read(self._tap.mtu) 
     102        self.write(self, data) 
     103 
     104 
     105class EtherWebSocketHandler(tornado.websocket.WebSocketHandler, DebugMixIn): 
    96106    def __init__(self, app, req, tap, debug=False): 
    97         super(EtherWebSocket, self).__init__(app, req) 
     107        super(EtherWebSocketHandler, self).__init__(app, req) 
    98108        self._tap = tap 
    99109        self._debug = debug 
     
    101111    def open(self): 
    102112        self._tap.register_client(self) 
     113        self.dprintf('connected: %s\n', self.request.remote_ip) 
    103114 
    104115    def on_message(self, message): 
    105116        self._tap.write(self, message) 
     117        self.dprintf('received: %s %s\n', 
     118                     self.request.remote_ip, message.encode('hex')) 
    106119 
    107120    def on_close(self): 
    108121        self._tap.unregister_client(self) 
    109  
    110  
    111 class  EtherWebSocketClient(object): 
    112     def __init__(self, tap, url, user=None, passwd=None): 
     122        self.dprintf('disconnected: %s\n', self.request.remote_ip) 
     123 
     124 
     125class EtherWebSocketClient(DebugMixIn): 
     126    def __init__(self, tap, url, user=None, passwd=None, debug=False): 
    113127        self._sock = None 
    114128        self._tap = tap 
    115129        self._url = url 
     130        self._debug = debug 
    116131        self._options = {} 
    117132 
     
    121136            self._options['header'] = auth 
    122137 
     138    @property 
     139    def closed(self): 
     140        return not self._sock 
     141 
    123142    def open(self): 
     143        if not self.closed: 
     144            raise websocket.WebSocketException('already opened') 
    124145        self._sock = websocket.WebSocket() 
    125146        self._sock.connect(self._url, **self._options) 
     147        self.dprintf('connected: %s\n', self._url) 
    126148 
    127149    def close(self): 
     150        if self.closed: 
     151            raise websocket.WebSocketException('already closed') 
    128152        self._sock.close() 
    129153        self._sock = None 
     154        self.dprintf('disconnected: %s\n', self._url) 
    130155 
    131156    def write_message(self, message, binary=False): 
    132         flag = websocket.ABNF.OPCODE_TEXT 
     157        if self.closed: 
     158            raise websocket.WebSocketException('closed socket') 
    133159        if binary: 
    134160            flag = websocket.ABNF.OPCODE_BINARY 
     161        else: 
     162            flag = websocket.ABNF.OPCODE_TEXT 
    135163        self._sock.send(message, flag) 
     164        self.dprintf('sent: %s %s\n', self._url, message.encode('hex')) 
    136165 
    137166    def run_forever(self): 
    138167        try: 
    139             if not self._sock: 
     168            if self.closed: 
    140169                self.open() 
    141170            while True: 
     
    173202 
    174203 
     204def realpath(ns, *keys): 
     205    for k in keys: 
     206        v = getattr(ns, k, None) 
     207        if v is not None: 
     208            v = os.path.realpath(v) 
     209            setattr(ns, k, v) 
     210            open(v).close()  # check readable 
     211    return ns 
     212 
     213 
    175214def server_main(args): 
    176     def may_auth_required(cls, users): 
     215    def wrap_basic_auth(cls, users): 
     216        o_exec = cls._execute 
     217 
    177218        if not users: 
    178219            return cls 
    179220 
    180         orig_execute = cls._execute 
    181  
    182         def _execute(self, transforms, *args, **kwargs): 
     221        def execute(self, transforms, *args, **kwargs): 
    183222            def auth_required(): 
    184223                self.stream.write(tornado.escape.utf8( 
     
    188227                self.stream.close() 
    189228 
     229            creds = self.request.headers.get('Authorization') 
     230 
     231            if not creds or not creds.startswith('Basic '): 
     232                return auth_required() 
     233 
    190234            try: 
    191                 creds = self.request.headers.get('Authorization') 
    192  
    193                 if not creds or not creds.startswith('Basic '): 
    194                     return auth_required() 
    195  
    196                 creds = base64.b64decode(creds[6:]) 
    197  
    198                 if creds.find(':') < 0: 
    199                     return auth_required() 
    200  
    201                 name, passwd = creds.split(':', 2) 
     235                name, passwd = base64.b64decode(creds[6:]).split(':', 1) 
    202236                passwd = base64.b64encode(hashlib.sha1(passwd).digest()) 
    203237 
     
    205239                    return auth_required() 
    206240 
    207                 return orig_execute(self, transforms, *args, **kwargs) 
     241                return o_exec(self, transforms, *args, **kwargs) 
    208242 
    209243            except: 
    210244                return auth_required() 
    211245 
    212         cls._execute = _execute 
     246        cls._execute = execute 
    213247        return cls 
    214248 
     
    220254                    line = line.strip() 
    221255                    if 0 <= line.find(':'): 
    222                         name, passwd = line.split(':', 2) 
     256                        name, passwd = line.split(':', 1) 
    223257                        if passwd.startswith('{SHA}'): 
    224258                            users[name] = passwd[5:] 
    225259            if not users: 
    226                 raise RuntimeError('no valid users found') 
     260                raise ValueError('no valid users found') 
    227261        except TypeError: 
    228262            pass 
    229263        return users 
    230264 
    231     handler = may_auth_required(EtherWebSocket, load_htpasswd(args.htpasswd)) 
    232     ssl_options = {} 
    233  
    234     for k in ['keyfile', 'certfile']: 
    235         v = getattr(args, k, None) 
    236         if v: 
    237             v = os.path.realpath(v) 
    238             ssl_options[k] = v 
    239             open(v).close()  # readable test 
    240  
    241     if len(ssl_options) == 1: 
     265    realpath(args, 'keyfile', 'certfile', 'htpasswd') 
     266 
     267    if args.keyfile and args.certfile: 
     268        ssl_options = {'keyfile': args.keyfile, 'certfile': args.certfile} 
     269    elif args.keyfile or args.certfile: 
    242270        raise ValueError('both keyfile and certfile are required') 
    243     elif not ssl_options: 
     271    else: 
    244272        ssl_options = None 
    245273 
    246     if not args.port: 
     274    if args.port is None: 
    247275        if ssl_options: 
    248276            args.port = 443 
    249277        else: 
    250278            args.port = 80 
     279    elif not (0 <= args.port <= 65535): 
     280        raise ValueError('invalid port: %s' % args.port) 
     281 
     282    handler = wrap_basic_auth(EtherWebSocketHandler, 
     283                              load_htpasswd(args.htpasswd)) 
    251284 
    252285    tap = TapHandler(args.device, debug=args.debug) 
     
    267300 
    268301def client_main(args): 
     302    realpath(args, 'cacerts') 
     303 
    269304    if args.debug: 
    270305        websocket.enableTrace(True) 
     
    275310                                      ca_certs=args.cacerts) 
    276311 
    277     passwd = None 
    278     if args.user: 
    279         passwd = getpass.getpass() 
     312    if args.user and args.passwd is None: 
     313        args.passwd = getpass.getpass() 
    280314 
    281315    tap = TapHandler(args.device, debug=args.debug) 
    282     client = EtherWebSocketClient(tap, args.uri, args.user, passwd) 
     316    client = EtherWebSocketClient(tap, args.uri, 
     317                                  args.user, args.passwd, args.debug) 
    283318 
    284319    tap.register_client(client) 
    285320    client.open() 
    286321 
    287     t = threading.Thread(target=client.run_forever) 
    288     t.setDaemon(True) 
    289  
    290322    ioloop = tornado.ioloop.IOLoop.instance() 
    291323    ioloop.add_handler(tap.fileno(), tap, ioloop.READ) 
    292324 
     325    t = threading.Thread(target=ioloop.start) 
     326    t.setDaemon(True) 
     327 
    293328    if not args.foreground: 
    294329        daemonize() 
    295330 
    296331    t.start() 
    297     ioloop.start() 
     332    client.run_forever() 
    298333 
    299334 
     
    319354    parser_c.add_argument('--cacerts', action='store') 
    320355    parser_c.add_argument('--user', action='store') 
     356    parser_c.add_argument('--passwd', action='store') 
    321357 
    322358    args = parser.parse_args() 
  • etherws/trunk/setup.py

    r157 r160  
    3737setup( 
    3838    name='etherws', 
    39     version='0.4', 
     39    version='0.5', 
    4040    description='Ethernet over WebSocket tunneling server/client', 
    4141    long_description=longdesc, 
Note: See TracChangeset for help on using the changeset viewer.