[Python-modules-commits] [python-websockets] 01/05: Import python-websockets_3.4.orig.tar.gz

Piotr Ożarowski piotr at moszumanska.debian.org
Fri Sep 29 10:14:41 UTC 2017


This is an automated email from the git hooks/post-receive script.

piotr pushed a commit to branch master
in repository python-websockets.

commit d9b8e75e4cf05a6a31e615bc367c45dcd2b1d5db
Author: Piotr Ożarowski <piotr at debian.org>
Date:   Fri Sep 29 12:00:07 2017 +0200

    Import python-websockets_3.4.orig.tar.gz
---
 PKG-INFO                                           |   3 +-
 setup.cfg                                          |   3 +-
 setup.py                                           |  16 +-
 websockets.egg-info/PKG-INFO                       |   3 +-
 websockets.egg-info/SOURCES.txt                    |   9 +-
 websockets/client.py                               | 124 +++++--
 websockets/compatibility.py                        |  40 +++
 websockets/exceptions.py                           |  36 +-
 websockets/framing.py                              |  10 +-
 websockets/http.py                                 | 170 +++++++--
 websockets/protocol.py                             |  38 +-
 websockets/py35/__pycache__/client.cpython-35.pyc  | Bin 1123 -> 1123 bytes
 websockets/py35/__pycache__/client.cpython-36.pyc  | Bin 1081 -> 1081 bytes
 .../py35/__pycache__/client_server.cpython-35.pyc  | Bin 1460 -> 1999 bytes
 .../py35/__pycache__/client_server.cpython-36.pyc  | Bin 1373 -> 1886 bytes
 websockets/py35/__pycache__/server.cpython-35.pyc  | Bin 0 -> 1133 bytes
 websockets/py35/__pycache__/server.cpython-36.pyc  | Bin 0 -> 1088 bytes
 websockets/py35/client_server.py                   |  16 +-
 websockets/py35/server.py                          |  22 ++
 websockets/server.py                               | 363 ++++++++++++++-----
 websockets/speedups.c                              | 130 +++++++
 websockets/test_client_server.py                   | 397 +++++++++++++++------
 websockets/test_http.py                            |  75 +++-
 websockets/test_protocol.py                        |  13 +-
 websockets/test_speedups.py                        |   0
 websockets/test_utils.py                           |  50 +++
 websockets/utils.py                                |  14 +
 websockets/version.py                              |   2 +-
 28 files changed, 1212 insertions(+), 322 deletions(-)

diff --git a/PKG-INFO b/PKG-INFO
index 25cb85c..e98f217 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: websockets
-Version: 3.3
+Version: 3.4
 Summary: An implementation of the WebSocket Protocol (RFC 6455)
 Home-page: https://github.com/aaugustin/websockets
 Author: Aymeric Augustin
@@ -46,3 +46,4 @@ Classifier: Programming Language :: Python :: 3
 Classifier: Programming Language :: Python :: 3.3
 Classifier: Programming Language :: Python :: 3.4
 Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
diff --git a/setup.cfg b/setup.cfg
index 3a7ef73..ae08535 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,5 @@
 [bdist_wheel]
-python-tag = py33.py34.py35
+python-tag = py33.py34.py35.py36
 
 [flake8]
 ignore = E731,F403,F405
@@ -7,6 +7,7 @@ ignore = E731,F403,F405
 [isort]
 known_standard_library = asyncio
 lines_after_imports = 2
+multi_line_output = 5
 
 [egg_info]
 tag_build = 
diff --git a/setup.py b/setup.py
index 57ced17..4ec80bb 100644
--- a/setup.py
+++ b/setup.py
@@ -7,10 +7,12 @@ root_dir = os.path.abspath(os.path.dirname(__file__))
 
 description = "An implementation of the WebSocket Protocol (RFC 6455)"
 
-with open(os.path.join(root_dir, 'README.rst')) as f:
+readme_file = os.path.join(root_dir, 'README.rst')
+with open(readme_file, encoding='utf-8') as f:
     long_description = f.read()
 
-with open(os.path.join(root_dir, 'websockets', 'version.py')) as f:
+version_module = os.path.join(root_dir, 'websockets', 'version.py')
+with open(version_module, encoding='utf-8') as f:
     exec(f.read())
 
 py_version = sys.version_info[:2]
@@ -23,6 +25,14 @@ packages = ['websockets']
 if py_version >= (3, 5):
     packages.append('websockets/py35')
 
+ext_modules = [
+    setuptools.Extension(
+        'websockets.speedups',
+        sources=['websockets/speedups.c'],
+        optional=not os.path.exists(os.path.join(root_dir, '.cibuildwheel')),
+    )
+]
+
 setuptools.setup(
     name='websockets',
     version=version,
@@ -43,8 +53,10 @@ setuptools.setup(
         'Programming Language :: Python :: 3.3',
         'Programming Language :: Python :: 3.4',
         'Programming Language :: Python :: 3.5',
+        'Programming Language :: Python :: 3.6',
     ],
     packages=packages,
+    ext_modules=ext_modules,
     extras_require={
         ':python_version=="3.3"': ['asyncio'],
     },
diff --git a/websockets.egg-info/PKG-INFO b/websockets.egg-info/PKG-INFO
index 25cb85c..e98f217 100644
--- a/websockets.egg-info/PKG-INFO
+++ b/websockets.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: websockets
-Version: 3.3
+Version: 3.4
 Summary: An implementation of the WebSocket Protocol (RFC 6455)
 Home-page: https://github.com/aaugustin/websockets
 Author: Aymeric Augustin
@@ -46,3 +46,4 @@ Classifier: Programming Language :: Python :: 3
 Classifier: Programming Language :: Python :: 3.3
 Classifier: Programming Language :: Python :: 3.4
 Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
diff --git a/websockets.egg-info/SOURCES.txt b/websockets.egg-info/SOURCES.txt
index 4bfa088..f5cd53c 100644
--- a/websockets.egg-info/SOURCES.txt
+++ b/websockets.egg-info/SOURCES.txt
@@ -12,13 +12,17 @@ websockets/handshake.py
 websockets/http.py
 websockets/protocol.py
 websockets/server.py
+websockets/speedups.c
 websockets/test_client_server.py
 websockets/test_framing.py
 websockets/test_handshake.py
 websockets/test_http.py
 websockets/test_protocol.py
+websockets/test_speedups.py
 websockets/test_uri.py
+websockets/test_utils.py
 websockets/uri.py
+websockets/utils.py
 websockets/version.py
 websockets.egg-info/PKG-INFO
 websockets.egg-info/SOURCES.txt
@@ -28,6 +32,7 @@ websockets.egg-info/top_level.txt
 websockets/py35/__init__.py
 websockets/py35/client.py
 websockets/py35/client_server.py
+websockets/py35/server.py
 websockets/py35/__pycache__/__init__.cpython-33.pyc
 websockets/py35/__pycache__/__init__.cpython-34.pyc
 websockets/py35/__pycache__/__init__.cpython-35.pyc
@@ -35,4 +40,6 @@ websockets/py35/__pycache__/__init__.cpython-36.pyc
 websockets/py35/__pycache__/client.cpython-35.pyc
 websockets/py35/__pycache__/client.cpython-36.pyc
 websockets/py35/__pycache__/client_server.cpython-35.pyc
-websockets/py35/__pycache__/client_server.cpython-36.pyc
\ No newline at end of file
+websockets/py35/__pycache__/client_server.cpython-36.pyc
+websockets/py35/__pycache__/server.cpython-35.pyc
+websockets/py35/__pycache__/server.cpython-36.pyc
\ No newline at end of file
diff --git a/websockets/client.py b/websockets/client.py
index acebce3..544223e 100644
--- a/websockets/client.py
+++ b/websockets/client.py
@@ -5,11 +5,10 @@ The :mod:`websockets.client` module defines a simple WebSocket client API.
 
 import asyncio
 import collections.abc
-import email.message
 
-from .exceptions import InvalidHandshake
+from .exceptions import InvalidHandshake, InvalidMessage, InvalidStatusCode
 from .handshake import build_request, check_response
-from .http import USER_AGENT, read_response
+from .http import USER_AGENT, build_headers, read_response
 from .protocol import CONNECTING, OPEN, WebSocketCommonProtocol
 from .uri import parse_uri
 
@@ -29,6 +28,60 @@ class WebSocketClientProtocol(WebSocketCommonProtocol):
     state = CONNECTING
 
     @asyncio.coroutine
+    def write_http_request(self, path, headers):
+        """
+        Write request line and headers to the HTTP request.
+
+        """
+        self.path = path
+        self.request_headers = build_headers(headers)
+        self.raw_request_headers = headers
+
+        # Since the path and headers only contain ASCII characters,
+        # we can keep this simple.
+        request = ['GET {path} HTTP/1.1'.format(path=path)]
+        request.extend('{}: {}'.format(k, v) for k, v in headers)
+        request.append('\r\n')
+        request = '\r\n'.join(request).encode()
+
+        self.writer.write(request)
+
+    @asyncio.coroutine
+    def read_http_response(self):
+        """
+        Read status line and headers from the HTTP response.
+
+        Raise :exc:`~websockets.exceptions.InvalidMessage` if the HTTP message
+        is malformed or isn't an HTTP/1.1 GET request.
+
+        Don't attempt to read the response body because WebSocket handshake
+        responses don't have one. If the response contains a body, it may be
+        read from ``self.reader`` after this coroutine returns.
+
+        """
+        try:
+            status_code, headers = yield from read_response(self.reader)
+        except ValueError as exc:
+            raise InvalidMessage("Malformed HTTP message") from exc
+
+        self.response_headers = build_headers(headers)
+        self.raw_response_headers = headers
+
+        return status_code, self.response_headers
+
+    def process_subprotocol(self, get_header, subprotocols=None):
+        """
+        Handle the Sec-WebSocket-Protocol HTTP header.
+
+        """
+        subprotocol = get_header('Sec-WebSocket-Protocol')
+        if subprotocol:
+            if subprotocols is None or subprotocol not in subprotocols:
+                raise InvalidHandshake(
+                    "Unknown subprotocol: {}".format(subprotocol))
+            return subprotocol
+
+    @asyncio.coroutine
     def handshake(self, wsuri,
                   origin=None, subprotocols=None, extra_headers=None):
         """
@@ -45,6 +98,7 @@ class WebSocketClientProtocol(WebSocketCommonProtocol):
         """
         headers = []
         set_header = lambda k, v: headers.append((k, v))
+
         if wsuri.port == (443 if wsuri.secure else 80):     # pragma: no cover
             set_header('Host', wsuri.host)
         else:
@@ -59,40 +113,20 @@ class WebSocketClientProtocol(WebSocketCommonProtocol):
             for name, value in extra_headers:
                 set_header(name, value)
         set_header('User-Agent', USER_AGENT)
+
         key = build_request(set_header)
 
-        self.request_headers = email.message.Message()
-        for name, value in headers:
-            self.request_headers[name] = value
-        self.raw_request_headers = headers
+        yield from self.write_http_request(wsuri.resource_name, headers)
 
-        # Send handshake request. Since the URI and the headers only contain
-        # ASCII characters, we can keep this simple.
-        request = ['GET %s HTTP/1.1' % wsuri.resource_name]
-        request.extend('{}: {}'.format(k, v) for k, v in headers)
-        request.append('\r\n')
-        request = '\r\n'.join(request).encode()
-        self.writer.write(request)
+        status_code, headers = yield from self.read_http_response()
+        get_header = lambda k: headers.get(k, '')
 
-        # Read handshake response.
-        try:
-            status_code, headers = yield from read_response(self.reader)
-        except ValueError as exc:
-            raise InvalidHandshake("Malformed HTTP message") from exc
         if status_code != 101:
-            raise InvalidHandshake("Bad status code: {}".format(status_code))
-
-        self.response_headers = headers
-        self.raw_response_headers = list(headers.raw_items())
+            raise InvalidStatusCode(status_code)
 
-        get_header = lambda k: headers.get(k, '')
         check_response(get_header, key)
 
-        self.subprotocol = headers.get('Sec-WebSocket-Protocol', None)
-        if (self.subprotocol is not None and
-                self.subprotocol not in subprotocols):
-            raise InvalidHandshake(
-                "Unknown subprotocol: {}".format(self.subprotocol))
+        self.subprotocol = self.process_subprotocol(get_header, subprotocols)
 
         assert self.state == CONNECTING
         self.state = OPEN
@@ -101,9 +135,10 @@ class WebSocketClientProtocol(WebSocketCommonProtocol):
 
 @asyncio.coroutine
 def connect(uri, *,
-            klass=WebSocketClientProtocol,
+            create_protocol=None,
             timeout=10, max_size=2 ** 20, max_queue=2 ** 5,
-            loop=None, legacy_recv=False,
+            read_limit=2 ** 16, write_limit=2 ** 16,
+            loop=None, legacy_recv=False, klass=None,
             origin=None, subprotocols=None, extra_headers=None,
             **kwds):
     """
@@ -113,7 +148,7 @@ def connect(uri, *,
     send and receive messages.
 
     :func:`connect` is a wrapper around the event loop's
-    :meth:`~asyncio.BaseEventLoop.create_connection` method. Extra keyword
+    :meth:`~asyncio.BaseEventLoop.create_connection` method. Unknown keyword
     arguments are passed to :meth:`~asyncio.BaseEventLoop.create_connection`.
 
     For example, you can set the ``ssl`` keyword argument to a
@@ -121,9 +156,15 @@ def connect(uri, *,
     a ``wss://`` URI, if this argument isn't provided explicitly, it's set to
     ``True``, which means Python's default :class:`~ssl.SSLContext` is used.
 
-    The behavior of the ``timeout``, ``max_size``, and ``max_queue`` optional
-    arguments is described the documentation of
-    :class:`~websockets.protocol.WebSocketCommonProtocol`.
+    The behavior of the ``timeout``, ``max_size``, and ``max_queue``,
+    ``read_limit``, and ``write_limit`` optional arguments is described in the
+    documentation of :class:`~websockets.protocol.WebSocketCommonProtocol`.
+
+    The ``create_protocol`` parameter allows customizing the asyncio protocol
+    that manages the connection. It should be a callable or class accepting
+    the same arguments as :class:`WebSocketClientProtocol` and returning a
+    :class:`WebSocketClientProtocol` instance. It defaults to
+    :class:`WebSocketClientProtocol`.
 
     :func:`connect` also accepts the following optional arguments:
 
@@ -144,15 +185,24 @@ def connect(uri, *,
     if loop is None:
         loop = asyncio.get_event_loop()
 
+    # Backwards-compatibility: create_protocol used to be called klass.
+    # In the unlikely event that both are specified, klass is ignored.
+    if create_protocol is None:
+        create_protocol = klass
+
+    if create_protocol is None:
+        create_protocol = WebSocketClientProtocol
+
     wsuri = parse_uri(uri)
     if wsuri.secure:
         kwds.setdefault('ssl', True)
-    elif 'ssl' in kwds:
+    elif kwds.get('ssl') is not None:
         raise ValueError("connect() received a SSL context for a ws:// URI. "
                          "Use a wss:// URI to enable TLS.")
-    factory = lambda: klass(
+    factory = lambda: create_protocol(
         host=wsuri.host, port=wsuri.port, secure=wsuri.secure,
         timeout=timeout, max_size=max_size, max_queue=max_queue,
+        read_limit=read_limit, write_limit=write_limit,
         loop=loop, legacy_recv=legacy_recv,
     )
 
diff --git a/websockets/compatibility.py b/websockets/compatibility.py
index 90afea9..c8c3014 100644
--- a/websockets/compatibility.py
+++ b/websockets/compatibility.py
@@ -1,4 +1,5 @@
 import asyncio
+import http
 
 
 # Replace with BaseEventLoop.create_task when dropping Python < 3.4.2.
@@ -6,3 +7,42 @@ try:                                                # pragma: no cover
     asyncio_ensure_future = asyncio.ensure_future   # Python ≥ 3.5
 except AttributeError:                              # pragma: no cover
     asyncio_ensure_future = asyncio.async           # Python < 3.5
+
+try:                                                # pragma: no cover
+                                                    # Python ≥ 3.5
+    SWITCHING_PROTOCOLS = http.HTTPStatus.SWITCHING_PROTOCOLS
+    OK = http.HTTPStatus.OK
+    BAD_REQUEST = http.HTTPStatus.BAD_REQUEST
+    UNAUTHORIZED = http.HTTPStatus.UNAUTHORIZED
+    FORBIDDEN = http.HTTPStatus.FORBIDDEN
+    INTERNAL_SERVER_ERROR = http.HTTPStatus.INTERNAL_SERVER_ERROR
+    SERVICE_UNAVAILABLE = http.HTTPStatus.SERVICE_UNAVAILABLE
+except AttributeError:                              # pragma: no cover
+                                                    # Python < 3.5
+    class SWITCHING_PROTOCOLS:
+        value = 101
+        phrase = "Switching Protocols"
+
+    class OK:
+        value = 200
+        phrase = "OK"
+
+    class BAD_REQUEST:
+        value = 400
+        phrase = "Bad Request"
+
+    class UNAUTHORIZED:
+        value = 401
+        phrase = "Unauthorized"
+
+    class FORBIDDEN:
+        value = 403
+        phrase = "Forbidden"
+
+    class INTERNAL_SERVER_ERROR:
+        value = 500
+        phrase = "Internal Server Error"
+
+    class SERVICE_UNAVAILABLE:
+        value = 503
+        phrase = "Service Unavailable"
diff --git a/websockets/exceptions.py b/websockets/exceptions.py
index 8824df0..3db5695 100644
--- a/websockets/exceptions.py
+++ b/websockets/exceptions.py
@@ -1,6 +1,7 @@
 __all__ = [
-    'InvalidHandshake', 'InvalidOrigin', 'InvalidState', 'InvalidURI',
-    'ConnectionClosed', 'PayloadTooBig', 'WebSocketProtocolError',
+    'AbortHandshake', 'InvalidHandshake', 'InvalidMessage', 'InvalidOrigin',
+    'InvalidState', 'InvalidStatusCode', 'InvalidURI', 'ConnectionClosed',
+    'PayloadTooBig', 'WebSocketProtocolError',
 ]
 
 
@@ -11,6 +12,24 @@ class InvalidHandshake(Exception):
     """
 
 
+class AbortHandshake(InvalidHandshake):
+    """
+    Exception raised to abort a handshake and return a HTTP response.
+
+    """
+    def __init__(self, status, headers, body=None):
+        self.status = status
+        self.headers = headers
+        self.body = body
+
+
+class InvalidMessage(InvalidHandshake):
+    """
+    Exception raised when the HTTP message in a handshake request is malformed.
+
+    """
+
+
 class InvalidOrigin(InvalidHandshake):
     """
     Exception raised when the origin in a handshake request is forbidden.
@@ -18,6 +37,19 @@ class InvalidOrigin(InvalidHandshake):
     """
 
 
+class InvalidStatusCode(InvalidHandshake):
+    """
+    Exception raised when a handshake response status code is invalid.
+
+    Provides the integer status code in its ``status_code`` attribute.
+
+    """
+    def __init__(self, status_code):
+        self.status_code = status_code
+        message = 'Status code not 101: {}'.format(status_code)
+        super().__init__(message)
+
+
 class InvalidState(Exception):
     """
     Exception raised when an operation is forbidden in the current state.
diff --git a/websockets/framing.py b/websockets/framing.py
index 544ac27..135a139 100644
--- a/websockets/framing.py
+++ b/websockets/framing.py
@@ -18,6 +18,12 @@ import struct
 from .exceptions import PayloadTooBig, WebSocketProtocolError
 
 
+try:
+    from .speedups import apply_mask
+except ImportError:                                         # pragma: no cover
+    from .utils import apply_mask
+
+
 __all__ = [
     'OP_CONT', 'OP_TEXT', 'OP_BINARY', 'OP_CLOSE', 'OP_PING', 'OP_PONG',
     'Frame', 'read_frame', 'write_frame', 'parse_close', 'serialize_close'
@@ -101,7 +107,7 @@ def read_frame(reader, mask, *, max_size=None):
     # Read the data
     data = yield from reader(length)
     if mask:
-        data = bytes(b ^ mask_bits[i % 4] for i, b in enumerate(data))
+        data = apply_mask(data, mask_bits)
 
     frame = Frame(fin, opcode, data)
     check_frame(frame)
@@ -144,7 +150,7 @@ def write_frame(frame, writer, mask):
 
     # Prepare the data
     if mask:
-        data = bytes(b ^ mask_bits[i % 4] for i, b in enumerate(frame.data))
+        data = apply_mask(frame.data, mask_bits)
     else:
         data = frame.data
     output.write(data)
diff --git a/websockets/http.py b/websockets/http.py
index 81f22a8..464e942 100644
--- a/websockets/http.py
+++ b/websockets/http.py
@@ -8,8 +8,8 @@ imported from :mod:`websockets.http`.
 """
 
 import asyncio
-import email.parser
-import io
+import http.client
+import re
 import sys
 
 from .version import version as websockets_version
@@ -26,26 +26,68 @@ USER_AGENT = ' '.join((
 ))
 
 
+# See https://tools.ietf.org/html/rfc7230#appendix-B.
+
+# Regex for validating header names.
+
+_token_re = re.compile(rb'^[-!#$%&\'*+.^_`|~0-9a-zA-Z]+$')
+
+# Regex for validating header values.
+
+# We don't attempt to support obsolete line folding.
+
+# Include HTAB (\x09), SP (\x20), VCHAR (\x21-\x7e), obs-text (\x80-\xff).
+
+# The ABNF is complicated because it attempts to express that optional
+# whitespace is ignored. We strip whitespace and don't revalidate that.
+
+# See also https://www.rfc-editor.org/errata_search.php?rfc=7230&eid=4189
+
+_value_re = re.compile(rb'^[\x09\x20-\x7e\x80-\xff]*$')
+
+
 @asyncio.coroutine
 def read_request(stream):
     """
-    Read an HTTP/1.1 request from ``stream``.
+    Read an HTTP/1.1 GET request from ``stream``.
+
+    ``stream`` is an :class:`~asyncio.StreamReader`.
 
     Return ``(path, headers)`` where ``path`` is a :class:`str` and
-    ``headers`` is a :class:`~email.message.Message`. ``path`` isn't
-    URL-decoded.
+    ``headers`` is a list of ``(name, value)`` tuples.
+
+    ``path`` isn't URL-decoded or validated in any way.
+
+    Non-ASCII characters are represented with surrogate escapes.
 
     Raise an exception if the request isn't well formatted.
 
-    The request is assumed not to contain a body.
+    Don't attempt to read the request body because WebSocket handshake
+    requests don't have one. If the request contains a body, it may be
+    read from ``stream`` after this coroutine returns.
 
     """
-    request_line, headers = yield from read_message(stream)
-    method, path, version = request_line[:-2].decode().split(None, 2)
-    if method != 'GET':
-        raise ValueError("Unsupported method")
-    if version != 'HTTP/1.1':
-        raise ValueError("Unsupported HTTP version")
+    # https://tools.ietf.org/html/rfc7230#section-3.1.1
+
+    # Parsing is simple because fixed values are expected for method and
+    # version and because path isn't checked. Since WebSocket software tends
+    # to implement HTTP/1.1 strictly, there's little need for lenient parsing.
+
+    # Given the implementation of read_line(), request_line ends with CRLF.
+    request_line = yield from read_line(stream)
+
+    # This may raise "ValueError: not enough values to unpack"
+    method, path, version = request_line[:-2].split(b' ', 2)
+
+    if method != b'GET':
+        raise ValueError("Unsupported HTTP method: %r" % method)
+    if version != b'HTTP/1.1':
+        raise ValueError("Unsupported HTTP version: %r" % version)
+
+    path = path.decode('ascii', 'surrogateescape')
+
+    headers = yield from read_headers(stream)
+
     return path, headers
 
 
@@ -54,44 +96,85 @@ def read_response(stream):
     """
     Read an HTTP/1.1 response from ``stream``.
 
-    Return ``(status, headers)`` where ``status`` is a :class:`int` and
-    ``headers`` is a :class:`~email.message.Message`.
+    ``stream`` is an :class:`~asyncio.StreamReader`.
 
-    Raise an exception if the request isn't well formatted.
+    Return ``(status_code, headers)`` where ``status_code`` is a :class:`int`
+    and ``headers`` is a list of ``(name, value)`` tuples.
+
+    Non-ASCII characters are represented with surrogate escapes.
 
-    The response is assumed not to contain a body.
+    Raise an exception if the response isn't well formatted.
+
+    Don't attempt to read the response body, because WebSocket handshake
+    responses don't have one. If the response contains a body, it may be
+    read from ``stream`` after this coroutine returns.
 
     """
-    status_line, headers = yield from read_message(stream)
-    version, status, reason = status_line[:-2].decode().split(" ", 2)
-    if version != 'HTTP/1.1':
-        raise ValueError("Unsupported HTTP version")
-    return int(status), headers
+    # https://tools.ietf.org/html/rfc7230#section-3.1.2
+
+    # As in read_request, parsing is simple because a fixed value is expected
+    # for version, status_code is a 3-digit number, and reason can be ignored.
+
+    # Given the implementation of read_line(), status_line ends with CRLF.
+    status_line = yield from read_line(stream)
+
+    # This may raise "ValueError: not enough values to unpack"
+    version, status_code, reason = status_line[:-2].split(b' ', 2)
+
+    if version != b'HTTP/1.1':
+        raise ValueError("Unsupported HTTP version: %r" % version)
+    # This may raise "ValueError: invalid literal for int() with base 10"
+    status_code = int(status_code)
+    if not 100 <= status_code < 1000:
+        raise ValueError("Unsupported HTTP status_code code: %d" % status_code)
+    if not _value_re.match(reason):
+        raise ValueError("Invalid HTTP reason phrase: %r" % reason)
+
+    headers = yield from read_headers(stream)
+
+    return status_code, headers
 
 
 @asyncio.coroutine
-def read_message(stream):
+def read_headers(stream):
     """
-    Read an HTTP message from ``stream``.
+    Read HTTP headers from ``stream``.
+
+    ``stream`` is an :class:`~asyncio.StreamReader`.
 
     Return ``(start_line, headers)`` where ``start_line`` is :class:`bytes`
-    and ``headers`` is a :class:`~email.message.Message`.
+    and ``headers`` is a list of ``(name, value)`` tuples.
 
-    The message is assumed not to contain a body.
+    Non-ASCII characters are represented with surrogate escapes.
 
     """
-    start_line = yield from read_line(stream)
-    header_lines = io.BytesIO()
-    for num in range(MAX_HEADERS):
-        header_line = yield from read_line(stream)
-        header_lines.write(header_line)
-        if header_line == b'\r\n':
+    # https://tools.ietf.org/html/rfc7230#section-3.2
+
+    # We don't attempt to support obsolete line folding.
+
+    headers = []
+    for _ in range(MAX_HEADERS):
+        line = yield from read_line(stream)
+        if line == b'\r\n':
             break
+
+        # This may raise "ValueError: not enough values to unpack"
+        name, value = line[:-2].split(b':', 1)
+        if not _token_re.match(name):
+            raise ValueError("Invalid HTTP header name: %r" % name)
+        value = value.strip(b' \t')
+        if not _value_re.match(value):
+            raise ValueError("Invalid HTTP header value: %r" % value)
+
+        headers.append((
+            name.decode('ascii'),   # guaranteed to be ASCII at this point
+            value.decode('ascii', 'surrogateescape'),
+        ))
+
     else:
-        raise ValueError("Too many headers")
-    header_lines.seek(0)
-    headers = email.parser.BytesHeaderParser().parse(header_lines)
-    return start_line, headers
+        raise ValueError("Too many HTTP headers")
+
+    return headers
 
 
 @asyncio.coroutine
@@ -99,10 +182,27 @@ def read_line(stream):
     """
     Read a single line from ``stream``.
 
+    ``stream`` is an :class:`~asyncio.StreamReader`.
+
     """
+    # Security: this is bounded by the StreamReader's limit (default = 32kB).
     line = yield from stream.readline()
+    # Security: this guarantees header values are small (hardcoded = 4kB)
     if len(line) > MAX_LINE:
         raise ValueError("Line too long")
+    # Not mandatory but safe - https://tools.ietf.org/html/rfc7230#section-3.5
     if not line.endswith(b'\r\n'):
         raise ValueError("Line without CRLF")
     return line
+
+
+def build_headers(raw_headers):
+    """
+    Build a date structure for HTTP headers from a list of name - value pairs.
+
+    See also https://github.com/aaugustin/websockets/issues/210.
+
+    """
+    headers = http.client.HTTPMessage()
+    headers._headers = raw_headers  # HACK
+    return headers
diff --git a/websockets/protocol.py b/websockets/protocol.py
index 979ad4b..bfe7e23 100644
--- a/websockets/protocol.py
+++ b/websockets/protocol.py
@@ -15,8 +15,9 @@ import random
 import struct
 
 from .compatibility import asyncio_ensure_future
-from .exceptions import (ConnectionClosed, InvalidState, PayloadTooBig,
-                         WebSocketProtocolError)
+from .exceptions import (
+    ConnectionClosed, InvalidState, PayloadTooBig, WebSocketProtocolError
+)
 from .framing import *
 from .handshake import *
 
@@ -78,14 +79,27 @@ class WebSocketCommonProtocol(asyncio.StreamReaderProtocol):
     this is 128MB. You may want to lower the limits, depending on your
     application's requirements.
 
-    Once the handshake is complete, request and response HTTP headers are
-    available:
+    The ``read_limit`` argument sets the high-water limit of the buffer for
+    incoming bytes. The low-water limit is half the high-water limit. The
+    default value is 64kB, half of asyncio's default (based on the current
+    implementation of :class:`~asyncio.StreamReader`).
 
-    * as a MIME :class:`~email.message.Message` in the :attr:`request_headers`
+    The ``write_limit`` argument sets the high-water limit of the buffer for
+    outgoing bytes. The low-water limit is a quarter of the high-water limit.
+    The default value is 64kB, equal to asyncio's default (based on the
+    current implementation of ``_FlowControlMixin``).
+
+    As soon as the HTTP request and response in the opening handshake are
+    processed, the request path is available in the :attr:`path` attribute,
+    and the request and response HTTP headers are available:
+
+    * as a :class:`~http.client.HTTPMessage` in the :attr:`request_headers`
       and :attr:`response_headers` attributes
     * as an iterable of (name, value) pairs in the :attr:`raw_request_headers`
       and :attr:`raw_response_headers` attributes
 
+    These attributes must be treated as immutable.
+
     If a subprotocol was negotiated, it's available in the :attr:`subprotocol`
     attribute.
 
@@ -104,28 +118,34 @@ class WebSocketCommonProtocol(asyncio.StreamReaderProtocol):
     def __init__(self, *,
                  host=None, port=None, secure=None,
                  timeout=10, max_size=2 ** 20, max_queue=2 ** 5,
+                 read_limit=2 ** 16, write_limit=2 ** 16,
                  loop=None, legacy_recv=False):
         self.host = host
         self.port = port
         self.secure = secure
-
         self.timeout = timeout
         self.max_size = max_size
+        self.max_queue = max_queue
+        self.read_limit = read_limit
+        self.write_limit = write_limit
+
         # Store a reference to loop to avoid relying on self._loop, a private
-        # attribute of StreamReaderProtocol, inherited from FlowControlMixin.
+        # attribute of StreamReaderProtocol, inherited from _FlowControlMixin.
         if loop is None:
             loop = asyncio.get_event_loop()
         self.loop = loop
 
         self.legacy_recv = legacy_recv
 
-        stream_reader = asyncio.StreamReader(loop=loop)
+        # This limit is both the line length limit and half the buffer limit.
+        stream_reader = asyncio.StreamReader(limit=read_limit // 2, loop=loop)
         super().__init__(stream_reader, self.client_connected, loop)
 
         self.reader = None
         self.writer = None
         self._drain_lock = asyncio.Lock(loop=loop)
 
+        self.path = None
         self.request_headers = None
         self.raw_request_headers = None
         self.response_headers = None
@@ -634,6 +654,8 @@ class WebSocketCommonProtocol(asyncio.StreamReaderProtocol):
     def client_connected(self, reader, writer):
         self.reader = reader
         self.writer = writer
+        # Configure write buffer limit.
+        self.writer._transport.set_write_buffer_limits(self.write_limit)
         # Start the task that handles incoming messages.
         self.worker_task = asyncio_ensure_future(self.run(), loop=self.loop)
 
diff --git a/websockets/py35/__pycache__/client.cpython-35.pyc b/websockets/py35/__pycache__/client.cpython-35.pyc
index 71b68ae..191921c 100644
Binary files a/websockets/py35/__pycache__/client.cpython-35.pyc and b/websockets/py35/__pycache__/client.cpython-35.pyc differ
diff --git a/websockets/py35/__pycache__/client.cpython-36.pyc b/websockets/py35/__pycache__/client.cpython-36.pyc
index e704181..a499309 100644
Binary files a/websockets/py35/__pycache__/client.cpython-36.pyc and b/websockets/py35/__pycache__/client.cpython-36.pyc differ
diff --git a/websockets/py35/__pycache__/client_server.cpython-35.pyc b/websockets/py35/__pycache__/client_server.cpython-35.pyc
index d482282..87599b0 100644
Binary files a/websockets/py35/__pycache__/client_server.cpython-35.pyc and b/websockets/py35/__pycache__/client_server.cpython-35.pyc differ
diff --git a/websockets/py35/__pycache__/client_server.cpython-36.pyc b/websockets/py35/__pycache__/client_server.cpython-36.pyc
index 15f129d..cc730b9 100644
Binary files a/websockets/py35/__pycache__/client_server.cpython-36.pyc and b/websockets/py35/__pycache__/client_server.cpython-36.pyc differ
diff --git a/websockets/py35/__pycache__/server.cpython-35.pyc b/websockets/py35/__pycache__/server.cpython-35.pyc
new file mode 100644
index 0000000..5d59619
Binary files /dev/null and b/websockets/py35/__pycache__/server.cpython-35.pyc differ
diff --git a/websockets/py35/__pycache__/server.cpython-36.pyc b/websockets/py35/__pycache__/server.cpython-36.pyc
new file mode 100644
index 0000000..920d1b7
Binary files /dev/null and b/websockets/py35/__pycache__/server.cpython-36.pyc differ
diff --git a/websockets/py35/client_server.py b/websockets/py35/client_server.py
index bfafa39..624824a 100644
--- a/websockets/py35/client_server.py
+++ b/websockets/py35/client_server.py
@@ -17,17 +17,27 @@ class ClientServerContextManager:
     def tearDown(self):
         self.loop.close()
 
-    def test_basic(self):
+    def test_client(self):
         server = serve(handler, 'localhost', 8642)
         self.server = self.loop.run_until_complete(server)
 
-        async def basic():
+        async def run_client():
             async with connect('ws://localhost:8642/') as client:
                 await client.send("Hello!")
                 reply = await client.recv()
                 self.assertEqual(reply, "Hello!")
 
-        self.loop.run_until_complete(basic())
+        self.loop.run_until_complete(run_client())
 
         self.server.close()
         self.loop.run_until_complete(self.server.wait_closed())
+
+    def test_server(self):
+        async def run_server():
+            async with serve(handler, 'localhost', 8642):
+                client = await connect('ws://localhost:8642/')
+                await client.send("Hello!")
+                reply = await client.recv()
+                self.assertEqual(reply, "Hello!")
+
+        self.loop.run_until_complete(run_server())
diff --git a/websockets/py35/server.py b/websockets/py35/server.py
new file mode 100644
index 0000000..3aba1c8
--- /dev/null
+++ b/websockets/py35/server.py
@@ -0,0 +1,22 @@
+class Serve:
+    """
+    This class wraps :func:`~websockets.server.serve` on Python ≥ 3.5.
+
+    This allows using it as an asynchronous context manager.
+
+    """
+    def __init__(self, *args, **kwargs):
+        self.server = self.__class__.__wrapped__(*args, **kwargs)
+
+    async def __aenter__(self):
+        self.server = await self
+        return self.server
+
+    async def __aexit__(self, exc_type, exc_value, traceback):
+        self.server.close()
+        await self.server.wait_closed()
+
+    def __await__(self):
+        return (yield from self.server)
+
+    __iter__ = __await__
diff --git a/websockets/server.py b/websockets/server.py
index dbc3eec..770609c 100644
--- a/websockets/server.py
+++ b/websockets/server.py
@@ -1,3 +1,4 @@
+
 """
 The :mod:`websockets.server` module defines a simple WebSocket server API.
 
@@ -5,13 +6,17 @@ The :mod:`websockets.server` module defines a simple WebSocket server API.
 
 import asyncio
 import collections.abc
-import email.message
 import logging
 
-from .compatibility import asyncio_ensure_future
-from .exceptions import InvalidHandshake, InvalidOrigin
+from .compatibility import (
+    BAD_REQUEST, FORBIDDEN, INTERNAL_SERVER_ERROR, SERVICE_UNAVAILABLE,
+    SWITCHING_PROTOCOLS, asyncio_ensure_future
+)
+from .exceptions import (
+    AbortHandshake, InvalidHandshake, InvalidMessage, InvalidOrigin
+)
 from .handshake import build_response, check_request
-from .http import USER_AGENT, read_request
+from .http import USER_AGENT, build_headers, read_request
 from .protocol import CONNECTING, OPEN, WebSocketCommonProtocol
 
 
@@ -63,22 +68,49 @@ class WebSocketServerProtocol(WebSocketCommonProtocol):
                     origins=self.origins, subprotocols=self.subprotocols,
                     extra_headers=self.extra_headers)
             except ConnectionError as exc:
-                logger.info('Connection error during opening handshake', exc_info=True)
+                logger.debug(
+                    "Connection error in opening handshake", exc_info=True)
                 raise
             except Exception as exc:
                 if self._is_server_shutting_down(exc):
-                    response = ('HTTP/1.1 503 Service Unavailable\r\n\r\n'
-                                'Server is shutting down.')
+                    early_response = (
+                        SERVICE_UNAVAILABLE,
+                        [],
+                        b"Server is shutting down.",
+                    )
+                elif isinstance(exc, AbortHandshake):
+                    early_response = (
+                        exc.status,
+                        exc.headers,
+                        exc.body,
+                    )
                 elif isinstance(exc, InvalidOrigin):
-                    response = 'HTTP/1.1 403 Forbidden\r\n\r\n' + str(exc)
+                    logger.warning("Invalid origin", exc_info=True)
+                    early_response = (
+                        FORBIDDEN,
+                        [],
+                        str(exc).encode(),
+                    )
                 elif isinstance(exc, InvalidHandshake):
-                    response = 'HTTP/1.1 400 Bad Request\r\n\r\n' + str(exc)
+                    logger.warning("Invalid handshake", exc_info=True)
+                    early_response = (
+                        BAD_REQUEST,
+                        [],
+                        str(exc).encode(),
... 1464 lines suppressed ...

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/python-websockets.git



More information about the Python-modules-commits mailing list