Index: etherws/trunk/etherws.py
===================================================================
--- etherws/trunk/etherws.py	(revision 158)
+++ etherws/trunk/etherws.py	(revision 160)
@@ -44,4 +44,5 @@
 import sys
 import ssl
+import time
 import base64
 import hashlib
@@ -52,11 +53,18 @@
 import pytun
 import websocket
+import tornado.web
+import tornado.ioloop
+import tornado.websocket
 import tornado.httpserver
-import tornado.ioloop
-import tornado.web
-import tornado.websocket
-
-
-class TapHandler(object):
+
+
+class DebugMixIn(object):
+    def dprintf(self, msg, *args):
+        if self._debug:
+            prefix = '[%s] %s - ' % (time.asctime(), self.__class__.__name__)
+            sys.stderr.write(prefix + (msg % args))
+
+
+class TapHandler(DebugMixIn):
     def __init__(self, dev, debug=False):
         self._debug = debug
@@ -64,20 +72,20 @@
         self._tap = pytun.TunTapDevice(dev, pytun.IFF_TAP | pytun.IFF_NO_PI)
         self._tap.up()
-        self._write_lock = threading.Lock()
+        self._glock = threading.Lock()
 
     def fileno(self):
-        return self._tap.fileno()
+        with self._glock:
+            return self._tap.fileno()
 
     def register_client(self, client):
-        self._clients.append(client)
+        with self._glock:
+            self._clients.append(client)
 
     def unregister_client(self, client):
-        self._clients.remove(client)
+        with self._glock:
+            self._clients.remove(client)
 
     def write(self, caller, message):
-        if self._debug:
-            sys.stderr.write('%s: %s\n' % (caller.__class__.__name__,
-                                           message.encode('hex')))
-        with self._write_lock:
+        with self._glock:
             clients = self._clients[:]
 
@@ -90,10 +98,12 @@
 
     def __call__(self, fd, events):
-        self.write(self, self._tap.read(self._tap.mtu))
-
-
-class EtherWebSocket(tornado.websocket.WebSocketHandler):
+        with self._glock:
+            data = self._tap.read(self._tap.mtu)
+        self.write(self, data)
+
+
+class EtherWebSocketHandler(tornado.websocket.WebSocketHandler, DebugMixIn):
     def __init__(self, app, req, tap, debug=False):
-        super(EtherWebSocket, self).__init__(app, req)
+        super(EtherWebSocketHandler, self).__init__(app, req)
         self._tap = tap
         self._debug = debug
@@ -101,17 +111,22 @@
     def open(self):
         self._tap.register_client(self)
+        self.dprintf('connected: %s\n', self.request.remote_ip)
 
     def on_message(self, message):
         self._tap.write(self, message)
+        self.dprintf('received: %s %s\n',
+                     self.request.remote_ip, message.encode('hex'))
 
     def on_close(self):
         self._tap.unregister_client(self)
-
-
-class  EtherWebSocketClient(object):
-    def __init__(self, tap, url, user=None, passwd=None):
+        self.dprintf('disconnected: %s\n', self.request.remote_ip)
+
+
+class EtherWebSocketClient(DebugMixIn):
+    def __init__(self, tap, url, user=None, passwd=None, debug=False):
         self._sock = None
         self._tap = tap
         self._url = url
+        self._debug = debug
         self._options = {}
 
@@ -121,21 +136,35 @@
             self._options['header'] = auth
 
+    @property
+    def closed(self):
+        return not self._sock
+
     def open(self):
+        if not self.closed:
+            raise websocket.WebSocketException('already opened')
         self._sock = websocket.WebSocket()
         self._sock.connect(self._url, **self._options)
+        self.dprintf('connected: %s\n', self._url)
 
     def close(self):
+        if self.closed:
+            raise websocket.WebSocketException('already closed')
         self._sock.close()
         self._sock = None
+        self.dprintf('disconnected: %s\n', self._url)
 
     def write_message(self, message, binary=False):
-        flag = websocket.ABNF.OPCODE_TEXT
+        if self.closed:
+            raise websocket.WebSocketException('closed socket')
         if binary:
             flag = websocket.ABNF.OPCODE_BINARY
+        else:
+            flag = websocket.ABNF.OPCODE_TEXT
         self._sock.send(message, flag)
+        self.dprintf('sent: %s %s\n', self._url, message.encode('hex'))
 
     def run_forever(self):
         try:
-            if not self._sock:
+            if self.closed:
                 self.open()
             while True:
@@ -173,12 +202,22 @@
 
 
+def realpath(ns, *keys):
+    for k in keys:
+        v = getattr(ns, k, None)
+        if v is not None:
+            v = os.path.realpath(v)
+            setattr(ns, k, v)
+            open(v).close()  # check readable
+    return ns
+
+
 def server_main(args):
-    def may_auth_required(cls, users):
+    def wrap_basic_auth(cls, users):
+        o_exec = cls._execute
+
         if not users:
             return cls
 
-        orig_execute = cls._execute
-
-        def _execute(self, transforms, *args, **kwargs):
+        def execute(self, transforms, *args, **kwargs):
             def auth_required():
                 self.stream.write(tornado.escape.utf8(
@@ -188,16 +227,11 @@
                 self.stream.close()
 
+            creds = self.request.headers.get('Authorization')
+
+            if not creds or not creds.startswith('Basic '):
+                return auth_required()
+
             try:
-                creds = self.request.headers.get('Authorization')
-
-                if not creds or not creds.startswith('Basic '):
-                    return auth_required()
-
-                creds = base64.b64decode(creds[6:])
-
-                if creds.find(':') < 0:
-                    return auth_required()
-
-                name, passwd = creds.split(':', 2)
+                name, passwd = base64.b64decode(creds[6:]).split(':', 1)
                 passwd = base64.b64encode(hashlib.sha1(passwd).digest())
 
@@ -205,10 +239,10 @@
                     return auth_required()
 
-                return orig_execute(self, transforms, *args, **kwargs)
+                return o_exec(self, transforms, *args, **kwargs)
 
             except:
                 return auth_required()
 
-        cls._execute = _execute
+        cls._execute = execute
         return cls
 
@@ -220,33 +254,32 @@
                     line = line.strip()
                     if 0 <= line.find(':'):
-                        name, passwd = line.split(':', 2)
+                        name, passwd = line.split(':', 1)
                         if passwd.startswith('{SHA}'):
                             users[name] = passwd[5:]
             if not users:
-                raise RuntimeError('no valid users found')
+                raise ValueError('no valid users found')
         except TypeError:
             pass
         return users
 
-    handler = may_auth_required(EtherWebSocket, load_htpasswd(args.htpasswd))
-    ssl_options = {}
-
-    for k in ['keyfile', 'certfile']:
-        v = getattr(args, k, None)
-        if v:
-            v = os.path.realpath(v)
-            ssl_options[k] = v
-            open(v).close()  # readable test
-
-    if len(ssl_options) == 1:
+    realpath(args, 'keyfile', 'certfile', 'htpasswd')
+
+    if args.keyfile and args.certfile:
+        ssl_options = {'keyfile': args.keyfile, 'certfile': args.certfile}
+    elif args.keyfile or args.certfile:
         raise ValueError('both keyfile and certfile are required')
-    elif not ssl_options:
+    else:
         ssl_options = None
 
-    if not args.port:
+    if args.port is None:
         if ssl_options:
             args.port = 443
         else:
             args.port = 80
+    elif not (0 <= args.port <= 65535):
+        raise ValueError('invalid port: %s' % args.port)
+
+    handler = wrap_basic_auth(EtherWebSocketHandler,
+                              load_htpasswd(args.htpasswd))
 
     tap = TapHandler(args.device, debug=args.debug)
@@ -267,4 +300,6 @@
 
 def client_main(args):
+    realpath(args, 'cacerts')
+
     if args.debug:
         websocket.enableTrace(True)
@@ -275,25 +310,25 @@
                                       ca_certs=args.cacerts)
 
-    passwd = None
-    if args.user:
-        passwd = getpass.getpass()
+    if args.user and args.passwd is None:
+        args.passwd = getpass.getpass()
 
     tap = TapHandler(args.device, debug=args.debug)
-    client = EtherWebSocketClient(tap, args.uri, args.user, passwd)
+    client = EtherWebSocketClient(tap, args.uri,
+                                  args.user, args.passwd, args.debug)
 
     tap.register_client(client)
     client.open()
 
-    t = threading.Thread(target=client.run_forever)
-    t.setDaemon(True)
-
     ioloop = tornado.ioloop.IOLoop.instance()
     ioloop.add_handler(tap.fileno(), tap, ioloop.READ)
 
+    t = threading.Thread(target=ioloop.start)
+    t.setDaemon(True)
+
     if not args.foreground:
         daemonize()
 
     t.start()
-    ioloop.start()
+    client.run_forever()
 
 
@@ -319,4 +354,5 @@
     parser_c.add_argument('--cacerts', action='store')
     parser_c.add_argument('--user', action='store')
+    parser_c.add_argument('--passwd', action='store')
 
     args = parser.parse_args()
