tuxbot-bot/venv/lib/python3.7/site-packages/websockets/http.py
2019-12-16 18:12:10 +01:00

331 lines
9.8 KiB
Python

"""
The :mod:`websockets.http` module provides basic HTTP parsing and
serialization. It is merely adequate for WebSocket handshake messages.
Its functions cannot be imported from :mod:`websockets`. They must be imported
from :mod:`websockets.http`.
"""
import asyncio
import collections.abc
import re
import sys
from .version import version as websockets_version
__all__ = [
'Headers', 'MultipleValuesError',
'read_request', 'read_response',
'USER_AGENT',
]
MAX_HEADERS = 256
MAX_LINE = 4096
USER_AGENT = ' '.join((
'Python/{}'.format(sys.version[:3]),
'websockets/{}'.format(websockets_version),
))
# 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 GET request from ``stream``.
``stream`` is an :class:`~asyncio.StreamReader`.
Return ``(path, headers)`` where ``path`` is a :class:`str` and
``headers`` is a :class:`Headers` instance.
``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.
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.
"""
# 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
@asyncio.coroutine
def read_response(stream):
"""
Read an HTTP/1.1 response from ``stream``.
``stream`` is an :class:`~asyncio.StreamReader`.
Return ``(status_code, headers)`` where ``status_code`` is a :class:`int`
and ``headers`` is a :class:`Headers` instance.
Non-ASCII characters are represented with surrogate escapes.
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.
"""
# 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: %d" % status_code)
if not _value_re.fullmatch(reason):
raise ValueError("Invalid HTTP reason phrase: %r" % reason)
headers = yield from read_headers(stream)
return status_code, headers
@asyncio.coroutine
def read_headers(stream):
"""
Read HTTP headers from ``stream``.
``stream`` is an :class:`~asyncio.StreamReader`.
Return a :class:`Headers` instance
Non-ASCII characters are represented with surrogate escapes.
"""
# https://tools.ietf.org/html/rfc7230#section-3.2
# We don't attempt to support obsolete line folding.
headers = Headers()
for _ in range(MAX_HEADERS + 1):
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.fullmatch(name):
raise ValueError("Invalid HTTP header name: %r" % name)
value = value.strip(b' \t')
if not _value_re.fullmatch(value):
raise ValueError("Invalid HTTP header value: %r" % value)
name = name.decode('ascii') # guaranteed to be ASCII at this point
value = value.decode('ascii', 'surrogateescape')
headers[name] = value
else:
raise ValueError("Too many HTTP headers")
return headers
@asyncio.coroutine
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
class MultipleValuesError(LookupError):
"""
Exception raised when :class:`Headers` has more than one value for a key.
"""
def __str__(self):
# Implement the same logic as KeyError_str in Objects/exceptions.c.
if len(self.args) == 1:
return repr(self.args[0])
return super().__str__()
class Headers(collections.abc.MutableMapping):
"""
Data structure for working with HTTP headers efficiently.
A :class:`list` of ``(name, values)`` is inefficient for lookups.
A :class:`dict` doesn't suffice because header names are case-insensitive
and multiple occurrences of headers with the same name are possible.
:class:`Headers` stores HTTP headers in a hybrid data structure to provide
efficient insertions and lookups while preserving the original data.
In order to account for multiple values with minimal hassle,
:class:`Headers` follows this logic:
- When getting a header with ``headers[name]``:
- if there's no value, :exc:`KeyError` is raised;
- if there's exactly one value, it's returned;
- if there's more than one value, :exc:`MultipleValuesError` is raised.
- When setting a header with ``headers[name] = value``, the value is
appended to the list of values for that header.
- When deleting a header with ``del headers[name]``, all values for that
header are removed (this is slow).
Other methods for manipulating headers are consistent with this logic.
As long as no header occurs multiple times, :class:`Headers` behaves like
:class:`dict`, except keys are lower-cased to provide case-insensitivity.
:meth:`get_all()` returns a list of all values for a header and
:meth:`raw_items()` returns an iterator of ``(name, values)`` pairs,
similar to :meth:`http.client.HTTPMessage`.
"""
__slots__ = ['_dict', '_list']
def __init__(self, *args, **kwargs):
self._dict = {}
self._list = []
# MutableMapping.update calls __setitem__ for each (name, value) pair.
self.update(*args, **kwargs)
def __str__(self):
return ''.join(
'{}: {}\r\n'.format(key, value)
for key, value in self._list
) + '\r\n'
def __repr__(self):
return '{}({})'.format(self.__class__.__name__, repr(self._list))
def copy(self):
copy = self.__class__()
copy._dict = self._dict.copy()
copy._list = self._list.copy()
return copy
# Collection methods
def __contains__(self, key):
return key.lower() in self._dict
def __iter__(self):
return iter(self._dict)
def __len__(self):
return len(self._dict)
# MutableMapping methods
def __getitem__(self, key):
value = self._dict[key.lower()]
if len(value) == 1:
return value[0]
else:
raise MultipleValuesError(key)
def __setitem__(self, key, value):
self._dict.setdefault(key.lower(), []).append(value)
self._list.append((key, value))
def __delitem__(self, key):
key_lower = key.lower()
self._dict.__delitem__(key_lower)
# This is inefficent. Fortunately deleting HTTP headers is uncommon.
self._list = [(k, v) for k, v in self._list if k.lower() != key_lower]
def __eq__(self, other):
if not isinstance(other, Headers):
return NotImplemented
return self._list == other._list
def clear(self):
"""
Remove all headers.
"""
self._dict = {}
self._list = []
# Methods for handling multiple values
def get_all(self, key):
"""
Return the (possibly empty) list of all values for a header.
"""
return self._dict.get(key.lower(), [])
def raw_items(self):
"""
Return an iterator of (header name, header value).
"""
return iter(self._list)