2009-11-25 Yuzo Fujishima <yuzo@google.com>
authoreric@webkit.org <eric@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 26 Nov 2009 06:16:09 +0000 (06:16 +0000)
committereric@webkit.org <eric@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 26 Nov 2009 06:16:09 +0000 (06:16 +0000)
        Reviewed by Eric Seidel.

        Update pywebsocket to 0.4.2

        Update pywebsocket to 0.4.2
        https://bugs.webkit.org/show_bug.cgi?id=31861

        * pywebsocket/example/echo_client.py:
        * pywebsocket/example/echo_wsh.py:
        * pywebsocket/mod_pywebsocket/__init__.py:
        * pywebsocket/mod_pywebsocket/dispatch.py:
        * pywebsocket/mod_pywebsocket/msgutil.py:
        * pywebsocket/mod_pywebsocket/standalone.py:
        * pywebsocket/setup.py:
        * pywebsocket/test/test_dispatch.py:
        * pywebsocket/test/test_msgutil.py:

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@51406 268f45cc-cd09-0410-ab3c-d52691b4dbfc

WebKitTools/ChangeLog
WebKitTools/pywebsocket/example/echo_client.py
WebKitTools/pywebsocket/example/echo_wsh.py
WebKitTools/pywebsocket/mod_pywebsocket/__init__.py
WebKitTools/pywebsocket/mod_pywebsocket/dispatch.py
WebKitTools/pywebsocket/mod_pywebsocket/msgutil.py
WebKitTools/pywebsocket/mod_pywebsocket/standalone.py
WebKitTools/pywebsocket/setup.py
WebKitTools/pywebsocket/test/test_dispatch.py
WebKitTools/pywebsocket/test/test_msgutil.py

index 0a77a7be446291a4bd8adca48875b80393a88bec..fbe25cde701280602f17456ce30902e9b720659f 100644 (file)
@@ -1,3 +1,22 @@
+2009-11-25  Yuzo Fujishima  <yuzo@google.com>
+
+        Reviewed by Eric Seidel.
+
+        Update pywebsocket to 0.4.2
+
+        Update pywebsocket to 0.4.2
+        https://bugs.webkit.org/show_bug.cgi?id=31861
+
+        * pywebsocket/example/echo_client.py:
+        * pywebsocket/example/echo_wsh.py:
+        * pywebsocket/mod_pywebsocket/__init__.py:
+        * pywebsocket/mod_pywebsocket/dispatch.py:
+        * pywebsocket/mod_pywebsocket/msgutil.py:
+        * pywebsocket/mod_pywebsocket/standalone.py:
+        * pywebsocket/setup.py:
+        * pywebsocket/test/test_dispatch.py:
+        * pywebsocket/test/test_msgutil.py:
+
 2009-11-25  Adam Barth  <abarth@webkit.org>
 
         Reviewed by Eric Seidel.
index 61b129c50d59b1c3e5ebb7e004a65fc0ce6ccd6d..3262a6d7633eda1b0bb0deb7c6a1480fb217ee47 100644 (file)
@@ -46,6 +46,8 @@ import socket
 import sys
 
 
+_TIMEOUT_SEC = 10
+
 _DEFAULT_PORT = 80
 _DEFAULT_SECURE_PORT = 443
 _UNDEFINED_PORT = -1
@@ -57,6 +59,8 @@ _EXPECTED_RESPONSE = (
         _UPGRADE_HEADER +
         _CONNECTION_HEADER)
 
+_GOODBYE_MESSAGE = 'Goodbye'
+
 
 def _method_line(resource):
     return 'GET %s HTTP/1.1\r\n' % resource
@@ -96,13 +100,14 @@ class EchoClient(object):
         Shake hands and then repeat sending message and receiving its echo.
         """
         self._socket = socket.socket()
+        self._socket.settimeout(self._options.socket_timeout)
         try:
             self._socket.connect((self._options.server_host,
                                   self._options.server_port))
             if self._options.use_tls:
                 self._socket = _TLSSocket(self._socket)
             self._handshake()
-            for line in self._options.message.split(','):
+            for line in self._options.message.split(',') + [_GOODBYE_MESSAGE]:
                 frame = '\x00' + line.encode('utf-8') + '\xff'
                 self._socket.send(frame)
                 if self._options.verbose:
@@ -111,7 +116,8 @@ class EchoClient(object):
                 if received != frame:
                     raise Exception('Incorrect echo: %r' % received)
                 if self._options.verbose:
-                    print 'Recv: %s' % received[1:-1].decode('utf-8')
+                    print 'Recv: %s' % received[1:-1].decode('utf-8',
+                                                             'replace')
         finally:
             self._socket.close()
 
@@ -166,11 +172,17 @@ def main():
     parser.add_option('-r', '--resource', dest='resource', type='string',
                       default='/echo', help='resource path')
     parser.add_option('-m', '--message', dest='message', type='string',
-                      help='comma-separated messages to send')
+                      help=('comma-separated messages to send excluding "%s" '
+                            'that is always sent at the end' %
+                            _GOODBYE_MESSAGE))
     parser.add_option('-q', '--quiet', dest='verbose', action='store_false',
                       default=True, help='suppress messages')
     parser.add_option('-t', '--tls', dest='use_tls', action='store_true',
                       default=False, help='use TLS (wss://)')
+    parser.add_option('-k', '--socket_timeout', dest='socket_timeout',
+                      type='int', default=_TIMEOUT_SEC,
+                      help='Timeout(sec) for sockets')
+
     (options, unused_args) = parser.parse_args()
 
     # Default port number depends on whether TLS is used.
index f680fa5cd17b95ea2fa1d55ba6d62c0b560ab513..50cad3184d45c5317efd5eb115f43aa490512f04 100644 (file)
@@ -31,6 +31,9 @@
 from mod_pywebsocket import msgutil
 
 
+_GOODBYE_MESSAGE = 'Goodbye'
+
+
 def web_socket_do_extra_handshake(request):
     pass  # Always accept.
 
@@ -39,6 +42,8 @@ def web_socket_transfer_data(request):
     while True:
         line = msgutil.receive_message(request)
         msgutil.send_message(request, line)
+        if line == _GOODBYE_MESSAGE:
+            return
 
 
 # vi:sts=4 sw=4 et
index beacc9ef14dbc503d18dae77399a2adb6ae55408..05e80e8b37f83d7697ace30976e3ead0136dd974 100644 (file)
@@ -96,6 +96,9 @@ web_socket_transfer_data is called after the handshake completed
 successfully. A handler can receive/send messages from/to the client
 using request. mod_pywebsocket.msgutil module provides utilities
 for data transfer.
+
+A Web Socket handler must be thread-safe if the server (Apache or
+standalone.py) is configured to use threads.
 """
 
 
index 6d500cbe1bb84b10fe577c0accac2ce0e1d1ab06..87a731575ccd7b64a928f7472d06c4d96d731e2b 100644 (file)
@@ -62,7 +62,7 @@ def _normalize_path(path):
     """
 
     path = path.replace('\\', os.path.sep)
-    path = os.path.abspath(path)
+    path = os.path.realpath(path)
     path = path.replace('\\', '/')
     return path
 
@@ -136,7 +136,8 @@ class Dispatcher(object):
         self._source_warnings = []
         if scan_dir is None:
             scan_dir = root_dir
-        if not os.path.realpath(scan_dir).startswith(os.path.realpath(root_dir)):
+        if not os.path.realpath(scan_dir).startswith(
+                os.path.realpath(root_dir)):
             raise DispatchError('scan_dir:%s must be a directory under '
                                 'root_dir:%s.' % (scan_dir, root_dir))
         self._source_files_in_dir(root_dir, scan_dir)
@@ -182,13 +183,14 @@ class Dispatcher(object):
 
     def _handler(self, request):
         try:
-            return self._handlers[request.ws_resource]
+            ws_resource_path = request.ws_resource.split('?', 1)[0]
+            return self._handlers[ws_resource_path]
         except KeyError:
             raise DispatchError('No handler for: %r' % request.ws_resource)
 
     def _source_files_in_dir(self, root_dir, scan_dir):
         """Source all the handler source files in the scan_dir directory.
-        
+
         The resource path is determined relative to root_dir.
         """
 
index bdb554de85240b4e1111ff47e6465514adc54966..9fa9b59a5c9e2ea49c1af7cc3e60d27a33cc29c4 100644 (file)
@@ -73,7 +73,9 @@ def receive_message(request):
         else:
             # The payload is delimited with \xff.
             bytes = _read_until(request, '\xff')
-            message = bytes.decode('utf-8')
+            # The Web Socket protocol section 4.4 specifies that invalid
+            # characters must be replaced with U+fffd REPLACEMENT CHARACTER.
+            message = bytes.decode('utf-8', 'replace')
             if frame_type == 0x00:
                 return message
             # Discard data of other types.
index a4c142b4151d497f576c2e35d73239636f8514cb..3b2d0dc95b861e306988c1f1309adb88639409a4 100644 (file)
@@ -38,6 +38,7 @@ Usage:
     python standalone.py [-p <ws_port>] [-w <websock_handlers>]
                          [-s <scan_dir>]
                          [-d <document_root>]
+                         ... for other options, see _main below ...
 
 <ws_port> is the port number to use for ws:// connection.
 
@@ -59,6 +60,7 @@ import BaseHTTPServer
 import SimpleHTTPServer
 import SocketServer
 import logging
+import logging.handlers
 import optparse
 import os
 import socket
@@ -75,6 +77,24 @@ import dispatch
 import handshake
 
 
+_LOG_LEVELS = {
+    'debug': logging.DEBUG,
+    'info': logging.INFO,
+    'warn': logging.WARN,
+    'error': logging.ERROR,
+    'critical': logging.CRITICAL};
+
+_DEFAULT_LOG_MAX_BYTES = 1024 * 256
+_DEFAULT_LOG_BACKUP_COUNT = 5
+
+
+def _print_warnings_if_any(dispatcher):
+    warnings = dispatcher.source_warnings()
+    if warnings:
+        for warning in warnings:
+            logging.warning('mod_pywebsocket: %s' % warning)
+
+
 class _StandaloneConnection(object):
     """Mimic mod_python mp_conn."""
 
@@ -152,6 +172,7 @@ class WebSocketServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
             socket_ = OpenSSL.SSL.Connection(ctx, socket_)
         return socket_
 
+
 class WebSocketRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
     """SimpleHTTPRequestHandler specialized for Web Socket."""
 
@@ -159,15 +180,13 @@ class WebSocketRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
         """Override SocketServer.StreamRequestHandler.setup."""
 
         self.connection = self.request
-        self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
-        self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
+        self.rfile = socket._fileobject(self.request, 'rb', self.rbufsize)
+        self.wfile = socket._fileobject(self.request, 'wb', self.wbufsize)
 
     def __init__(self, *args, **keywords):
         self._request = _StandaloneRequest(
                 self, WebSocketRequestHandler.options.use_tls)
-        self._dispatcher = dispatch.Dispatcher(
-                WebSocketRequestHandler.options.websock_handlers,
-                WebSocketRequestHandler.options.scan_dir)
+        self._dispatcher = WebSocketRequestHandler.options.dispatcher
         self._print_warnings_if_any()
         self._handshaker = handshake.Handshaker(self._request,
                                                 self._dispatcher)
@@ -200,10 +219,35 @@ class WebSocketRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
                 return False
         return result
 
+    def log_request(self, code='-', size='-'):
+        """Override BaseHTTPServer.log_request."""
+
+        logging.info('"%s" %s %s',
+                     self.requestline, str(code), str(size))
+
+    def log_error(self, *args):
+        """Override BaseHTTPServer.log_error."""
+
+        # Despite the name, this method is for warnings than for errors.
+        # For example, HTTP status code is logged by this method.
+        logging.warn('%s - %s' % (self.address_string(), (args[0] % args[1:])))
 
-def _main():
-    logging.basicConfig()
 
+def _configure_logging(options):
+    logger = logging.getLogger()
+    logger.setLevel(_LOG_LEVELS[options.log_level])
+    if options.log_file:
+        handler = logging.handlers.RotatingFileHandler(
+                options.log_file, 'a', options.log_max, options.log_count)
+    else:
+        handler = logging.StreamHandler()
+    formatter = logging.Formatter(
+            "[%(asctime)s] [%(levelname)s] %(name)s: %(message)s")
+    handler.setFormatter(formatter)
+    logger.addHandler(handler)
+
+
+def _main():
     parser = optparse.OptionParser()
     parser.add_option('-p', '--port', dest='port', type='int',
                       default=handshake._DEFAULT_WEB_SOCKET_PORT,
@@ -224,27 +268,51 @@ def _main():
                       default='', help='TLS private key file.')
     parser.add_option('-c', '--certificate', dest='certificate',
                       default='', help='TLS certificate file.')
+    parser.add_option('-l', '--log_file', dest='log_file',
+                      default='', help='Log file.')
+    parser.add_option('--log_level', type='choice', dest='log_level',
+                      default='warn',
+                      choices=['debug', 'info', 'warn', 'error', 'critical'],
+                      help='Log level.')
+    parser.add_option('--log_max', dest='log_max', type='int',
+                      default=_DEFAULT_LOG_MAX_BYTES,
+                      help='Log maximum bytes')
+    parser.add_option('--log_count', dest='log_count', type='int',
+                      default=_DEFAULT_LOG_BACKUP_COUNT,
+                      help='Log backup count')
     options = parser.parse_args()[0]
 
+    os.chdir(options.document_root)
+
+    _configure_logging(options)
+
     if options.use_tls:
         if not _HAS_OPEN_SSL:
-            print >>sys.stderr, 'To use TLS, install pyOpenSSL.'
+            logging.critical('To use TLS, install pyOpenSSL.')
             sys.exit(1)
         if not options.private_key or not options.certificate:
-            print >>sys.stderr, ('To use TLS, specify private_key and '
-                                 'certificate.')
+            logging.critical(
+                    'To use TLS, specify private_key and certificate.')
             sys.exit(1)
 
     if not options.scan_dir:
         options.scan_dir = options.websock_handlers
 
-    WebSocketRequestHandler.options = options
-    WebSocketServer.options = options
-
-    os.chdir(options.document_root)
-
-    server = WebSocketServer(('', options.port), WebSocketRequestHandler)
-    server.serve_forever()
+    try:
+        # Share a Dispatcher among request handlers to save time for
+        # instantiation.  Dispatcher can be shared because it is thread-safe.
+        options.dispatcher = dispatch.Dispatcher(options.websock_handlers,
+                                                 options.scan_dir)
+        _print_warnings_if_any(options.dispatcher)
+
+        WebSocketRequestHandler.options = options
+        WebSocketServer.options = options
+
+        server = WebSocketServer(('', options.port), WebSocketRequestHandler)
+        server.serve_forever()
+    except Exception, e:
+        logging.critical(str(e))
+        sys.exit(1)
 
 
 if __name__ == '__main__':
index 1810a6da8a3bca787b1eb7861151ae5cd721df3d..ae07f8a47be9326116b94c2ec8f67ad14eca630d 100644 (file)
@@ -56,7 +56,7 @@ setup(author='Yuzo Fujishima',
       name=_PACKAGE_NAME,
       packages=[_PACKAGE_NAME],
       url='http://code.google.com/p/pywebsocket/',
-      version='0.4.1',
+      version='0.4.2',
       )
 
 
index d6172053866e0d5d2f3db6d414acc1c0e34268a5..d31d6bdfde45a01f209edf819052c0c8de14018e 100644 (file)
@@ -156,6 +156,20 @@ class DispatcherTest(unittest.TestCase):
         self.assertEqual('sub/plain_wsh.py is called for /sub/plain, None',
                          request.connection.written_data())
 
+        request = mock.MockRequest(connection=mock.MockConn(''))
+        request.ws_resource = '/sub/plain?'
+        request.ws_protocol = None
+        dispatcher.transfer_data(request)
+        self.assertEqual('sub/plain_wsh.py is called for /sub/plain?, None',
+                         request.connection.written_data())
+
+        request = mock.MockRequest(connection=mock.MockConn(''))
+        request.ws_resource = '/sub/plain?q=v'
+        request.ws_protocol = None
+        dispatcher.transfer_data(request)
+        self.assertEqual('sub/plain_wsh.py is called for /sub/plain?q=v, None',
+                         request.connection.written_data())
+
     def test_transfer_data_no_handler(self):
         dispatcher = dispatch.Dispatcher(_TEST_HANDLERS_DIR, None)
         for resource in ['/blank', '/sub/non_callable',
index b3ba539cbddde39753a66911b61568beadbb8196..16b88e0f2eb89d298ef07b2617a421c0a564c948 100644 (file)
@@ -71,6 +71,13 @@ class MessageTest(unittest.TestCase):
         # U+672c is encoded as e6,9c,ac in UTF-8
         self.assertEqual(u'\u672c', msgutil.receive_message(request))
 
+    def test_receive_message_erroneous_unicode(self):
+        # \x80 and \x81 are invalid as UTF-8.
+        request = _create_request('\x00\x80\x81\xff')
+        # Invalid characters should be replaced with
+        # U+fffd REPLACEMENT CHARACTER
+        self.assertEqual(u'\ufffd\ufffd', msgutil.receive_message(request))
+
     def test_receive_message_discard(self):
         request = _create_request('\x80\x06IGNORE\x00Hello\xff'
                                 '\x01DISREGARD\xff\x00World!\xff')