[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