Tests/LibWeb: Vendor WPT Python test server infrastructure
Vendor the WPT Python server support used by `.py` test resources so it can be integrated into our test runner. This will allow us to import a larger set of WPT tests, including tests that depend on Python-backed HTTP handlers. Import command ran was: rsync -a --delete --delete-excluded --prune-empty-dirs \ --include='/__init__.py' \ --include='/localpaths.py' \ --include='/wptserve/' \ --include='/wptserve/wptserve/***' \ --include='/third_party/' \ --include='/third_party/pywebsocket3/' \ --include='/third_party/pywebsocket3/pywebsocket3/***' \ --include='/third_party/h2/' \ --include='/third_party/h2/src/' \ --include='/third_party/h2/src/h2/***' \ --include='/third_party/hpack/' \ --include='/third_party/hpack/src/' \ --include='/third_party/hpack/src/hpack/***' \ --include='/third_party/hyperframe/' \ --include='/third_party/hyperframe/src/' \ --include='/third_party/hyperframe/src/hyperframe/***' \ --include='/third_party/six/' \ --include='/third_party/six/six.py' \ --exclude='*' \ Tests/LibWeb/WPT/wpt/tools/ \ Tests/LibWeb/Text/input/wpt-import/_wpttools/
This commit is contained in:
parent
d4f8879e8b
commit
cac30c8d73
68 changed files with 26720 additions and 0 deletions
1
Tests/LibWeb/Text/input/wpt-import/_wpttools/__init__.py
Normal file
1
Tests/LibWeb/Text/input/wpt-import/_wpttools/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import localpaths as _localpaths # noqa: F401
|
||||
36
Tests/LibWeb/Text/input/wpt-import/_wpttools/localpaths.py
Normal file
36
Tests/LibWeb/Text/input/wpt-import/_wpttools/localpaths.py
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import os
|
||||
import sys
|
||||
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
repo_root = os.path.abspath(os.path.join(here, os.pardir))
|
||||
|
||||
sys.path.insert(0, os.path.join(here))
|
||||
sys.path.insert(0, os.path.join(here, "wptserve"))
|
||||
sys.path.insert(0, os.path.join(here, "third_party", "pywebsocket3"))
|
||||
sys.path.insert(0, os.path.join(here, "third_party", "atomicwrites"))
|
||||
sys.path.insert(0, os.path.join(here, "third_party", "attrs", "src"))
|
||||
sys.path.insert(0, os.path.join(here, "third_party", "html5lib"))
|
||||
sys.path.insert(0, os.path.join(here, "third_party", "zipp"))
|
||||
sys.path.insert(0, os.path.join(here, "third_party", "exceptiongroup", "src"))
|
||||
sys.path.insert(0, os.path.join(here, "third_party", "more-itertools"))
|
||||
sys.path.insert(0, os.path.join(here, "third_party", "packaging"))
|
||||
sys.path.insert(0, os.path.join(here, "third_party", "pathlib2"))
|
||||
sys.path.insert(0, os.path.join(here, "third_party", "pluggy", "src"))
|
||||
sys.path.insert(0, os.path.join(here, "third_party", "py"))
|
||||
sys.path.insert(0, os.path.join(here, "third_party", "pytest"))
|
||||
sys.path.insert(0, os.path.join(here, "third_party", "pytest", "src"))
|
||||
sys.path.insert(0, os.path.join(here, "third_party", "pytest-asyncio"))
|
||||
sys.path.insert(0, os.path.join(here, "third_party", "six"))
|
||||
sys.path.insert(0, os.path.join(here, "third_party", "webencodings"))
|
||||
sys.path.insert(0, os.path.join(here, "third_party", "h2", "src"))
|
||||
sys.path.insert(0, os.path.join(here, "third_party", "hpack", "src"))
|
||||
sys.path.insert(0, os.path.join(here, "third_party", "hyperframe", "src"))
|
||||
sys.path.insert(0, os.path.join(here, "third_party", "certifi"))
|
||||
sys.path.insert(0, os.path.join(here, "third_party", "hyper"))
|
||||
sys.path.insert(0, os.path.join(here, "third_party", "websockets", "src"))
|
||||
sys.path.insert(0, os.path.join(here, "third_party", "iniconfig", "src"))
|
||||
if sys.version_info < (3, 8):
|
||||
sys.path.insert(0, os.path.join(here, "third_party", "importlib_metadata"))
|
||||
sys.path.insert(0, os.path.join(here, "webdriver"))
|
||||
sys.path.insert(0, os.path.join(here, "wptrunner"))
|
||||
sys.path.insert(0, os.path.join(here, "webtransport"))
|
||||
8
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/h2/src/h2/__init__.py
vendored
Normal file
8
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/h2/src/h2/__init__.py
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
h2
|
||||
~~
|
||||
|
||||
A HTTP/2 implementation.
|
||||
"""
|
||||
__version__ = '4.1.0'
|
||||
195
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/h2/src/h2/config.py
vendored
Normal file
195
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/h2/src/h2/config.py
vendored
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
h2/config
|
||||
~~~~~~~~~
|
||||
|
||||
Objects for controlling the configuration of the HTTP/2 stack.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
class _BooleanConfigOption:
|
||||
"""
|
||||
Descriptor for handling a boolean config option. This will block
|
||||
attempts to set boolean config options to non-bools.
|
||||
"""
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.attr_name = '_%s' % self.name
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
return getattr(instance, self.attr_name)
|
||||
|
||||
def __set__(self, instance, value):
|
||||
if not isinstance(value, bool):
|
||||
raise ValueError("%s must be a bool" % self.name)
|
||||
setattr(instance, self.attr_name, value)
|
||||
|
||||
|
||||
class DummyLogger:
|
||||
"""
|
||||
A Logger object that does not actual logging, hence a DummyLogger.
|
||||
|
||||
For the class the log operation is merely a no-op. The intent is to avoid
|
||||
conditionals being sprinkled throughout the h2 code for calls to
|
||||
logging functions when no logger is passed into the corresponding object.
|
||||
"""
|
||||
def __init__(self, *vargs):
|
||||
pass
|
||||
|
||||
def debug(self, *vargs, **kwargs):
|
||||
"""
|
||||
No-op logging. Only level needed for now.
|
||||
"""
|
||||
pass
|
||||
|
||||
def trace(self, *vargs, **kwargs):
|
||||
"""
|
||||
No-op logging. Only level needed for now.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class OutputLogger:
|
||||
"""
|
||||
A Logger object that prints to stderr or any other file-like object.
|
||||
|
||||
This class is provided for convenience and not part of the stable API.
|
||||
|
||||
:param file: A file-like object passed to the print function.
|
||||
Defaults to ``sys.stderr``.
|
||||
:param trace: Enables trace-level output. Defaults to ``False``.
|
||||
"""
|
||||
def __init__(self, file=None, trace_level=False):
|
||||
super().__init__()
|
||||
self.file = file or sys.stderr
|
||||
self.trace_level = trace_level
|
||||
|
||||
def debug(self, fmtstr, *args):
|
||||
print(f"h2 (debug): {fmtstr % args}", file=self.file)
|
||||
|
||||
def trace(self, fmtstr, *args):
|
||||
if self.trace_level:
|
||||
print(f"h2 (trace): {fmtstr % args}", file=self.file)
|
||||
|
||||
|
||||
class H2Configuration:
|
||||
"""
|
||||
An object that controls the way a single HTTP/2 connection behaves.
|
||||
|
||||
This object allows the users to customize behaviour. In particular, it
|
||||
allows users to enable or disable optional features, or to otherwise handle
|
||||
various unusual behaviours.
|
||||
|
||||
This object has very little behaviour of its own: it mostly just ensures
|
||||
that configuration is self-consistent.
|
||||
|
||||
:param client_side: Whether this object is to be used on the client side of
|
||||
a connection, or on the server side. Affects the logic used by the
|
||||
state machine, the default settings values, the allowable stream IDs,
|
||||
and several other properties. Defaults to ``True``.
|
||||
:type client_side: ``bool``
|
||||
|
||||
:param header_encoding: Controls whether the headers emitted by this object
|
||||
in events are transparently decoded to ``unicode`` strings, and what
|
||||
encoding is used to do that decoding. This defaults to ``None``,
|
||||
meaning that headers will be returned as bytes. To automatically
|
||||
decode headers (that is, to return them as unicode strings), this can
|
||||
be set to the string name of any encoding, e.g. ``'utf-8'``.
|
||||
|
||||
.. versionchanged:: 3.0.0
|
||||
Changed default value from ``'utf-8'`` to ``None``
|
||||
|
||||
:type header_encoding: ``str``, ``False``, or ``None``
|
||||
|
||||
:param validate_outbound_headers: Controls whether the headers emitted
|
||||
by this object are validated against the rules in RFC 7540.
|
||||
Disabling this setting will cause outbound header validation to
|
||||
be skipped, and allow the object to emit headers that may be illegal
|
||||
according to RFC 7540. Defaults to ``True``.
|
||||
:type validate_outbound_headers: ``bool``
|
||||
|
||||
:param normalize_outbound_headers: Controls whether the headers emitted
|
||||
by this object are normalized before sending. Disabling this setting
|
||||
will cause outbound header normalization to be skipped, and allow
|
||||
the object to emit headers that may be illegal according to
|
||||
RFC 7540. Defaults to ``True``.
|
||||
:type normalize_outbound_headers: ``bool``
|
||||
|
||||
:param validate_inbound_headers: Controls whether the headers received
|
||||
by this object are validated against the rules in RFC 7540.
|
||||
Disabling this setting will cause inbound header validation to
|
||||
be skipped, and allow the object to receive headers that may be illegal
|
||||
according to RFC 7540. Defaults to ``True``.
|
||||
:type validate_inbound_headers: ``bool``
|
||||
|
||||
:param normalize_inbound_headers: Controls whether the headers received by
|
||||
this object are normalized according to the rules of RFC 7540.
|
||||
Disabling this setting may lead to h2 emitting header blocks that
|
||||
some RFCs forbid, e.g. with multiple cookie fields.
|
||||
|
||||
.. versionadded:: 3.0.0
|
||||
|
||||
:type normalize_inbound_headers: ``bool``
|
||||
|
||||
:param logger: A logger that conforms to the requirements for this module,
|
||||
those being no I/O and no context switches, which is needed in order
|
||||
to run in asynchronous operation.
|
||||
|
||||
.. versionadded:: 2.6.0
|
||||
|
||||
:type logger: ``logging.Logger``
|
||||
"""
|
||||
client_side = _BooleanConfigOption('client_side')
|
||||
validate_outbound_headers = _BooleanConfigOption(
|
||||
'validate_outbound_headers'
|
||||
)
|
||||
normalize_outbound_headers = _BooleanConfigOption(
|
||||
'normalize_outbound_headers'
|
||||
)
|
||||
validate_inbound_headers = _BooleanConfigOption(
|
||||
'validate_inbound_headers'
|
||||
)
|
||||
normalize_inbound_headers = _BooleanConfigOption(
|
||||
'normalize_inbound_headers'
|
||||
)
|
||||
|
||||
def __init__(self,
|
||||
client_side=True,
|
||||
header_encoding=None,
|
||||
validate_outbound_headers=True,
|
||||
normalize_outbound_headers=True,
|
||||
validate_inbound_headers=True,
|
||||
normalize_inbound_headers=True,
|
||||
logger=None):
|
||||
self.client_side = client_side
|
||||
self.header_encoding = header_encoding
|
||||
self.validate_outbound_headers = validate_outbound_headers
|
||||
self.normalize_outbound_headers = normalize_outbound_headers
|
||||
self.validate_inbound_headers = validate_inbound_headers
|
||||
self.normalize_inbound_headers = normalize_inbound_headers
|
||||
self.logger = logger or DummyLogger(__name__)
|
||||
|
||||
@property
|
||||
def header_encoding(self):
|
||||
"""
|
||||
Controls whether the headers emitted by this object in events are
|
||||
transparently decoded to ``unicode`` strings, and what encoding is used
|
||||
to do that decoding. This defaults to ``None``, meaning that headers
|
||||
will be returned as bytes. To automatically decode headers (that is, to
|
||||
return them as unicode strings), this can be set to the string name of
|
||||
any encoding, e.g. ``'utf-8'``.
|
||||
"""
|
||||
return self._header_encoding
|
||||
|
||||
@header_encoding.setter
|
||||
def header_encoding(self, value):
|
||||
"""
|
||||
Enforces constraints on the value of header encoding.
|
||||
"""
|
||||
if not isinstance(value, (bool, str, type(None))):
|
||||
raise ValueError("header_encoding must be bool, string, or None")
|
||||
if value is True:
|
||||
raise ValueError("header_encoding cannot be True")
|
||||
self._header_encoding = value
|
||||
2048
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/h2/src/h2/connection.py
vendored
Normal file
2048
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/h2/src/h2/connection.py
vendored
Normal file
File diff suppressed because it is too large
Load diff
75
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/h2/src/h2/errors.py
vendored
Normal file
75
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/h2/src/h2/errors.py
vendored
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
h2/errors
|
||||
~~~~~~~~~
|
||||
|
||||
Global error code registry containing the established HTTP/2 error codes.
|
||||
|
||||
The current registry is available at:
|
||||
https://tools.ietf.org/html/rfc7540#section-11.4
|
||||
"""
|
||||
import enum
|
||||
|
||||
|
||||
class ErrorCodes(enum.IntEnum):
|
||||
"""
|
||||
All known HTTP/2 error codes.
|
||||
|
||||
.. versionadded:: 2.5.0
|
||||
"""
|
||||
#: Graceful shutdown.
|
||||
NO_ERROR = 0x0
|
||||
|
||||
#: Protocol error detected.
|
||||
PROTOCOL_ERROR = 0x1
|
||||
|
||||
#: Implementation fault.
|
||||
INTERNAL_ERROR = 0x2
|
||||
|
||||
#: Flow-control limits exceeded.
|
||||
FLOW_CONTROL_ERROR = 0x3
|
||||
|
||||
#: Settings not acknowledged.
|
||||
SETTINGS_TIMEOUT = 0x4
|
||||
|
||||
#: Frame received for closed stream.
|
||||
STREAM_CLOSED = 0x5
|
||||
|
||||
#: Frame size incorrect.
|
||||
FRAME_SIZE_ERROR = 0x6
|
||||
|
||||
#: Stream not processed.
|
||||
REFUSED_STREAM = 0x7
|
||||
|
||||
#: Stream cancelled.
|
||||
CANCEL = 0x8
|
||||
|
||||
#: Compression state not updated.
|
||||
COMPRESSION_ERROR = 0x9
|
||||
|
||||
#: TCP connection error for CONNECT method.
|
||||
CONNECT_ERROR = 0xa
|
||||
|
||||
#: Processing capacity exceeded.
|
||||
ENHANCE_YOUR_CALM = 0xb
|
||||
|
||||
#: Negotiated TLS parameters not acceptable.
|
||||
INADEQUATE_SECURITY = 0xc
|
||||
|
||||
#: Use HTTP/1.1 for the request.
|
||||
HTTP_1_1_REQUIRED = 0xd
|
||||
|
||||
|
||||
def _error_code_from_int(code):
|
||||
"""
|
||||
Given an integer error code, returns either one of :class:`ErrorCodes
|
||||
<h2.errors.ErrorCodes>` or, if not present in the known set of codes,
|
||||
returns the integer directly.
|
||||
"""
|
||||
try:
|
||||
return ErrorCodes(code)
|
||||
except ValueError:
|
||||
return code
|
||||
|
||||
|
||||
__all__ = ['ErrorCodes']
|
||||
634
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/h2/src/h2/events.py
vendored
Normal file
634
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/h2/src/h2/events.py
vendored
Normal file
|
|
@ -0,0 +1,634 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
h2/events
|
||||
~~~~~~~~~
|
||||
|
||||
Defines Event types for HTTP/2.
|
||||
|
||||
Events are returned by the H2 state machine to allow implementations to keep
|
||||
track of events triggered by receiving data. Each time data is provided to the
|
||||
H2 state machine it processes the data and returns a list of Event objects.
|
||||
"""
|
||||
import binascii
|
||||
|
||||
from .settings import ChangedSetting, _setting_code_from_int
|
||||
|
||||
|
||||
class Event:
|
||||
"""
|
||||
Base class for h2 events.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class RequestReceived(Event):
|
||||
"""
|
||||
The RequestReceived event is fired whenever request headers are received.
|
||||
This event carries the HTTP headers for the given request and the stream ID
|
||||
of the new stream.
|
||||
|
||||
.. versionchanged:: 2.3.0
|
||||
Changed the type of ``headers`` to :class:`HeaderTuple
|
||||
<hpack:hpack.HeaderTuple>`. This has no effect on current users.
|
||||
|
||||
.. versionchanged:: 2.4.0
|
||||
Added ``stream_ended`` and ``priority_updated`` properties.
|
||||
"""
|
||||
def __init__(self):
|
||||
#: The Stream ID for the stream this request was made on.
|
||||
self.stream_id = None
|
||||
|
||||
#: The request headers.
|
||||
self.headers = None
|
||||
|
||||
#: If this request also ended the stream, the associated
|
||||
#: :class:`StreamEnded <h2.events.StreamEnded>` event will be available
|
||||
#: here.
|
||||
#:
|
||||
#: .. versionadded:: 2.4.0
|
||||
self.stream_ended = None
|
||||
|
||||
#: If this request also had associated priority information, the
|
||||
#: associated :class:`PriorityUpdated <h2.events.PriorityUpdated>`
|
||||
#: event will be available here.
|
||||
#:
|
||||
#: .. versionadded:: 2.4.0
|
||||
self.priority_updated = None
|
||||
|
||||
def __repr__(self):
|
||||
return "<RequestReceived stream_id:%s, headers:%s>" % (
|
||||
self.stream_id, self.headers
|
||||
)
|
||||
|
||||
|
||||
class ResponseReceived(Event):
|
||||
"""
|
||||
The ResponseReceived event is fired whenever response headers are received.
|
||||
This event carries the HTTP headers for the given response and the stream
|
||||
ID of the new stream.
|
||||
|
||||
.. versionchanged:: 2.3.0
|
||||
Changed the type of ``headers`` to :class:`HeaderTuple
|
||||
<hpack:hpack.HeaderTuple>`. This has no effect on current users.
|
||||
|
||||
.. versionchanged:: 2.4.0
|
||||
Added ``stream_ended`` and ``priority_updated`` properties.
|
||||
"""
|
||||
def __init__(self):
|
||||
#: The Stream ID for the stream this response was made on.
|
||||
self.stream_id = None
|
||||
|
||||
#: The response headers.
|
||||
self.headers = None
|
||||
|
||||
#: If this response also ended the stream, the associated
|
||||
#: :class:`StreamEnded <h2.events.StreamEnded>` event will be available
|
||||
#: here.
|
||||
#:
|
||||
#: .. versionadded:: 2.4.0
|
||||
self.stream_ended = None
|
||||
|
||||
#: If this response also had associated priority information, the
|
||||
#: associated :class:`PriorityUpdated <h2.events.PriorityUpdated>`
|
||||
#: event will be available here.
|
||||
#:
|
||||
#: .. versionadded:: 2.4.0
|
||||
self.priority_updated = None
|
||||
|
||||
def __repr__(self):
|
||||
return "<ResponseReceived stream_id:%s, headers:%s>" % (
|
||||
self.stream_id, self.headers
|
||||
)
|
||||
|
||||
|
||||
class TrailersReceived(Event):
|
||||
"""
|
||||
The TrailersReceived event is fired whenever trailers are received on a
|
||||
stream. Trailers are a set of headers sent after the body of the
|
||||
request/response, and are used to provide information that wasn't known
|
||||
ahead of time (e.g. content-length). This event carries the HTTP header
|
||||
fields that form the trailers and the stream ID of the stream on which they
|
||||
were received.
|
||||
|
||||
.. versionchanged:: 2.3.0
|
||||
Changed the type of ``headers`` to :class:`HeaderTuple
|
||||
<hpack:hpack.HeaderTuple>`. This has no effect on current users.
|
||||
|
||||
.. versionchanged:: 2.4.0
|
||||
Added ``stream_ended`` and ``priority_updated`` properties.
|
||||
"""
|
||||
def __init__(self):
|
||||
#: The Stream ID for the stream on which these trailers were received.
|
||||
self.stream_id = None
|
||||
|
||||
#: The trailers themselves.
|
||||
self.headers = None
|
||||
|
||||
#: Trailers always end streams. This property has the associated
|
||||
#: :class:`StreamEnded <h2.events.StreamEnded>` in it.
|
||||
#:
|
||||
#: .. versionadded:: 2.4.0
|
||||
self.stream_ended = None
|
||||
|
||||
#: If the trailers also set associated priority information, the
|
||||
#: associated :class:`PriorityUpdated <h2.events.PriorityUpdated>`
|
||||
#: event will be available here.
|
||||
#:
|
||||
#: .. versionadded:: 2.4.0
|
||||
self.priority_updated = None
|
||||
|
||||
def __repr__(self):
|
||||
return "<TrailersReceived stream_id:%s, headers:%s>" % (
|
||||
self.stream_id, self.headers
|
||||
)
|
||||
|
||||
|
||||
class _HeadersSent(Event):
|
||||
"""
|
||||
The _HeadersSent event is fired whenever headers are sent.
|
||||
|
||||
This is an internal event, used to determine validation steps on
|
||||
outgoing header blocks.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class _ResponseSent(_HeadersSent):
|
||||
"""
|
||||
The _ResponseSent event is fired whenever response headers are sent
|
||||
on a stream.
|
||||
|
||||
This is an internal event, used to determine validation steps on
|
||||
outgoing header blocks.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class _RequestSent(_HeadersSent):
|
||||
"""
|
||||
The _RequestSent event is fired whenever request headers are sent
|
||||
on a stream.
|
||||
|
||||
This is an internal event, used to determine validation steps on
|
||||
outgoing header blocks.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class _TrailersSent(_HeadersSent):
|
||||
"""
|
||||
The _TrailersSent event is fired whenever trailers are sent on a
|
||||
stream. Trailers are a set of headers sent after the body of the
|
||||
request/response, and are used to provide information that wasn't known
|
||||
ahead of time (e.g. content-length).
|
||||
|
||||
This is an internal event, used to determine validation steps on
|
||||
outgoing header blocks.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class _PushedRequestSent(_HeadersSent):
|
||||
"""
|
||||
The _PushedRequestSent event is fired whenever pushed request headers are
|
||||
sent.
|
||||
|
||||
This is an internal event, used to determine validation steps on outgoing
|
||||
header blocks.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class InformationalResponseReceived(Event):
|
||||
"""
|
||||
The InformationalResponseReceived event is fired when an informational
|
||||
response (that is, one whose status code is a 1XX code) is received from
|
||||
the remote peer.
|
||||
|
||||
The remote peer may send any number of these, from zero upwards. These
|
||||
responses are most commonly sent in response to requests that have the
|
||||
``expect: 100-continue`` header field present. Most users can safely
|
||||
ignore this event unless you are intending to use the
|
||||
``expect: 100-continue`` flow, or are for any reason expecting a different
|
||||
1XX status code.
|
||||
|
||||
.. versionadded:: 2.2.0
|
||||
|
||||
.. versionchanged:: 2.3.0
|
||||
Changed the type of ``headers`` to :class:`HeaderTuple
|
||||
<hpack:hpack.HeaderTuple>`. This has no effect on current users.
|
||||
|
||||
.. versionchanged:: 2.4.0
|
||||
Added ``priority_updated`` property.
|
||||
"""
|
||||
def __init__(self):
|
||||
#: The Stream ID for the stream this informational response was made
|
||||
#: on.
|
||||
self.stream_id = None
|
||||
|
||||
#: The headers for this informational response.
|
||||
self.headers = None
|
||||
|
||||
#: If this response also had associated priority information, the
|
||||
#: associated :class:`PriorityUpdated <h2.events.PriorityUpdated>`
|
||||
#: event will be available here.
|
||||
#:
|
||||
#: .. versionadded:: 2.4.0
|
||||
self.priority_updated = None
|
||||
|
||||
def __repr__(self):
|
||||
return "<InformationalResponseReceived stream_id:%s, headers:%s>" % (
|
||||
self.stream_id, self.headers
|
||||
)
|
||||
|
||||
|
||||
class DataReceived(Event):
|
||||
"""
|
||||
The DataReceived event is fired whenever data is received on a stream from
|
||||
the remote peer. The event carries the data itself, and the stream ID on
|
||||
which the data was received.
|
||||
|
||||
.. versionchanged:: 2.4.0
|
||||
Added ``stream_ended`` property.
|
||||
"""
|
||||
def __init__(self):
|
||||
#: The Stream ID for the stream this data was received on.
|
||||
self.stream_id = None
|
||||
|
||||
#: The data itself.
|
||||
self.data = None
|
||||
|
||||
#: The amount of data received that counts against the flow control
|
||||
#: window. Note that padding counts against the flow control window, so
|
||||
#: when adjusting flow control you should always use this field rather
|
||||
#: than ``len(data)``.
|
||||
self.flow_controlled_length = None
|
||||
|
||||
#: If this data chunk also completed the stream, the associated
|
||||
#: :class:`StreamEnded <h2.events.StreamEnded>` event will be available
|
||||
#: here.
|
||||
#:
|
||||
#: .. versionadded:: 2.4.0
|
||||
self.stream_ended = None
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
"<DataReceived stream_id:%s, "
|
||||
"flow_controlled_length:%s, "
|
||||
"data:%s>" % (
|
||||
self.stream_id,
|
||||
self.flow_controlled_length,
|
||||
_bytes_representation(self.data[:20]),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class WindowUpdated(Event):
|
||||
"""
|
||||
The WindowUpdated event is fired whenever a flow control window changes
|
||||
size. HTTP/2 defines flow control windows for connections and streams: this
|
||||
event fires for both connections and streams. The event carries the ID of
|
||||
the stream to which it applies (set to zero if the window update applies to
|
||||
the connection), and the delta in the window size.
|
||||
"""
|
||||
def __init__(self):
|
||||
#: The Stream ID of the stream whose flow control window was changed.
|
||||
#: May be ``0`` if the connection window was changed.
|
||||
self.stream_id = None
|
||||
|
||||
#: The window delta.
|
||||
self.delta = None
|
||||
|
||||
def __repr__(self):
|
||||
return "<WindowUpdated stream_id:%s, delta:%s>" % (
|
||||
self.stream_id, self.delta
|
||||
)
|
||||
|
||||
|
||||
class RemoteSettingsChanged(Event):
|
||||
"""
|
||||
The RemoteSettingsChanged event is fired whenever the remote peer changes
|
||||
its settings. It contains a complete inventory of changed settings,
|
||||
including their previous values.
|
||||
|
||||
In HTTP/2, settings changes need to be acknowledged. h2 automatically
|
||||
acknowledges settings changes for efficiency. However, it is possible that
|
||||
the caller may not be happy with the changed setting.
|
||||
|
||||
When this event is received, the caller should confirm that the new
|
||||
settings are acceptable. If they are not acceptable, the user should close
|
||||
the connection with the error code :data:`PROTOCOL_ERROR
|
||||
<h2.errors.ErrorCodes.PROTOCOL_ERROR>`.
|
||||
|
||||
.. versionchanged:: 2.0.0
|
||||
Prior to this version the user needed to acknowledge settings changes.
|
||||
This is no longer the case: h2 now automatically acknowledges
|
||||
them.
|
||||
"""
|
||||
def __init__(self):
|
||||
#: A dictionary of setting byte to
|
||||
#: :class:`ChangedSetting <h2.settings.ChangedSetting>`, representing
|
||||
#: the changed settings.
|
||||
self.changed_settings = {}
|
||||
|
||||
@classmethod
|
||||
def from_settings(cls, old_settings, new_settings):
|
||||
"""
|
||||
Build a RemoteSettingsChanged event from a set of changed settings.
|
||||
|
||||
:param old_settings: A complete collection of old settings, in the form
|
||||
of a dictionary of ``{setting: value}``.
|
||||
:param new_settings: All the changed settings and their new values, in
|
||||
the form of a dictionary of ``{setting: value}``.
|
||||
"""
|
||||
e = cls()
|
||||
for setting, new_value in new_settings.items():
|
||||
setting = _setting_code_from_int(setting)
|
||||
original_value = old_settings.get(setting)
|
||||
change = ChangedSetting(setting, original_value, new_value)
|
||||
e.changed_settings[setting] = change
|
||||
|
||||
return e
|
||||
|
||||
def __repr__(self):
|
||||
return "<RemoteSettingsChanged changed_settings:{%s}>" % (
|
||||
", ".join(repr(cs) for cs in self.changed_settings.values()),
|
||||
)
|
||||
|
||||
|
||||
class PingReceived(Event):
|
||||
"""
|
||||
The PingReceived event is fired whenever a PING is received. It contains
|
||||
the 'opaque data' of the PING frame. A ping acknowledgment with the same
|
||||
'opaque data' is automatically emitted after receiving a ping.
|
||||
|
||||
.. versionadded:: 3.1.0
|
||||
"""
|
||||
def __init__(self):
|
||||
#: The data included on the ping.
|
||||
self.ping_data = None
|
||||
|
||||
def __repr__(self):
|
||||
return "<PingReceived ping_data:%s>" % (
|
||||
_bytes_representation(self.ping_data),
|
||||
)
|
||||
|
||||
|
||||
class PingAckReceived(Event):
|
||||
"""
|
||||
The PingAckReceived event is fired whenever a PING acknowledgment is
|
||||
received. It contains the 'opaque data' of the PING+ACK frame, allowing the
|
||||
user to correlate PINGs and calculate RTT.
|
||||
|
||||
.. versionadded:: 3.1.0
|
||||
|
||||
.. versionchanged:: 4.0.0
|
||||
Removed deprecated but equivalent ``PingAcknowledged``.
|
||||
"""
|
||||
def __init__(self):
|
||||
#: The data included on the ping.
|
||||
self.ping_data = None
|
||||
|
||||
def __repr__(self):
|
||||
return "<PingAckReceived ping_data:%s>" % (
|
||||
_bytes_representation(self.ping_data),
|
||||
)
|
||||
|
||||
|
||||
class StreamEnded(Event):
|
||||
"""
|
||||
The StreamEnded event is fired whenever a stream is ended by a remote
|
||||
party. The stream may not be fully closed if it has not been closed
|
||||
locally, but no further data or headers should be expected on that stream.
|
||||
"""
|
||||
def __init__(self):
|
||||
#: The Stream ID of the stream that was closed.
|
||||
self.stream_id = None
|
||||
|
||||
def __repr__(self):
|
||||
return "<StreamEnded stream_id:%s>" % self.stream_id
|
||||
|
||||
|
||||
class StreamReset(Event):
|
||||
"""
|
||||
The StreamReset event is fired in two situations. The first is when the
|
||||
remote party forcefully resets the stream. The second is when the remote
|
||||
party has made a protocol error which only affects a single stream. In this
|
||||
case, h2 will terminate the stream early and return this event.
|
||||
|
||||
.. versionchanged:: 2.0.0
|
||||
This event is now fired when h2 automatically resets a stream.
|
||||
"""
|
||||
def __init__(self):
|
||||
#: The Stream ID of the stream that was reset.
|
||||
self.stream_id = None
|
||||
|
||||
#: The error code given. Either one of :class:`ErrorCodes
|
||||
#: <h2.errors.ErrorCodes>` or ``int``
|
||||
self.error_code = None
|
||||
|
||||
#: Whether the remote peer sent a RST_STREAM or we did.
|
||||
self.remote_reset = True
|
||||
|
||||
def __repr__(self):
|
||||
return "<StreamReset stream_id:%s, error_code:%s, remote_reset:%s>" % (
|
||||
self.stream_id, self.error_code, self.remote_reset
|
||||
)
|
||||
|
||||
|
||||
class PushedStreamReceived(Event):
|
||||
"""
|
||||
The PushedStreamReceived event is fired whenever a pushed stream has been
|
||||
received from a remote peer. The event carries on it the new stream ID, the
|
||||
ID of the parent stream, and the request headers pushed by the remote peer.
|
||||
"""
|
||||
def __init__(self):
|
||||
#: The Stream ID of the stream created by the push.
|
||||
self.pushed_stream_id = None
|
||||
|
||||
#: The Stream ID of the stream that the push is related to.
|
||||
self.parent_stream_id = None
|
||||
|
||||
#: The request headers, sent by the remote party in the push.
|
||||
self.headers = None
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
"<PushedStreamReceived pushed_stream_id:%s, parent_stream_id:%s, "
|
||||
"headers:%s>" % (
|
||||
self.pushed_stream_id,
|
||||
self.parent_stream_id,
|
||||
self.headers,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class SettingsAcknowledged(Event):
|
||||
"""
|
||||
The SettingsAcknowledged event is fired whenever a settings ACK is received
|
||||
from the remote peer. The event carries on it the settings that were
|
||||
acknowedged, in the same format as
|
||||
:class:`h2.events.RemoteSettingsChanged`.
|
||||
"""
|
||||
def __init__(self):
|
||||
#: A dictionary of setting byte to
|
||||
#: :class:`ChangedSetting <h2.settings.ChangedSetting>`, representing
|
||||
#: the changed settings.
|
||||
self.changed_settings = {}
|
||||
|
||||
def __repr__(self):
|
||||
return "<SettingsAcknowledged changed_settings:{%s}>" % (
|
||||
", ".join(repr(cs) for cs in self.changed_settings.values()),
|
||||
)
|
||||
|
||||
|
||||
class PriorityUpdated(Event):
|
||||
"""
|
||||
The PriorityUpdated event is fired whenever a stream sends updated priority
|
||||
information. This can occur when the stream is opened, or at any time
|
||||
during the stream lifetime.
|
||||
|
||||
This event is purely advisory, and does not need to be acted on.
|
||||
|
||||
.. versionadded:: 2.0.0
|
||||
"""
|
||||
def __init__(self):
|
||||
#: The ID of the stream whose priority information is being updated.
|
||||
self.stream_id = None
|
||||
|
||||
#: The new stream weight. May be the same as the original stream
|
||||
#: weight. An integer between 1 and 256.
|
||||
self.weight = None
|
||||
|
||||
#: The stream ID this stream now depends on. May be ``0``.
|
||||
self.depends_on = None
|
||||
|
||||
#: Whether the stream *exclusively* depends on the parent stream. If it
|
||||
#: does, this stream should inherit the current children of its new
|
||||
#: parent.
|
||||
self.exclusive = None
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
"<PriorityUpdated stream_id:%s, weight:%s, depends_on:%s, "
|
||||
"exclusive:%s>" % (
|
||||
self.stream_id,
|
||||
self.weight,
|
||||
self.depends_on,
|
||||
self.exclusive
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class ConnectionTerminated(Event):
|
||||
"""
|
||||
The ConnectionTerminated event is fired when a connection is torn down by
|
||||
the remote peer using a GOAWAY frame. Once received, no further action may
|
||||
be taken on the connection: a new connection must be established.
|
||||
"""
|
||||
def __init__(self):
|
||||
#: The error code cited when tearing down the connection. Should be
|
||||
#: one of :class:`ErrorCodes <h2.errors.ErrorCodes>`, but may not be if
|
||||
#: unknown HTTP/2 extensions are being used.
|
||||
self.error_code = None
|
||||
|
||||
#: The stream ID of the last stream the remote peer saw. This can
|
||||
#: provide an indication of what data, if any, never reached the remote
|
||||
#: peer and so can safely be resent.
|
||||
self.last_stream_id = None
|
||||
|
||||
#: Additional debug data that can be appended to GOAWAY frame.
|
||||
self.additional_data = None
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
"<ConnectionTerminated error_code:%s, last_stream_id:%s, "
|
||||
"additional_data:%s>" % (
|
||||
self.error_code,
|
||||
self.last_stream_id,
|
||||
_bytes_representation(
|
||||
self.additional_data[:20]
|
||||
if self.additional_data else None)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class AlternativeServiceAvailable(Event):
|
||||
"""
|
||||
The AlternativeServiceAvailable event is fired when the remote peer
|
||||
advertises an `RFC 7838 <https://tools.ietf.org/html/rfc7838>`_ Alternative
|
||||
Service using an ALTSVC frame.
|
||||
|
||||
This event always carries the origin to which the ALTSVC information
|
||||
applies. That origin is either supplied by the server directly, or inferred
|
||||
by h2 from the ``:authority`` pseudo-header field that was sent by
|
||||
the user when initiating a given stream.
|
||||
|
||||
This event also carries what RFC 7838 calls the "Alternative Service Field
|
||||
Value", which is formatted like a HTTP header field and contains the
|
||||
relevant alternative service information. h2 does not parse or in any
|
||||
way modify that information: the user is required to do that.
|
||||
|
||||
This event can only be fired on the client end of a connection.
|
||||
|
||||
.. versionadded:: 2.3.0
|
||||
"""
|
||||
def __init__(self):
|
||||
#: The origin to which the alternative service field value applies.
|
||||
#: This field is either supplied by the server directly, or inferred by
|
||||
#: h2 from the ``:authority`` pseudo-header field that was sent
|
||||
#: by the user when initiating the stream on which the frame was
|
||||
#: received.
|
||||
self.origin = None
|
||||
|
||||
#: The ALTSVC field value. This contains information about the HTTP
|
||||
#: alternative service being advertised by the server. h2 does
|
||||
#: not parse this field: it is left exactly as sent by the server. The
|
||||
#: structure of the data in this field is given by `RFC 7838 Section 3
|
||||
#: <https://tools.ietf.org/html/rfc7838#section-3>`_.
|
||||
self.field_value = None
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
"<AlternativeServiceAvailable origin:%s, field_value:%s>" % (
|
||||
self.origin.decode('utf-8', 'ignore'),
|
||||
self.field_value.decode('utf-8', 'ignore'),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class UnknownFrameReceived(Event):
|
||||
"""
|
||||
The UnknownFrameReceived event is fired when the remote peer sends a frame
|
||||
that h2 does not understand. This occurs primarily when the remote
|
||||
peer is employing HTTP/2 extensions that h2 doesn't know anything
|
||||
about.
|
||||
|
||||
RFC 7540 requires that HTTP/2 implementations ignore these frames. h2
|
||||
does so. However, this event is fired to allow implementations to perform
|
||||
special processing on those frames if needed (e.g. if the implementation
|
||||
is capable of handling the frame itself).
|
||||
|
||||
.. versionadded:: 2.7.0
|
||||
"""
|
||||
def __init__(self):
|
||||
#: The hyperframe Frame object that encapsulates the received frame.
|
||||
self.frame = None
|
||||
|
||||
def __repr__(self):
|
||||
return "<UnknownFrameReceived>"
|
||||
|
||||
|
||||
def _bytes_representation(data):
|
||||
"""
|
||||
Converts a bytestring into something that is safe to print on all Python
|
||||
platforms.
|
||||
|
||||
This function is relatively expensive, so it should not be called on the
|
||||
mainline of the code. It's safe to use in things like object repr methods
|
||||
though.
|
||||
"""
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
return binascii.hexlify(data).decode('ascii')
|
||||
187
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/h2/src/h2/exceptions.py
vendored
Normal file
187
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/h2/src/h2/exceptions.py
vendored
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
h2/exceptions
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Exceptions for the HTTP/2 module.
|
||||
"""
|
||||
import h2.errors
|
||||
|
||||
|
||||
class H2Error(Exception):
|
||||
"""
|
||||
The base class for all exceptions for the HTTP/2 module.
|
||||
"""
|
||||
|
||||
|
||||
class ProtocolError(H2Error):
|
||||
"""
|
||||
An action was attempted in violation of the HTTP/2 protocol.
|
||||
"""
|
||||
#: The error code corresponds to this kind of Protocol Error.
|
||||
error_code = h2.errors.ErrorCodes.PROTOCOL_ERROR
|
||||
|
||||
|
||||
class FrameTooLargeError(ProtocolError):
|
||||
"""
|
||||
The frame that we tried to send or that we received was too large.
|
||||
"""
|
||||
#: The error code corresponds to this kind of Protocol Error.
|
||||
error_code = h2.errors.ErrorCodes.FRAME_SIZE_ERROR
|
||||
|
||||
|
||||
class FrameDataMissingError(ProtocolError):
|
||||
"""
|
||||
The frame that we received is missing some data.
|
||||
|
||||
.. versionadded:: 2.0.0
|
||||
"""
|
||||
#: The error code corresponds to this kind of Protocol Error.
|
||||
error_code = h2.errors.ErrorCodes.FRAME_SIZE_ERROR
|
||||
|
||||
|
||||
class TooManyStreamsError(ProtocolError):
|
||||
"""
|
||||
An attempt was made to open a stream that would lead to too many concurrent
|
||||
streams.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class FlowControlError(ProtocolError):
|
||||
"""
|
||||
An attempted action violates flow control constraints.
|
||||
"""
|
||||
#: The error code corresponds to this kind of Protocol Error.
|
||||
error_code = h2.errors.ErrorCodes.FLOW_CONTROL_ERROR
|
||||
|
||||
|
||||
class StreamIDTooLowError(ProtocolError):
|
||||
"""
|
||||
An attempt was made to open a stream that had an ID that is lower than the
|
||||
highest ID we have seen on this connection.
|
||||
"""
|
||||
def __init__(self, stream_id, max_stream_id):
|
||||
#: The ID of the stream that we attempted to open.
|
||||
self.stream_id = stream_id
|
||||
|
||||
#: The current highest-seen stream ID.
|
||||
self.max_stream_id = max_stream_id
|
||||
|
||||
def __str__(self):
|
||||
return "StreamIDTooLowError: %d is lower than %d" % (
|
||||
self.stream_id, self.max_stream_id
|
||||
)
|
||||
|
||||
|
||||
class NoAvailableStreamIDError(ProtocolError):
|
||||
"""
|
||||
There are no available stream IDs left to the connection. All stream IDs
|
||||
have been exhausted.
|
||||
|
||||
.. versionadded:: 2.0.0
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class NoSuchStreamError(ProtocolError):
|
||||
"""
|
||||
A stream-specific action referenced a stream that does not exist.
|
||||
|
||||
.. versionchanged:: 2.0.0
|
||||
Became a subclass of :class:`ProtocolError
|
||||
<h2.exceptions.ProtocolError>`
|
||||
"""
|
||||
def __init__(self, stream_id):
|
||||
#: The stream ID corresponds to the non-existent stream.
|
||||
self.stream_id = stream_id
|
||||
|
||||
|
||||
class StreamClosedError(NoSuchStreamError):
|
||||
"""
|
||||
A more specific form of
|
||||
:class:`NoSuchStreamError <h2.exceptions.NoSuchStreamError>`. Indicates
|
||||
that the stream has since been closed, and that all state relating to that
|
||||
stream has been removed.
|
||||
"""
|
||||
def __init__(self, stream_id):
|
||||
#: The stream ID corresponds to the nonexistent stream.
|
||||
self.stream_id = stream_id
|
||||
|
||||
#: The relevant HTTP/2 error code.
|
||||
self.error_code = h2.errors.ErrorCodes.STREAM_CLOSED
|
||||
|
||||
# Any events that internal code may need to fire. Not relevant to
|
||||
# external users that may receive a StreamClosedError.
|
||||
self._events = []
|
||||
|
||||
|
||||
class InvalidSettingsValueError(ProtocolError, ValueError):
|
||||
"""
|
||||
An attempt was made to set an invalid Settings value.
|
||||
|
||||
.. versionadded:: 2.0.0
|
||||
"""
|
||||
def __init__(self, msg, error_code):
|
||||
super(InvalidSettingsValueError, self).__init__(msg)
|
||||
self.error_code = error_code
|
||||
|
||||
|
||||
class InvalidBodyLengthError(ProtocolError):
|
||||
"""
|
||||
The remote peer sent more or less data that the Content-Length header
|
||||
indicated.
|
||||
|
||||
.. versionadded:: 2.0.0
|
||||
"""
|
||||
def __init__(self, expected, actual):
|
||||
self.expected_length = expected
|
||||
self.actual_length = actual
|
||||
|
||||
def __str__(self):
|
||||
return "InvalidBodyLengthError: Expected %d bytes, received %d" % (
|
||||
self.expected_length, self.actual_length
|
||||
)
|
||||
|
||||
|
||||
class UnsupportedFrameError(ProtocolError):
|
||||
"""
|
||||
The remote peer sent a frame that is unsupported in this context.
|
||||
|
||||
.. versionadded:: 2.1.0
|
||||
|
||||
.. versionchanged:: 4.0.0
|
||||
Removed deprecated KeyError parent class.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class RFC1122Error(H2Error):
|
||||
"""
|
||||
Emitted when users attempt to do something that is literally allowed by the
|
||||
relevant RFC, but is sufficiently ill-defined that it's unwise to allow
|
||||
users to actually do it.
|
||||
|
||||
While there is some disagreement about whether or not we should be liberal
|
||||
in what accept, it is a truth universally acknowledged that we should be
|
||||
conservative in what emit.
|
||||
|
||||
.. versionadded:: 2.4.0
|
||||
"""
|
||||
# shazow says I'm going to regret naming the exception this way. If that
|
||||
# turns out to be true, TELL HIM NOTHING.
|
||||
pass
|
||||
|
||||
|
||||
class DenialOfServiceError(ProtocolError):
|
||||
"""
|
||||
Emitted when the remote peer exhibits a behaviour that is likely to be an
|
||||
attempt to perform a Denial of Service attack on the implementation. This
|
||||
is a form of ProtocolError that carries a different error code, and allows
|
||||
more easy detection of this kind of behaviour.
|
||||
|
||||
.. versionadded:: 2.5.0
|
||||
"""
|
||||
#: The error code corresponds to this kind of
|
||||
#: :class:`ProtocolError <h2.exceptions.ProtocolError>`
|
||||
error_code = h2.errors.ErrorCodes.ENHANCE_YOUR_CALM
|
||||
160
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/h2/src/h2/frame_buffer.py
vendored
Normal file
160
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/h2/src/h2/frame_buffer.py
vendored
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
h2/frame_buffer
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
A data structure that provides a way to iterate over a byte buffer in terms of
|
||||
frames.
|
||||
"""
|
||||
from hyperframe.exceptions import InvalidFrameError, InvalidDataError
|
||||
from hyperframe.frame import (
|
||||
Frame, HeadersFrame, ContinuationFrame, PushPromiseFrame
|
||||
)
|
||||
|
||||
from .exceptions import (
|
||||
ProtocolError, FrameTooLargeError, FrameDataMissingError
|
||||
)
|
||||
|
||||
# To avoid a DOS attack based on sending loads of continuation frames, we limit
|
||||
# the maximum number we're perpared to receive. In this case, we'll set the
|
||||
# limit to 64, which means the largest encoded header block we can receive by
|
||||
# default is 262144 bytes long, and the largest possible *at all* is 1073741760
|
||||
# bytes long.
|
||||
#
|
||||
# This value seems reasonable for now, but in future we may want to evaluate
|
||||
# making it configurable.
|
||||
CONTINUATION_BACKLOG = 64
|
||||
|
||||
|
||||
class FrameBuffer:
|
||||
"""
|
||||
This is a data structure that expects to act as a buffer for HTTP/2 data
|
||||
that allows iteraton in terms of H2 frames.
|
||||
"""
|
||||
def __init__(self, server=False):
|
||||
self.data = b''
|
||||
self.max_frame_size = 0
|
||||
self._preamble = b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n' if server else b''
|
||||
self._preamble_len = len(self._preamble)
|
||||
self._headers_buffer = []
|
||||
|
||||
def add_data(self, data):
|
||||
"""
|
||||
Add more data to the frame buffer.
|
||||
|
||||
:param data: A bytestring containing the byte buffer.
|
||||
"""
|
||||
if self._preamble_len:
|
||||
data_len = len(data)
|
||||
of_which_preamble = min(self._preamble_len, data_len)
|
||||
|
||||
if self._preamble[:of_which_preamble] != data[:of_which_preamble]:
|
||||
raise ProtocolError("Invalid HTTP/2 preamble.")
|
||||
|
||||
data = data[of_which_preamble:]
|
||||
self._preamble_len -= of_which_preamble
|
||||
self._preamble = self._preamble[of_which_preamble:]
|
||||
|
||||
self.data += data
|
||||
|
||||
def _validate_frame_length(self, length):
|
||||
"""
|
||||
Confirm that the frame is an appropriate length.
|
||||
"""
|
||||
if length > self.max_frame_size:
|
||||
raise FrameTooLargeError(
|
||||
"Received overlong frame: length %d, max %d" %
|
||||
(length, self.max_frame_size)
|
||||
)
|
||||
|
||||
def _update_header_buffer(self, f):
|
||||
"""
|
||||
Updates the internal header buffer. Returns a frame that should replace
|
||||
the current one. May throw exceptions if this frame is invalid.
|
||||
"""
|
||||
# Check if we're in the middle of a headers block. If we are, this
|
||||
# frame *must* be a CONTINUATION frame with the same stream ID as the
|
||||
# leading HEADERS or PUSH_PROMISE frame. Anything else is a
|
||||
# ProtocolError. If the frame *is* valid, append it to the header
|
||||
# buffer.
|
||||
if self._headers_buffer:
|
||||
stream_id = self._headers_buffer[0].stream_id
|
||||
valid_frame = (
|
||||
f is not None and
|
||||
isinstance(f, ContinuationFrame) and
|
||||
f.stream_id == stream_id
|
||||
)
|
||||
if not valid_frame:
|
||||
raise ProtocolError("Invalid frame during header block.")
|
||||
|
||||
# Append the frame to the buffer.
|
||||
self._headers_buffer.append(f)
|
||||
if len(self._headers_buffer) > CONTINUATION_BACKLOG:
|
||||
raise ProtocolError("Too many continuation frames received.")
|
||||
|
||||
# If this is the end of the header block, then we want to build a
|
||||
# mutant HEADERS frame that's massive. Use the original one we got,
|
||||
# then set END_HEADERS and set its data appopriately. If it's not
|
||||
# the end of the block, lose the current frame: we can't yield it.
|
||||
if 'END_HEADERS' in f.flags:
|
||||
f = self._headers_buffer[0]
|
||||
f.flags.add('END_HEADERS')
|
||||
f.data = b''.join(x.data for x in self._headers_buffer)
|
||||
self._headers_buffer = []
|
||||
else:
|
||||
f = None
|
||||
elif (isinstance(f, (HeadersFrame, PushPromiseFrame)) and
|
||||
'END_HEADERS' not in f.flags):
|
||||
# This is the start of a headers block! Save the frame off and then
|
||||
# act like we didn't receive one.
|
||||
self._headers_buffer.append(f)
|
||||
f = None
|
||||
|
||||
return f
|
||||
|
||||
# The methods below support the iterator protocol.
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
# First, check that we have enough data to successfully parse the
|
||||
# next frame header. If not, bail. Otherwise, parse it.
|
||||
if len(self.data) < 9:
|
||||
raise StopIteration()
|
||||
|
||||
try:
|
||||
f, length = Frame.parse_frame_header(self.data[:9])
|
||||
except (InvalidDataError, InvalidFrameError) as e: # pragma: no cover
|
||||
raise ProtocolError(
|
||||
"Received frame with invalid header: %s" % str(e)
|
||||
)
|
||||
|
||||
# Next, check that we have enough length to parse the frame body. If
|
||||
# not, bail, leaving the frame header data in the buffer for next time.
|
||||
if len(self.data) < length + 9:
|
||||
raise StopIteration()
|
||||
|
||||
# Confirm the frame has an appropriate length.
|
||||
self._validate_frame_length(length)
|
||||
|
||||
# Try to parse the frame body
|
||||
try:
|
||||
f.parse_body(memoryview(self.data[9:9+length]))
|
||||
except InvalidDataError:
|
||||
raise ProtocolError("Received frame with non-compliant data")
|
||||
except InvalidFrameError:
|
||||
raise FrameDataMissingError("Frame data missing or invalid")
|
||||
|
||||
# At this point, as we know we'll use or discard the entire frame, we
|
||||
# can update the data.
|
||||
self.data = self.data[9+length:]
|
||||
|
||||
# Pass the frame through the header buffer.
|
||||
f = self._update_header_buffer(f)
|
||||
|
||||
# If we got a frame we didn't understand or shouldn't yield, rather
|
||||
# than return None it'd be better if we just tried to get the next
|
||||
# frame in the sequence instead. Recurse back into ourselves to do
|
||||
# that. This is safe because the amount of work we have to do here is
|
||||
# strictly bounded by the length of the buffer.
|
||||
return f if f is not None else self.__next__()
|
||||
334
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/h2/src/h2/settings.py
vendored
Normal file
334
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/h2/src/h2/settings.py
vendored
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
h2/settings
|
||||
~~~~~~~~~~~
|
||||
|
||||
This module contains a HTTP/2 settings object. This object provides a simple
|
||||
API for manipulating HTTP/2 settings, keeping track of both the current active
|
||||
state of the settings and the unacknowledged future values of the settings.
|
||||
"""
|
||||
import collections
|
||||
from collections.abc import MutableMapping
|
||||
import enum
|
||||
|
||||
from hyperframe.frame import SettingsFrame
|
||||
|
||||
from h2.errors import ErrorCodes
|
||||
from h2.exceptions import InvalidSettingsValueError
|
||||
|
||||
|
||||
class SettingCodes(enum.IntEnum):
|
||||
"""
|
||||
All known HTTP/2 setting codes.
|
||||
|
||||
.. versionadded:: 2.6.0
|
||||
"""
|
||||
|
||||
#: Allows the sender to inform the remote endpoint of the maximum size of
|
||||
#: the header compression table used to decode header blocks, in octets.
|
||||
HEADER_TABLE_SIZE = SettingsFrame.HEADER_TABLE_SIZE
|
||||
|
||||
#: This setting can be used to disable server push. To disable server push
|
||||
#: on a client, set this to 0.
|
||||
ENABLE_PUSH = SettingsFrame.ENABLE_PUSH
|
||||
|
||||
#: Indicates the maximum number of concurrent streams that the sender will
|
||||
#: allow.
|
||||
MAX_CONCURRENT_STREAMS = SettingsFrame.MAX_CONCURRENT_STREAMS
|
||||
|
||||
#: Indicates the sender's initial window size (in octets) for stream-level
|
||||
#: flow control.
|
||||
INITIAL_WINDOW_SIZE = SettingsFrame.INITIAL_WINDOW_SIZE
|
||||
|
||||
#: Indicates the size of the largest frame payload that the sender is
|
||||
#: willing to receive, in octets.
|
||||
MAX_FRAME_SIZE = SettingsFrame.MAX_FRAME_SIZE
|
||||
|
||||
#: This advisory setting informs a peer of the maximum size of header list
|
||||
#: that the sender is prepared to accept, in octets. The value is based on
|
||||
#: the uncompressed size of header fields, including the length of the name
|
||||
#: and value in octets plus an overhead of 32 octets for each header field.
|
||||
MAX_HEADER_LIST_SIZE = SettingsFrame.MAX_HEADER_LIST_SIZE
|
||||
|
||||
#: This setting can be used to enable the connect protocol. To enable on a
|
||||
#: client set this to 1.
|
||||
ENABLE_CONNECT_PROTOCOL = SettingsFrame.ENABLE_CONNECT_PROTOCOL
|
||||
|
||||
|
||||
def _setting_code_from_int(code):
|
||||
"""
|
||||
Given an integer setting code, returns either one of :class:`SettingCodes
|
||||
<h2.settings.SettingCodes>` or, if not present in the known set of codes,
|
||||
returns the integer directly.
|
||||
"""
|
||||
try:
|
||||
return SettingCodes(code)
|
||||
except ValueError:
|
||||
return code
|
||||
|
||||
|
||||
class ChangedSetting:
|
||||
|
||||
def __init__(self, setting, original_value, new_value):
|
||||
#: The setting code given. Either one of :class:`SettingCodes
|
||||
#: <h2.settings.SettingCodes>` or ``int``
|
||||
#:
|
||||
#: .. versionchanged:: 2.6.0
|
||||
self.setting = setting
|
||||
|
||||
#: The original value before being changed.
|
||||
self.original_value = original_value
|
||||
|
||||
#: The new value after being changed.
|
||||
self.new_value = new_value
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
"ChangedSetting(setting=%s, original_value=%s, "
|
||||
"new_value=%s)"
|
||||
) % (
|
||||
self.setting,
|
||||
self.original_value,
|
||||
self.new_value
|
||||
)
|
||||
|
||||
|
||||
class Settings(MutableMapping):
|
||||
"""
|
||||
An object that encapsulates HTTP/2 settings state.
|
||||
|
||||
HTTP/2 Settings are a complex beast. Each party, remote and local, has its
|
||||
own settings and a view of the other party's settings. When a settings
|
||||
frame is emitted by a peer it cannot assume that the new settings values
|
||||
are in place until the remote peer acknowledges the setting. In principle,
|
||||
multiple settings changes can be "in flight" at the same time, all with
|
||||
different values.
|
||||
|
||||
This object encapsulates this mess. It provides a dict-like interface to
|
||||
settings, which return the *current* values of the settings in question.
|
||||
Additionally, it keeps track of the stack of proposed values: each time an
|
||||
acknowledgement is sent/received, it updates the current values with the
|
||||
stack of proposed values. On top of all that, it validates the values to
|
||||
make sure they're allowed, and raises :class:`InvalidSettingsValueError
|
||||
<h2.exceptions.InvalidSettingsValueError>` if they are not.
|
||||
|
||||
Finally, this object understands what the default values of the HTTP/2
|
||||
settings are, and sets those defaults appropriately.
|
||||
|
||||
.. versionchanged:: 2.2.0
|
||||
Added the ``initial_values`` parameter.
|
||||
|
||||
.. versionchanged:: 2.5.0
|
||||
Added the ``max_header_list_size`` property.
|
||||
|
||||
:param client: (optional) Whether these settings should be defaulted for a
|
||||
client implementation or a server implementation. Defaults to ``True``.
|
||||
:type client: ``bool``
|
||||
:param initial_values: (optional) Any initial values the user would like
|
||||
set, rather than RFC 7540's defaults.
|
||||
:type initial_vales: ``MutableMapping``
|
||||
"""
|
||||
def __init__(self, client=True, initial_values=None):
|
||||
# Backing object for the settings. This is a dictionary of
|
||||
# (setting: [list of values]), where the first value in the list is the
|
||||
# current value of the setting. Strictly this doesn't use lists but
|
||||
# instead uses collections.deque to avoid repeated memory allocations.
|
||||
#
|
||||
# This contains the default values for HTTP/2.
|
||||
self._settings = {
|
||||
SettingCodes.HEADER_TABLE_SIZE: collections.deque([4096]),
|
||||
SettingCodes.ENABLE_PUSH: collections.deque([int(client)]),
|
||||
SettingCodes.INITIAL_WINDOW_SIZE: collections.deque([65535]),
|
||||
SettingCodes.MAX_FRAME_SIZE: collections.deque([16384]),
|
||||
SettingCodes.ENABLE_CONNECT_PROTOCOL: collections.deque([0]),
|
||||
}
|
||||
if initial_values is not None:
|
||||
for key, value in initial_values.items():
|
||||
invalid = _validate_setting(key, value)
|
||||
if invalid:
|
||||
raise InvalidSettingsValueError(
|
||||
"Setting %d has invalid value %d" % (key, value),
|
||||
error_code=invalid
|
||||
)
|
||||
self._settings[key] = collections.deque([value])
|
||||
|
||||
def acknowledge(self):
|
||||
"""
|
||||
The settings have been acknowledged, either by the user (remote
|
||||
settings) or by the remote peer (local settings).
|
||||
|
||||
:returns: A dict of {setting: ChangedSetting} that were applied.
|
||||
"""
|
||||
changed_settings = {}
|
||||
|
||||
# If there is more than one setting in the list, we have a setting
|
||||
# value outstanding. Update them.
|
||||
for k, v in self._settings.items():
|
||||
if len(v) > 1:
|
||||
old_setting = v.popleft()
|
||||
new_setting = v[0]
|
||||
changed_settings[k] = ChangedSetting(
|
||||
k, old_setting, new_setting
|
||||
)
|
||||
|
||||
return changed_settings
|
||||
|
||||
# Provide easy-access to well known settings.
|
||||
@property
|
||||
def header_table_size(self):
|
||||
"""
|
||||
The current value of the :data:`HEADER_TABLE_SIZE
|
||||
<h2.settings.SettingCodes.HEADER_TABLE_SIZE>` setting.
|
||||
"""
|
||||
return self[SettingCodes.HEADER_TABLE_SIZE]
|
||||
|
||||
@header_table_size.setter
|
||||
def header_table_size(self, value):
|
||||
self[SettingCodes.HEADER_TABLE_SIZE] = value
|
||||
|
||||
@property
|
||||
def enable_push(self):
|
||||
"""
|
||||
The current value of the :data:`ENABLE_PUSH
|
||||
<h2.settings.SettingCodes.ENABLE_PUSH>` setting.
|
||||
"""
|
||||
return self[SettingCodes.ENABLE_PUSH]
|
||||
|
||||
@enable_push.setter
|
||||
def enable_push(self, value):
|
||||
self[SettingCodes.ENABLE_PUSH] = value
|
||||
|
||||
@property
|
||||
def initial_window_size(self):
|
||||
"""
|
||||
The current value of the :data:`INITIAL_WINDOW_SIZE
|
||||
<h2.settings.SettingCodes.INITIAL_WINDOW_SIZE>` setting.
|
||||
"""
|
||||
return self[SettingCodes.INITIAL_WINDOW_SIZE]
|
||||
|
||||
@initial_window_size.setter
|
||||
def initial_window_size(self, value):
|
||||
self[SettingCodes.INITIAL_WINDOW_SIZE] = value
|
||||
|
||||
@property
|
||||
def max_frame_size(self):
|
||||
"""
|
||||
The current value of the :data:`MAX_FRAME_SIZE
|
||||
<h2.settings.SettingCodes.MAX_FRAME_SIZE>` setting.
|
||||
"""
|
||||
return self[SettingCodes.MAX_FRAME_SIZE]
|
||||
|
||||
@max_frame_size.setter
|
||||
def max_frame_size(self, value):
|
||||
self[SettingCodes.MAX_FRAME_SIZE] = value
|
||||
|
||||
@property
|
||||
def max_concurrent_streams(self):
|
||||
"""
|
||||
The current value of the :data:`MAX_CONCURRENT_STREAMS
|
||||
<h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS>` setting.
|
||||
"""
|
||||
return self.get(SettingCodes.MAX_CONCURRENT_STREAMS, 2**32+1)
|
||||
|
||||
@max_concurrent_streams.setter
|
||||
def max_concurrent_streams(self, value):
|
||||
self[SettingCodes.MAX_CONCURRENT_STREAMS] = value
|
||||
|
||||
@property
|
||||
def max_header_list_size(self):
|
||||
"""
|
||||
The current value of the :data:`MAX_HEADER_LIST_SIZE
|
||||
<h2.settings.SettingCodes.MAX_HEADER_LIST_SIZE>` setting. If not set,
|
||||
returns ``None``, which means unlimited.
|
||||
|
||||
.. versionadded:: 2.5.0
|
||||
"""
|
||||
return self.get(SettingCodes.MAX_HEADER_LIST_SIZE, None)
|
||||
|
||||
@max_header_list_size.setter
|
||||
def max_header_list_size(self, value):
|
||||
self[SettingCodes.MAX_HEADER_LIST_SIZE] = value
|
||||
|
||||
@property
|
||||
def enable_connect_protocol(self):
|
||||
"""
|
||||
The current value of the :data:`ENABLE_CONNECT_PROTOCOL
|
||||
<h2.settings.SettingCodes.ENABLE_CONNECT_PROTOCOL>` setting.
|
||||
"""
|
||||
return self[SettingCodes.ENABLE_CONNECT_PROTOCOL]
|
||||
|
||||
@enable_connect_protocol.setter
|
||||
def enable_connect_protocol(self, value):
|
||||
self[SettingCodes.ENABLE_CONNECT_PROTOCOL] = value
|
||||
|
||||
# Implement the MutableMapping API.
|
||||
def __getitem__(self, key):
|
||||
val = self._settings[key][0]
|
||||
|
||||
# Things that were created when a setting was received should stay
|
||||
# KeyError'd.
|
||||
if val is None:
|
||||
raise KeyError
|
||||
|
||||
return val
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
invalid = _validate_setting(key, value)
|
||||
if invalid:
|
||||
raise InvalidSettingsValueError(
|
||||
"Setting %d has invalid value %d" % (key, value),
|
||||
error_code=invalid
|
||||
)
|
||||
|
||||
try:
|
||||
items = self._settings[key]
|
||||
except KeyError:
|
||||
items = collections.deque([None])
|
||||
self._settings[key] = items
|
||||
|
||||
items.append(value)
|
||||
|
||||
def __delitem__(self, key):
|
||||
del self._settings[key]
|
||||
|
||||
def __iter__(self):
|
||||
return self._settings.__iter__()
|
||||
|
||||
def __len__(self):
|
||||
return len(self._settings)
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, Settings):
|
||||
return self._settings == other._settings
|
||||
else:
|
||||
return NotImplemented
|
||||
|
||||
def __ne__(self, other):
|
||||
if isinstance(other, Settings):
|
||||
return not self == other
|
||||
else:
|
||||
return NotImplemented
|
||||
|
||||
|
||||
def _validate_setting(setting, value): # noqa: C901
|
||||
"""
|
||||
Confirms that a specific setting has a well-formed value. If the setting is
|
||||
invalid, returns an error code. Otherwise, returns 0 (NO_ERROR).
|
||||
"""
|
||||
if setting == SettingCodes.ENABLE_PUSH:
|
||||
if value not in (0, 1):
|
||||
return ErrorCodes.PROTOCOL_ERROR
|
||||
elif setting == SettingCodes.INITIAL_WINDOW_SIZE:
|
||||
if not 0 <= value <= 2147483647: # 2^31 - 1
|
||||
return ErrorCodes.FLOW_CONTROL_ERROR
|
||||
elif setting == SettingCodes.MAX_FRAME_SIZE:
|
||||
if not 16384 <= value <= 16777215: # 2^14 and 2^24 - 1
|
||||
return ErrorCodes.PROTOCOL_ERROR
|
||||
elif setting == SettingCodes.MAX_HEADER_LIST_SIZE:
|
||||
if value < 0:
|
||||
return ErrorCodes.PROTOCOL_ERROR
|
||||
elif setting == SettingCodes.ENABLE_CONNECT_PROTOCOL:
|
||||
if value not in (0, 1):
|
||||
return ErrorCodes.PROTOCOL_ERROR
|
||||
|
||||
return 0
|
||||
1371
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/h2/src/h2/stream.py
vendored
Normal file
1371
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/h2/src/h2/stream.py
vendored
Normal file
File diff suppressed because it is too large
Load diff
672
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/h2/src/h2/utilities.py
vendored
Normal file
672
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/h2/src/h2/utilities.py
vendored
Normal file
|
|
@ -0,0 +1,672 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
h2/utilities
|
||||
~~~~~~~~~~~~
|
||||
|
||||
Utility functions that do not belong in a separate module.
|
||||
"""
|
||||
import collections
|
||||
import re
|
||||
from string import whitespace
|
||||
|
||||
from hpack import HeaderTuple, NeverIndexedHeaderTuple
|
||||
|
||||
from .exceptions import ProtocolError, FlowControlError
|
||||
|
||||
UPPER_RE = re.compile(b"[A-Z]")
|
||||
|
||||
# A set of headers that are hop-by-hop or connection-specific and thus
|
||||
# forbidden in HTTP/2. This list comes from RFC 7540 § 8.1.2.2.
|
||||
CONNECTION_HEADERS = frozenset([
|
||||
b'connection', u'connection',
|
||||
b'proxy-connection', u'proxy-connection',
|
||||
b'keep-alive', u'keep-alive',
|
||||
b'transfer-encoding', u'transfer-encoding',
|
||||
b'upgrade', u'upgrade',
|
||||
])
|
||||
|
||||
|
||||
_ALLOWED_PSEUDO_HEADER_FIELDS = frozenset([
|
||||
b':method', u':method',
|
||||
b':scheme', u':scheme',
|
||||
b':authority', u':authority',
|
||||
b':path', u':path',
|
||||
b':status', u':status',
|
||||
b':protocol', u':protocol',
|
||||
])
|
||||
|
||||
|
||||
_SECURE_HEADERS = frozenset([
|
||||
# May have basic credentials which are vulnerable to dictionary attacks.
|
||||
b'authorization', u'authorization',
|
||||
b'proxy-authorization', u'proxy-authorization',
|
||||
])
|
||||
|
||||
|
||||
_REQUEST_ONLY_HEADERS = frozenset([
|
||||
b':scheme', u':scheme',
|
||||
b':path', u':path',
|
||||
b':authority', u':authority',
|
||||
b':method', u':method',
|
||||
b':protocol', u':protocol',
|
||||
])
|
||||
|
||||
|
||||
_RESPONSE_ONLY_HEADERS = frozenset([b':status', u':status'])
|
||||
|
||||
|
||||
# A Set of pseudo headers that are only valid if the method is
|
||||
# CONNECT, see RFC 8441 § 5
|
||||
_CONNECT_REQUEST_ONLY_HEADERS = frozenset([b':protocol', u':protocol'])
|
||||
|
||||
|
||||
_WHITESPACE = frozenset(map(ord, whitespace))
|
||||
|
||||
|
||||
def _secure_headers(headers, hdr_validation_flags):
|
||||
"""
|
||||
Certain headers are at risk of being attacked during the header compression
|
||||
phase, and so need to be kept out of header compression contexts. This
|
||||
function automatically transforms certain specific headers into HPACK
|
||||
never-indexed fields to ensure they don't get added to header compression
|
||||
contexts.
|
||||
|
||||
This function currently implements two rules:
|
||||
|
||||
- 'authorization' and 'proxy-authorization' fields are automatically made
|
||||
never-indexed.
|
||||
- Any 'cookie' header field shorter than 20 bytes long is made
|
||||
never-indexed.
|
||||
|
||||
These fields are the most at-risk. These rules are inspired by Firefox
|
||||
and nghttp2.
|
||||
"""
|
||||
for header in headers:
|
||||
if header[0] in _SECURE_HEADERS:
|
||||
yield NeverIndexedHeaderTuple(*header)
|
||||
elif header[0] in (b'cookie', u'cookie') and len(header[1]) < 20:
|
||||
yield NeverIndexedHeaderTuple(*header)
|
||||
else:
|
||||
yield header
|
||||
|
||||
|
||||
def extract_method_header(headers):
|
||||
"""
|
||||
Extracts the request method from the headers list.
|
||||
"""
|
||||
for k, v in headers:
|
||||
if k in (b':method', u':method'):
|
||||
if not isinstance(v, bytes):
|
||||
return v.encode('utf-8')
|
||||
else:
|
||||
return v
|
||||
|
||||
|
||||
def is_informational_response(headers):
|
||||
"""
|
||||
Searches a header block for a :status header to confirm that a given
|
||||
collection of headers are an informational response. Assumes the header
|
||||
block is well formed: that is, that the HTTP/2 special headers are first
|
||||
in the block, and so that it can stop looking when it finds the first
|
||||
header field whose name does not begin with a colon.
|
||||
|
||||
:param headers: The HTTP/2 header block.
|
||||
:returns: A boolean indicating if this is an informational response.
|
||||
"""
|
||||
for n, v in headers:
|
||||
if isinstance(n, bytes):
|
||||
sigil = b':'
|
||||
status = b':status'
|
||||
informational_start = b'1'
|
||||
else:
|
||||
sigil = u':'
|
||||
status = u':status'
|
||||
informational_start = u'1'
|
||||
|
||||
# If we find a non-special header, we're done here: stop looping.
|
||||
if not n.startswith(sigil):
|
||||
return False
|
||||
|
||||
# This isn't the status header, bail.
|
||||
if n != status:
|
||||
continue
|
||||
|
||||
# If the first digit is a 1, we've got informational headers.
|
||||
return v.startswith(informational_start)
|
||||
|
||||
|
||||
def guard_increment_window(current, increment):
|
||||
"""
|
||||
Increments a flow control window, guarding against that window becoming too
|
||||
large.
|
||||
|
||||
:param current: The current value of the flow control window.
|
||||
:param increment: The increment to apply to that window.
|
||||
:returns: The new value of the window.
|
||||
:raises: ``FlowControlError``
|
||||
"""
|
||||
# The largest value the flow control window may take.
|
||||
LARGEST_FLOW_CONTROL_WINDOW = 2**31 - 1
|
||||
|
||||
new_size = current + increment
|
||||
|
||||
if new_size > LARGEST_FLOW_CONTROL_WINDOW:
|
||||
raise FlowControlError(
|
||||
"May not increment flow control window past %d" %
|
||||
LARGEST_FLOW_CONTROL_WINDOW
|
||||
)
|
||||
|
||||
return new_size
|
||||
|
||||
|
||||
def authority_from_headers(headers):
|
||||
"""
|
||||
Given a header set, searches for the authority header and returns the
|
||||
value.
|
||||
|
||||
Note that this doesn't terminate early, so should only be called if the
|
||||
headers are for a client request. Otherwise, will loop over the entire
|
||||
header set, which is potentially unwise.
|
||||
|
||||
:param headers: The HTTP header set.
|
||||
:returns: The value of the authority header, or ``None``.
|
||||
:rtype: ``bytes`` or ``None``.
|
||||
"""
|
||||
for n, v in headers:
|
||||
# This gets run against headers that come both from HPACK and from the
|
||||
# user, so we may have unicode floating around in here. We only want
|
||||
# bytes.
|
||||
if n in (b':authority', u':authority'):
|
||||
return v.encode('utf-8') if not isinstance(v, bytes) else v
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# Flags used by the validate_headers pipeline to determine which checks
|
||||
# should be applied to a given set of headers.
|
||||
HeaderValidationFlags = collections.namedtuple(
|
||||
'HeaderValidationFlags',
|
||||
['is_client', 'is_trailer', 'is_response_header', 'is_push_promise']
|
||||
)
|
||||
|
||||
|
||||
def validate_headers(headers, hdr_validation_flags):
|
||||
"""
|
||||
Validates a header sequence against a set of constraints from RFC 7540.
|
||||
|
||||
:param headers: The HTTP header set.
|
||||
:param hdr_validation_flags: An instance of HeaderValidationFlags.
|
||||
"""
|
||||
# This validation logic is built on a sequence of generators that are
|
||||
# iterated over to provide the final header list. This reduces some of the
|
||||
# overhead of doing this checking. However, it's worth noting that this
|
||||
# checking remains somewhat expensive, and attempts should be made wherever
|
||||
# possible to reduce the time spent doing them.
|
||||
#
|
||||
# For example, we avoid tuple unpacking in loops because it represents a
|
||||
# fixed cost that we don't want to spend, instead indexing into the header
|
||||
# tuples.
|
||||
headers = _reject_empty_header_names(
|
||||
headers, hdr_validation_flags
|
||||
)
|
||||
headers = _reject_uppercase_header_fields(
|
||||
headers, hdr_validation_flags
|
||||
)
|
||||
headers = _reject_surrounding_whitespace(
|
||||
headers, hdr_validation_flags
|
||||
)
|
||||
headers = _reject_te(
|
||||
headers, hdr_validation_flags
|
||||
)
|
||||
headers = _reject_connection_header(
|
||||
headers, hdr_validation_flags
|
||||
)
|
||||
headers = _reject_pseudo_header_fields(
|
||||
headers, hdr_validation_flags
|
||||
)
|
||||
headers = _check_host_authority_header(
|
||||
headers, hdr_validation_flags
|
||||
)
|
||||
headers = _check_path_header(headers, hdr_validation_flags)
|
||||
|
||||
return headers
|
||||
|
||||
|
||||
def _reject_empty_header_names(headers, hdr_validation_flags):
|
||||
"""
|
||||
Raises a ProtocolError if any header names are empty (length 0).
|
||||
While hpack decodes such headers without errors, they are semantically
|
||||
forbidden in HTTP, see RFC 7230, stating that they must be at least one
|
||||
character long.
|
||||
"""
|
||||
for header in headers:
|
||||
if len(header[0]) == 0:
|
||||
raise ProtocolError("Received header name with zero length.")
|
||||
yield header
|
||||
|
||||
|
||||
def _reject_uppercase_header_fields(headers, hdr_validation_flags):
|
||||
"""
|
||||
Raises a ProtocolError if any uppercase character is found in a header
|
||||
block.
|
||||
"""
|
||||
for header in headers:
|
||||
if UPPER_RE.search(header[0]):
|
||||
raise ProtocolError(
|
||||
"Received uppercase header name %s." % header[0])
|
||||
yield header
|
||||
|
||||
|
||||
def _reject_surrounding_whitespace(headers, hdr_validation_flags):
|
||||
"""
|
||||
Raises a ProtocolError if any header name or value is surrounded by
|
||||
whitespace characters.
|
||||
"""
|
||||
# For compatibility with RFC 7230 header fields, we need to allow the field
|
||||
# value to be an empty string. This is ludicrous, but technically allowed.
|
||||
# The field name may not be empty, though, so we can safely assume that it
|
||||
# must have at least one character in it and throw exceptions if it
|
||||
# doesn't.
|
||||
for header in headers:
|
||||
if header[0][0] in _WHITESPACE or header[0][-1] in _WHITESPACE:
|
||||
raise ProtocolError(
|
||||
"Received header name surrounded by whitespace %r" % header[0])
|
||||
if header[1] and ((header[1][0] in _WHITESPACE) or
|
||||
(header[1][-1] in _WHITESPACE)):
|
||||
raise ProtocolError(
|
||||
"Received header value surrounded by whitespace %r" % header[1]
|
||||
)
|
||||
yield header
|
||||
|
||||
|
||||
def _reject_te(headers, hdr_validation_flags):
|
||||
"""
|
||||
Raises a ProtocolError if the TE header is present in a header block and
|
||||
its value is anything other than "trailers".
|
||||
"""
|
||||
for header in headers:
|
||||
if header[0] in (b'te', u'te'):
|
||||
if header[1].lower() not in (b'trailers', u'trailers'):
|
||||
raise ProtocolError(
|
||||
"Invalid value for TE header: %s" %
|
||||
header[1]
|
||||
)
|
||||
|
||||
yield header
|
||||
|
||||
|
||||
def _reject_connection_header(headers, hdr_validation_flags):
|
||||
"""
|
||||
Raises a ProtocolError if the Connection header is present in a header
|
||||
block.
|
||||
"""
|
||||
for header in headers:
|
||||
if header[0] in CONNECTION_HEADERS:
|
||||
raise ProtocolError(
|
||||
"Connection-specific header field present: %s." % header[0]
|
||||
)
|
||||
|
||||
yield header
|
||||
|
||||
|
||||
def _custom_startswith(test_string, bytes_prefix, unicode_prefix):
|
||||
"""
|
||||
Given a string that might be a bytestring or a Unicode string,
|
||||
return True if it starts with the appropriate prefix.
|
||||
"""
|
||||
if isinstance(test_string, bytes):
|
||||
return test_string.startswith(bytes_prefix)
|
||||
else:
|
||||
return test_string.startswith(unicode_prefix)
|
||||
|
||||
|
||||
def _assert_header_in_set(string_header, bytes_header, header_set):
|
||||
"""
|
||||
Given a set of header names, checks whether the string or byte version of
|
||||
the header name is present. Raises a Protocol error with the appropriate
|
||||
error if it's missing.
|
||||
"""
|
||||
if not (string_header in header_set or bytes_header in header_set):
|
||||
raise ProtocolError(
|
||||
"Header block missing mandatory %s header" % string_header
|
||||
)
|
||||
|
||||
|
||||
def _reject_pseudo_header_fields(headers, hdr_validation_flags):
|
||||
"""
|
||||
Raises a ProtocolError if duplicate pseudo-header fields are found in a
|
||||
header block or if a pseudo-header field appears in a block after an
|
||||
ordinary header field.
|
||||
|
||||
Raises a ProtocolError if pseudo-header fields are found in trailers.
|
||||
"""
|
||||
seen_pseudo_header_fields = set()
|
||||
seen_regular_header = False
|
||||
method = None
|
||||
|
||||
for header in headers:
|
||||
if _custom_startswith(header[0], b':', u':'):
|
||||
if header[0] in seen_pseudo_header_fields:
|
||||
raise ProtocolError(
|
||||
"Received duplicate pseudo-header field %s" % header[0]
|
||||
)
|
||||
|
||||
seen_pseudo_header_fields.add(header[0])
|
||||
|
||||
if seen_regular_header:
|
||||
raise ProtocolError(
|
||||
"Received pseudo-header field out of sequence: %s" %
|
||||
header[0]
|
||||
)
|
||||
|
||||
if header[0] not in _ALLOWED_PSEUDO_HEADER_FIELDS:
|
||||
raise ProtocolError(
|
||||
"Received custom pseudo-header field %s" % header[0]
|
||||
)
|
||||
|
||||
if header[0] in (b':method', u':method'):
|
||||
if not isinstance(header[1], bytes):
|
||||
method = header[1].encode('utf-8')
|
||||
else:
|
||||
method = header[1]
|
||||
|
||||
else:
|
||||
seen_regular_header = True
|
||||
|
||||
yield header
|
||||
|
||||
# Check the pseudo-headers we got to confirm they're acceptable.
|
||||
_check_pseudo_header_field_acceptability(
|
||||
seen_pseudo_header_fields, method, hdr_validation_flags
|
||||
)
|
||||
|
||||
|
||||
def _check_pseudo_header_field_acceptability(pseudo_headers,
|
||||
method,
|
||||
hdr_validation_flags):
|
||||
"""
|
||||
Given the set of pseudo-headers present in a header block and the
|
||||
validation flags, confirms that RFC 7540 allows them.
|
||||
"""
|
||||
# Pseudo-header fields MUST NOT appear in trailers - RFC 7540 § 8.1.2.1
|
||||
if hdr_validation_flags.is_trailer and pseudo_headers:
|
||||
raise ProtocolError(
|
||||
"Received pseudo-header in trailer %s" % pseudo_headers
|
||||
)
|
||||
|
||||
# If ':status' pseudo-header is not there in a response header, reject it.
|
||||
# Similarly, if ':path', ':method', or ':scheme' are not there in a request
|
||||
# header, reject it. Additionally, if a response contains any request-only
|
||||
# headers or vice-versa, reject it.
|
||||
# Relevant RFC section: RFC 7540 § 8.1.2.4
|
||||
# https://tools.ietf.org/html/rfc7540#section-8.1.2.4
|
||||
if hdr_validation_flags.is_response_header:
|
||||
_assert_header_in_set(u':status', b':status', pseudo_headers)
|
||||
invalid_response_headers = pseudo_headers & _REQUEST_ONLY_HEADERS
|
||||
if invalid_response_headers:
|
||||
raise ProtocolError(
|
||||
"Encountered request-only headers %s" %
|
||||
invalid_response_headers
|
||||
)
|
||||
elif (not hdr_validation_flags.is_response_header and
|
||||
not hdr_validation_flags.is_trailer):
|
||||
# This is a request, so we need to have seen :path, :method, and
|
||||
# :scheme.
|
||||
_assert_header_in_set(u':path', b':path', pseudo_headers)
|
||||
_assert_header_in_set(u':method', b':method', pseudo_headers)
|
||||
_assert_header_in_set(u':scheme', b':scheme', pseudo_headers)
|
||||
invalid_request_headers = pseudo_headers & _RESPONSE_ONLY_HEADERS
|
||||
if invalid_request_headers:
|
||||
raise ProtocolError(
|
||||
"Encountered response-only headers %s" %
|
||||
invalid_request_headers
|
||||
)
|
||||
if method != b'CONNECT':
|
||||
invalid_headers = pseudo_headers & _CONNECT_REQUEST_ONLY_HEADERS
|
||||
if invalid_headers:
|
||||
raise ProtocolError(
|
||||
"Encountered connect-request-only headers %s" %
|
||||
invalid_headers
|
||||
)
|
||||
|
||||
|
||||
def _validate_host_authority_header(headers):
|
||||
"""
|
||||
Given the :authority and Host headers from a request block that isn't
|
||||
a trailer, check that:
|
||||
1. At least one of these headers is set.
|
||||
2. If both headers are set, they match.
|
||||
|
||||
:param headers: The HTTP header set.
|
||||
:raises: ``ProtocolError``
|
||||
"""
|
||||
# We use None as a sentinel value. Iterate over the list of headers,
|
||||
# and record the value of these headers (if present). We don't need
|
||||
# to worry about receiving duplicate :authority headers, as this is
|
||||
# enforced by the _reject_pseudo_header_fields() pipeline.
|
||||
#
|
||||
# TODO: We should also guard against receiving duplicate Host headers,
|
||||
# and against sending duplicate headers.
|
||||
authority_header_val = None
|
||||
host_header_val = None
|
||||
|
||||
for header in headers:
|
||||
if header[0] in (b':authority', u':authority'):
|
||||
authority_header_val = header[1]
|
||||
elif header[0] in (b'host', u'host'):
|
||||
host_header_val = header[1]
|
||||
|
||||
yield header
|
||||
|
||||
# If we have not-None values for these variables, then we know we saw
|
||||
# the corresponding header.
|
||||
authority_present = (authority_header_val is not None)
|
||||
host_present = (host_header_val is not None)
|
||||
|
||||
# It is an error for a request header block to contain neither
|
||||
# an :authority header nor a Host header.
|
||||
if not authority_present and not host_present:
|
||||
raise ProtocolError(
|
||||
"Request header block does not have an :authority or Host header."
|
||||
)
|
||||
|
||||
# If we receive both headers, they should definitely match.
|
||||
if authority_present and host_present:
|
||||
if authority_header_val != host_header_val:
|
||||
raise ProtocolError(
|
||||
"Request header block has mismatched :authority and "
|
||||
"Host headers: %r / %r"
|
||||
% (authority_header_val, host_header_val)
|
||||
)
|
||||
|
||||
|
||||
def _check_host_authority_header(headers, hdr_validation_flags):
|
||||
"""
|
||||
Raises a ProtocolError if a header block arrives that does not contain an
|
||||
:authority or a Host header, or if a header block contains both fields,
|
||||
but their values do not match.
|
||||
"""
|
||||
# We only expect to see :authority and Host headers on request header
|
||||
# blocks that aren't trailers, so skip this validation if this is a
|
||||
# response header or we're looking at trailer blocks.
|
||||
skip_validation = (
|
||||
hdr_validation_flags.is_response_header or
|
||||
hdr_validation_flags.is_trailer
|
||||
)
|
||||
if skip_validation:
|
||||
return headers
|
||||
|
||||
return _validate_host_authority_header(headers)
|
||||
|
||||
|
||||
def _check_path_header(headers, hdr_validation_flags):
|
||||
"""
|
||||
Raise a ProtocolError if a header block arrives or is sent that contains an
|
||||
empty :path header.
|
||||
"""
|
||||
def inner():
|
||||
for header in headers:
|
||||
if header[0] in (b':path', u':path'):
|
||||
if not header[1]:
|
||||
raise ProtocolError("An empty :path header is forbidden")
|
||||
|
||||
yield header
|
||||
|
||||
# We only expect to see :authority and Host headers on request header
|
||||
# blocks that aren't trailers, so skip this validation if this is a
|
||||
# response header or we're looking at trailer blocks.
|
||||
skip_validation = (
|
||||
hdr_validation_flags.is_response_header or
|
||||
hdr_validation_flags.is_trailer
|
||||
)
|
||||
if skip_validation:
|
||||
return headers
|
||||
else:
|
||||
return inner()
|
||||
|
||||
|
||||
def _lowercase_header_names(headers, hdr_validation_flags):
|
||||
"""
|
||||
Given an iterable of header two-tuples, rebuilds that iterable with the
|
||||
header names lowercased. This generator produces tuples that preserve the
|
||||
original type of the header tuple for tuple and any ``HeaderTuple``.
|
||||
"""
|
||||
for header in headers:
|
||||
if isinstance(header, HeaderTuple):
|
||||
yield header.__class__(header[0].lower(), header[1])
|
||||
else:
|
||||
yield (header[0].lower(), header[1])
|
||||
|
||||
|
||||
def _strip_surrounding_whitespace(headers, hdr_validation_flags):
|
||||
"""
|
||||
Given an iterable of header two-tuples, strip both leading and trailing
|
||||
whitespace from both header names and header values. This generator
|
||||
produces tuples that preserve the original type of the header tuple for
|
||||
tuple and any ``HeaderTuple``.
|
||||
"""
|
||||
for header in headers:
|
||||
if isinstance(header, HeaderTuple):
|
||||
yield header.__class__(header[0].strip(), header[1].strip())
|
||||
else:
|
||||
yield (header[0].strip(), header[1].strip())
|
||||
|
||||
|
||||
def _strip_connection_headers(headers, hdr_validation_flags):
|
||||
"""
|
||||
Strip any connection headers as per RFC7540 § 8.1.2.2.
|
||||
"""
|
||||
for header in headers:
|
||||
if header[0] not in CONNECTION_HEADERS:
|
||||
yield header
|
||||
|
||||
|
||||
def _check_sent_host_authority_header(headers, hdr_validation_flags):
|
||||
"""
|
||||
Raises an InvalidHeaderBlockError if we try to send a header block
|
||||
that does not contain an :authority or a Host header, or if
|
||||
the header block contains both fields, but their values do not match.
|
||||
"""
|
||||
# We only expect to see :authority and Host headers on request header
|
||||
# blocks that aren't trailers, so skip this validation if this is a
|
||||
# response header or we're looking at trailer blocks.
|
||||
skip_validation = (
|
||||
hdr_validation_flags.is_response_header or
|
||||
hdr_validation_flags.is_trailer
|
||||
)
|
||||
if skip_validation:
|
||||
return headers
|
||||
|
||||
return _validate_host_authority_header(headers)
|
||||
|
||||
|
||||
def _combine_cookie_fields(headers, hdr_validation_flags):
|
||||
"""
|
||||
RFC 7540 § 8.1.2.5 allows HTTP/2 clients to split the Cookie header field,
|
||||
which must normally appear only once, into multiple fields for better
|
||||
compression. However, they MUST be joined back up again when received.
|
||||
This normalization step applies that transform. The side-effect is that
|
||||
all cookie fields now appear *last* in the header block.
|
||||
"""
|
||||
# There is a problem here about header indexing. Specifically, it's
|
||||
# possible that all these cookies are sent with different header indexing
|
||||
# values. At this point it shouldn't matter too much, so we apply our own
|
||||
# logic and make them never-indexed.
|
||||
cookies = []
|
||||
for header in headers:
|
||||
if header[0] == b'cookie':
|
||||
cookies.append(header[1])
|
||||
else:
|
||||
yield header
|
||||
if cookies:
|
||||
cookie_val = b'; '.join(cookies)
|
||||
yield NeverIndexedHeaderTuple(b'cookie', cookie_val)
|
||||
|
||||
|
||||
def normalize_outbound_headers(headers, hdr_validation_flags):
|
||||
"""
|
||||
Normalizes a header sequence that we are about to send.
|
||||
|
||||
:param headers: The HTTP header set.
|
||||
:param hdr_validation_flags: An instance of HeaderValidationFlags.
|
||||
"""
|
||||
headers = _lowercase_header_names(headers, hdr_validation_flags)
|
||||
headers = _strip_surrounding_whitespace(headers, hdr_validation_flags)
|
||||
headers = _strip_connection_headers(headers, hdr_validation_flags)
|
||||
headers = _secure_headers(headers, hdr_validation_flags)
|
||||
|
||||
return headers
|
||||
|
||||
|
||||
def normalize_inbound_headers(headers, hdr_validation_flags):
|
||||
"""
|
||||
Normalizes a header sequence that we have received.
|
||||
|
||||
:param headers: The HTTP header set.
|
||||
:param hdr_validation_flags: An instance of HeaderValidationFlags
|
||||
"""
|
||||
headers = _combine_cookie_fields(headers, hdr_validation_flags)
|
||||
return headers
|
||||
|
||||
|
||||
def validate_outbound_headers(headers, hdr_validation_flags):
|
||||
"""
|
||||
Validates and normalizes a header sequence that we are about to send.
|
||||
|
||||
:param headers: The HTTP header set.
|
||||
:param hdr_validation_flags: An instance of HeaderValidationFlags.
|
||||
"""
|
||||
headers = _reject_te(
|
||||
headers, hdr_validation_flags
|
||||
)
|
||||
headers = _reject_connection_header(
|
||||
headers, hdr_validation_flags
|
||||
)
|
||||
headers = _reject_pseudo_header_fields(
|
||||
headers, hdr_validation_flags
|
||||
)
|
||||
headers = _check_sent_host_authority_header(
|
||||
headers, hdr_validation_flags
|
||||
)
|
||||
headers = _check_path_header(headers, hdr_validation_flags)
|
||||
|
||||
return headers
|
||||
|
||||
|
||||
class SizeLimitDict(collections.OrderedDict):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._size_limit = kwargs.pop("size_limit", None)
|
||||
super(SizeLimitDict, self).__init__(*args, **kwargs)
|
||||
|
||||
self._check_size_limit()
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
super(SizeLimitDict, self).__setitem__(key, value)
|
||||
|
||||
self._check_size_limit()
|
||||
|
||||
def _check_size_limit(self):
|
||||
if self._size_limit is not None:
|
||||
while len(self) > self._size_limit:
|
||||
self.popitem(last=False)
|
||||
139
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/h2/src/h2/windows.py
vendored
Normal file
139
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/h2/src/h2/windows.py
vendored
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
h2/windows
|
||||
~~~~~~~~~~
|
||||
|
||||
Defines tools for managing HTTP/2 flow control windows.
|
||||
|
||||
The objects defined in this module are used to automatically manage HTTP/2
|
||||
flow control windows. Specifically, they keep track of what the size of the
|
||||
window is, how much data has been consumed from that window, and how much data
|
||||
the user has already used. It then implements a basic algorithm that attempts
|
||||
to manage the flow control window without user input, trying to ensure that it
|
||||
does not emit too many WINDOW_UPDATE frames.
|
||||
"""
|
||||
from __future__ import division
|
||||
|
||||
from .exceptions import FlowControlError
|
||||
|
||||
|
||||
# The largest acceptable value for a HTTP/2 flow control window.
|
||||
LARGEST_FLOW_CONTROL_WINDOW = 2**31 - 1
|
||||
|
||||
|
||||
class WindowManager:
|
||||
"""
|
||||
A basic HTTP/2 window manager.
|
||||
|
||||
:param max_window_size: The maximum size of the flow control window.
|
||||
:type max_window_size: ``int``
|
||||
"""
|
||||
def __init__(self, max_window_size):
|
||||
assert max_window_size <= LARGEST_FLOW_CONTROL_WINDOW
|
||||
self.max_window_size = max_window_size
|
||||
self.current_window_size = max_window_size
|
||||
self._bytes_processed = 0
|
||||
|
||||
def window_consumed(self, size):
|
||||
"""
|
||||
We have received a certain number of bytes from the remote peer. This
|
||||
necessarily shrinks the flow control window!
|
||||
|
||||
:param size: The number of flow controlled bytes we received from the
|
||||
remote peer.
|
||||
:type size: ``int``
|
||||
:returns: Nothing.
|
||||
:rtype: ``None``
|
||||
"""
|
||||
self.current_window_size -= size
|
||||
if self.current_window_size < 0:
|
||||
raise FlowControlError("Flow control window shrunk below 0")
|
||||
|
||||
def window_opened(self, size):
|
||||
"""
|
||||
The flow control window has been incremented, either because of manual
|
||||
flow control management or because of the user changing the flow
|
||||
control settings. This can have the effect of increasing what we
|
||||
consider to be the "maximum" flow control window size.
|
||||
|
||||
This does not increase our view of how many bytes have been processed,
|
||||
only of how much space is in the window.
|
||||
|
||||
:param size: The increment to the flow control window we received.
|
||||
:type size: ``int``
|
||||
:returns: Nothing
|
||||
:rtype: ``None``
|
||||
"""
|
||||
self.current_window_size += size
|
||||
|
||||
if self.current_window_size > LARGEST_FLOW_CONTROL_WINDOW:
|
||||
raise FlowControlError(
|
||||
"Flow control window mustn't exceed %d" %
|
||||
LARGEST_FLOW_CONTROL_WINDOW
|
||||
)
|
||||
|
||||
if self.current_window_size > self.max_window_size:
|
||||
self.max_window_size = self.current_window_size
|
||||
|
||||
def process_bytes(self, size):
|
||||
"""
|
||||
The application has informed us that it has processed a certain number
|
||||
of bytes. This may cause us to want to emit a window update frame. If
|
||||
we do want to emit a window update frame, this method will return the
|
||||
number of bytes that we should increment the window by.
|
||||
|
||||
:param size: The number of flow controlled bytes that the application
|
||||
has processed.
|
||||
:type size: ``int``
|
||||
:returns: The number of bytes to increment the flow control window by,
|
||||
or ``None``.
|
||||
:rtype: ``int`` or ``None``
|
||||
"""
|
||||
self._bytes_processed += size
|
||||
return self._maybe_update_window()
|
||||
|
||||
def _maybe_update_window(self):
|
||||
"""
|
||||
Run the algorithm.
|
||||
|
||||
Our current algorithm can be described like this.
|
||||
|
||||
1. If no bytes have been processed, we immediately return 0. There is
|
||||
no meaningful way for us to hand space in the window back to the
|
||||
remote peer, so let's not even try.
|
||||
2. If there is no space in the flow control window, and we have
|
||||
processed at least 1024 bytes (or 1/4 of the window, if the window
|
||||
is smaller), we will emit a window update frame. This is to avoid
|
||||
the risk of blocking a stream altogether.
|
||||
3. If there is space in the flow control window, and we have processed
|
||||
at least 1/2 of the window worth of bytes, we will emit a window
|
||||
update frame. This is to minimise the number of window update frames
|
||||
we have to emit.
|
||||
|
||||
In a healthy system with large flow control windows, this will
|
||||
irregularly emit WINDOW_UPDATE frames. This prevents us starving the
|
||||
connection by emitting eleventy bajillion WINDOW_UPDATE frames,
|
||||
especially in situations where the remote peer is sending a lot of very
|
||||
small DATA frames.
|
||||
"""
|
||||
# TODO: Can the window be smaller than 1024 bytes? If not, we can
|
||||
# streamline this algorithm.
|
||||
if not self._bytes_processed:
|
||||
return None
|
||||
|
||||
max_increment = (self.max_window_size - self.current_window_size)
|
||||
increment = 0
|
||||
|
||||
# Note that, even though we may increment less than _bytes_processed,
|
||||
# we still want to set it to zero whenever we emit an increment. This
|
||||
# is because we'll always increment up to the maximum we can.
|
||||
if (self.current_window_size == 0) and (
|
||||
self._bytes_processed > min(1024, self.max_window_size // 4)):
|
||||
increment = min(self._bytes_processed, max_increment)
|
||||
self._bytes_processed = 0
|
||||
elif self._bytes_processed >= (self.max_window_size // 2):
|
||||
increment = min(self._bytes_processed, max_increment)
|
||||
self._bytes_processed = 0
|
||||
|
||||
self.current_window_size += increment
|
||||
return increment
|
||||
30
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/hpack/src/hpack/__init__.py
vendored
Normal file
30
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/hpack/src/hpack/__init__.py
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
hpack
|
||||
~~~~~
|
||||
|
||||
HTTP/2 header encoding for Python.
|
||||
"""
|
||||
from .hpack import Encoder, Decoder
|
||||
from .struct import HeaderTuple, NeverIndexedHeaderTuple
|
||||
from .exceptions import (
|
||||
HPACKError,
|
||||
HPACKDecodingError,
|
||||
InvalidTableIndex,
|
||||
OversizedHeaderListError,
|
||||
InvalidTableSizeError
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'Encoder',
|
||||
'Decoder',
|
||||
'HeaderTuple',
|
||||
'NeverIndexedHeaderTuple',
|
||||
'HPACKError',
|
||||
'HPACKDecodingError',
|
||||
'InvalidTableIndex',
|
||||
'OversizedHeaderListError',
|
||||
'InvalidTableSizeError',
|
||||
]
|
||||
|
||||
__version__ = '4.0.0'
|
||||
49
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/hpack/src/hpack/exceptions.py
vendored
Normal file
49
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/hpack/src/hpack/exceptions.py
vendored
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
hyper/http20/exceptions
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This defines exceptions used in the HTTP/2 portion of hyper.
|
||||
"""
|
||||
|
||||
|
||||
class HPACKError(Exception):
|
||||
"""
|
||||
The base class for all ``hpack`` exceptions.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class HPACKDecodingError(HPACKError):
|
||||
"""
|
||||
An error has been encountered while performing HPACK decoding.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class InvalidTableIndex(HPACKDecodingError):
|
||||
"""
|
||||
An invalid table index was received.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class OversizedHeaderListError(HPACKDecodingError):
|
||||
"""
|
||||
A header list that was larger than we allow has been received. This may be
|
||||
a DoS attack.
|
||||
|
||||
.. versionadded:: 2.3.0
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class InvalidTableSizeError(HPACKDecodingError):
|
||||
"""
|
||||
An attempt was made to change the decoder table size to a value larger than
|
||||
allowed, or the list was shrunk and the remote peer didn't shrink their
|
||||
table size.
|
||||
|
||||
.. versionadded:: 3.0.0
|
||||
"""
|
||||
pass
|
||||
633
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/hpack/src/hpack/hpack.py
vendored
Normal file
633
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/hpack/src/hpack/hpack.py
vendored
Normal file
|
|
@ -0,0 +1,633 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
hpack/hpack
|
||||
~~~~~~~~~~~
|
||||
|
||||
Implements the HPACK header compression algorithm as detailed by the IETF.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from .table import HeaderTable, table_entry_size
|
||||
from .exceptions import (
|
||||
HPACKDecodingError, OversizedHeaderListError, InvalidTableSizeError
|
||||
)
|
||||
from .huffman import HuffmanEncoder
|
||||
from .huffman_constants import (
|
||||
REQUEST_CODES, REQUEST_CODES_LENGTH
|
||||
)
|
||||
from .huffman_table import decode_huffman
|
||||
from .struct import HeaderTuple, NeverIndexedHeaderTuple
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
INDEX_NONE = b'\x00'
|
||||
INDEX_NEVER = b'\x10'
|
||||
INDEX_INCREMENTAL = b'\x40'
|
||||
|
||||
# Precompute 2^i for 1-8 for use in prefix calcs.
|
||||
# Zero index is not used but there to save a subtraction
|
||||
# as prefix numbers are not zero indexed.
|
||||
_PREFIX_BIT_MAX_NUMBERS = [(2 ** i) - 1 for i in range(9)]
|
||||
|
||||
try: # pragma: no cover
|
||||
basestring = basestring
|
||||
except NameError: # pragma: no cover
|
||||
basestring = (str, bytes)
|
||||
|
||||
|
||||
# We default the maximum header list we're willing to accept to 64kB. That's a
|
||||
# lot of headers, but if applications want to raise it they can do.
|
||||
DEFAULT_MAX_HEADER_LIST_SIZE = 2 ** 16
|
||||
|
||||
|
||||
def _unicode_if_needed(header, raw):
|
||||
"""
|
||||
Provides a header as a unicode string if raw is False, otherwise returns
|
||||
it as a bytestring.
|
||||
"""
|
||||
name = bytes(header[0])
|
||||
value = bytes(header[1])
|
||||
if not raw:
|
||||
name = name.decode('utf-8')
|
||||
value = value.decode('utf-8')
|
||||
return header.__class__(name, value)
|
||||
|
||||
|
||||
def encode_integer(integer, prefix_bits):
|
||||
"""
|
||||
This encodes an integer according to the wacky integer encoding rules
|
||||
defined in the HPACK spec.
|
||||
"""
|
||||
log.debug("Encoding %d with %d bits", integer, prefix_bits)
|
||||
|
||||
if integer < 0:
|
||||
raise ValueError(
|
||||
"Can only encode positive integers, got %s" % integer
|
||||
)
|
||||
|
||||
if prefix_bits < 1 or prefix_bits > 8:
|
||||
raise ValueError(
|
||||
"Prefix bits must be between 1 and 8, got %s" % prefix_bits
|
||||
)
|
||||
|
||||
max_number = _PREFIX_BIT_MAX_NUMBERS[prefix_bits]
|
||||
|
||||
if integer < max_number:
|
||||
return bytearray([integer]) # Seriously?
|
||||
else:
|
||||
elements = [max_number]
|
||||
integer -= max_number
|
||||
|
||||
while integer >= 128:
|
||||
elements.append((integer & 127) + 128)
|
||||
integer >>= 7
|
||||
|
||||
elements.append(integer)
|
||||
|
||||
return bytearray(elements)
|
||||
|
||||
|
||||
def decode_integer(data, prefix_bits):
|
||||
"""
|
||||
This decodes an integer according to the wacky integer encoding rules
|
||||
defined in the HPACK spec. Returns a tuple of the decoded integer and the
|
||||
number of bytes that were consumed from ``data`` in order to get that
|
||||
integer.
|
||||
"""
|
||||
if prefix_bits < 1 or prefix_bits > 8:
|
||||
raise ValueError(
|
||||
"Prefix bits must be between 1 and 8, got %s" % prefix_bits
|
||||
)
|
||||
|
||||
max_number = _PREFIX_BIT_MAX_NUMBERS[prefix_bits]
|
||||
index = 1
|
||||
shift = 0
|
||||
mask = (0xFF >> (8 - prefix_bits))
|
||||
|
||||
try:
|
||||
number = data[0] & mask
|
||||
if number == max_number:
|
||||
while True:
|
||||
next_byte = data[index]
|
||||
index += 1
|
||||
|
||||
if next_byte >= 128:
|
||||
number += (next_byte - 128) << shift
|
||||
else:
|
||||
number += next_byte << shift
|
||||
break
|
||||
shift += 7
|
||||
|
||||
except IndexError:
|
||||
raise HPACKDecodingError(
|
||||
"Unable to decode HPACK integer representation from %r" % data
|
||||
)
|
||||
|
||||
log.debug("Decoded %d, consumed %d bytes", number, index)
|
||||
|
||||
return number, index
|
||||
|
||||
|
||||
def _dict_to_iterable(header_dict):
|
||||
"""
|
||||
This converts a dictionary to an iterable of two-tuples. This is a
|
||||
HPACK-specific function because it pulls "special-headers" out first and
|
||||
then emits them.
|
||||
"""
|
||||
assert isinstance(header_dict, dict)
|
||||
keys = sorted(
|
||||
header_dict.keys(),
|
||||
key=lambda k: not _to_bytes(k).startswith(b':')
|
||||
)
|
||||
for key in keys:
|
||||
yield key, header_dict[key]
|
||||
|
||||
|
||||
def _to_bytes(string):
|
||||
"""
|
||||
Convert string to bytes.
|
||||
"""
|
||||
if not isinstance(string, basestring): # pragma: no cover
|
||||
string = str(string)
|
||||
|
||||
return string if isinstance(string, bytes) else string.encode('utf-8')
|
||||
|
||||
|
||||
class Encoder:
|
||||
"""
|
||||
An HPACK encoder object. This object takes HTTP headers and emits encoded
|
||||
HTTP/2 header blocks.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.header_table = HeaderTable()
|
||||
self.huffman_coder = HuffmanEncoder(
|
||||
REQUEST_CODES, REQUEST_CODES_LENGTH
|
||||
)
|
||||
self.table_size_changes = []
|
||||
|
||||
@property
|
||||
def header_table_size(self):
|
||||
"""
|
||||
Controls the size of the HPACK header table.
|
||||
"""
|
||||
return self.header_table.maxsize
|
||||
|
||||
@header_table_size.setter
|
||||
def header_table_size(self, value):
|
||||
self.header_table.maxsize = value
|
||||
if self.header_table.resized:
|
||||
self.table_size_changes.append(value)
|
||||
|
||||
def encode(self, headers, huffman=True):
|
||||
"""
|
||||
Takes a set of headers and encodes them into a HPACK-encoded header
|
||||
block.
|
||||
|
||||
:param headers: The headers to encode. Must be either an iterable of
|
||||
tuples, an iterable of :class:`HeaderTuple
|
||||
<hpack.HeaderTuple>`, or a ``dict``.
|
||||
|
||||
If an iterable of tuples, the tuples may be either
|
||||
two-tuples or three-tuples. If they are two-tuples, the
|
||||
tuples must be of the format ``(name, value)``. If they
|
||||
are three-tuples, they must be of the format
|
||||
``(name, value, sensitive)``, where ``sensitive`` is a
|
||||
boolean value indicating whether the header should be
|
||||
added to header tables anywhere. If not present,
|
||||
``sensitive`` defaults to ``False``.
|
||||
|
||||
If an iterable of :class:`HeaderTuple
|
||||
<hpack.HeaderTuple>`, the tuples must always be
|
||||
two-tuples. Instead of using ``sensitive`` as a third
|
||||
tuple entry, use :class:`NeverIndexedHeaderTuple
|
||||
<hpack.NeverIndexedHeaderTuple>` to request that
|
||||
the field never be indexed.
|
||||
|
||||
.. warning:: HTTP/2 requires that all special headers
|
||||
(headers whose names begin with ``:`` characters)
|
||||
appear at the *start* of the header block. While
|
||||
this method will ensure that happens for ``dict``
|
||||
subclasses, callers using any other iterable of
|
||||
tuples **must** ensure they place their special
|
||||
headers at the start of the iterable.
|
||||
|
||||
For efficiency reasons users should prefer to use
|
||||
iterables of two-tuples: fixing the ordering of
|
||||
dictionary headers is an expensive operation that
|
||||
should be avoided if possible.
|
||||
|
||||
:param huffman: (optional) Whether to Huffman-encode any header sent as
|
||||
a literal value. Except for use when debugging, it is
|
||||
recommended that this be left enabled.
|
||||
|
||||
:returns: A bytestring containing the HPACK-encoded header block.
|
||||
"""
|
||||
# Transforming the headers into a header block is a procedure that can
|
||||
# be modeled as a chain or pipe. First, the headers are encoded. This
|
||||
# encoding can be done a number of ways. If the header name-value pair
|
||||
# are already in the header table we can represent them using the
|
||||
# indexed representation: the same is true if they are in the static
|
||||
# table. Otherwise, a literal representation will be used.
|
||||
header_block = []
|
||||
|
||||
# Turn the headers into a list of tuples if possible. This is the
|
||||
# natural way to interact with them in HPACK. Because dictionaries are
|
||||
# un-ordered, we need to make sure we grab the "special" headers first.
|
||||
if isinstance(headers, dict):
|
||||
headers = _dict_to_iterable(headers)
|
||||
|
||||
# Before we begin, if the header table size has been changed we need
|
||||
# to signal all changes since last emission appropriately.
|
||||
if self.header_table.resized:
|
||||
header_block.append(self._encode_table_size_change())
|
||||
self.header_table.resized = False
|
||||
|
||||
# Add each header to the header block
|
||||
for header in headers:
|
||||
sensitive = False
|
||||
if isinstance(header, HeaderTuple):
|
||||
sensitive = not header.indexable
|
||||
elif len(header) > 2:
|
||||
sensitive = header[2]
|
||||
|
||||
header = (_to_bytes(header[0]), _to_bytes(header[1]))
|
||||
header_block.append(self.add(header, sensitive, huffman))
|
||||
|
||||
header_block = b''.join(header_block)
|
||||
|
||||
log.debug("Encoded header block to %s", header_block)
|
||||
|
||||
return header_block
|
||||
|
||||
def add(self, to_add, sensitive, huffman=False):
|
||||
"""
|
||||
This function takes a header key-value tuple and serializes it.
|
||||
"""
|
||||
log.debug(
|
||||
"Adding %s to the header table, sensitive:%s, huffman:%s",
|
||||
to_add,
|
||||
sensitive,
|
||||
huffman
|
||||
)
|
||||
|
||||
name, value = to_add
|
||||
|
||||
# Set our indexing mode
|
||||
indexbit = INDEX_INCREMENTAL if not sensitive else INDEX_NEVER
|
||||
|
||||
# Search for a matching header in the header table.
|
||||
match = self.header_table.search(name, value)
|
||||
|
||||
if match is None:
|
||||
# Not in the header table. Encode using the literal syntax,
|
||||
# and add it to the header table.
|
||||
encoded = self._encode_literal(name, value, indexbit, huffman)
|
||||
if not sensitive:
|
||||
self.header_table.add(name, value)
|
||||
return encoded
|
||||
|
||||
# The header is in the table, break out the values. If we matched
|
||||
# perfectly, we can use the indexed representation: otherwise we
|
||||
# can use the indexed literal.
|
||||
index, name, perfect = match
|
||||
|
||||
if perfect:
|
||||
# Indexed representation.
|
||||
encoded = self._encode_indexed(index)
|
||||
else:
|
||||
# Indexed literal. We are going to add header to the
|
||||
# header table unconditionally. It is a future todo to
|
||||
# filter out headers which are known to be ineffective for
|
||||
# indexing since they just take space in the table and
|
||||
# pushed out other valuable headers.
|
||||
encoded = self._encode_indexed_literal(
|
||||
index, value, indexbit, huffman
|
||||
)
|
||||
if not sensitive:
|
||||
self.header_table.add(name, value)
|
||||
|
||||
return encoded
|
||||
|
||||
def _encode_indexed(self, index):
|
||||
"""
|
||||
Encodes a header using the indexed representation.
|
||||
"""
|
||||
field = encode_integer(index, 7)
|
||||
field[0] |= 0x80 # we set the top bit
|
||||
return bytes(field)
|
||||
|
||||
def _encode_literal(self, name, value, indexbit, huffman=False):
|
||||
"""
|
||||
Encodes a header with a literal name and literal value. If ``indexing``
|
||||
is True, the header will be added to the header table: otherwise it
|
||||
will not.
|
||||
"""
|
||||
if huffman:
|
||||
name = self.huffman_coder.encode(name)
|
||||
value = self.huffman_coder.encode(value)
|
||||
|
||||
name_len = encode_integer(len(name), 7)
|
||||
value_len = encode_integer(len(value), 7)
|
||||
|
||||
if huffman:
|
||||
name_len[0] |= 0x80
|
||||
value_len[0] |= 0x80
|
||||
|
||||
return b''.join(
|
||||
[indexbit, bytes(name_len), name, bytes(value_len), value]
|
||||
)
|
||||
|
||||
def _encode_indexed_literal(self, index, value, indexbit, huffman=False):
|
||||
"""
|
||||
Encodes a header with an indexed name and a literal value and performs
|
||||
incremental indexing.
|
||||
"""
|
||||
if indexbit != INDEX_INCREMENTAL:
|
||||
prefix = encode_integer(index, 4)
|
||||
else:
|
||||
prefix = encode_integer(index, 6)
|
||||
|
||||
prefix[0] |= ord(indexbit)
|
||||
|
||||
if huffman:
|
||||
value = self.huffman_coder.encode(value)
|
||||
|
||||
value_len = encode_integer(len(value), 7)
|
||||
|
||||
if huffman:
|
||||
value_len[0] |= 0x80
|
||||
|
||||
return b''.join([bytes(prefix), bytes(value_len), value])
|
||||
|
||||
def _encode_table_size_change(self):
|
||||
"""
|
||||
Produces the encoded form of all header table size change context
|
||||
updates.
|
||||
"""
|
||||
block = b''
|
||||
for size_bytes in self.table_size_changes:
|
||||
size_bytes = encode_integer(size_bytes, 5)
|
||||
size_bytes[0] |= 0x20
|
||||
block += bytes(size_bytes)
|
||||
self.table_size_changes = []
|
||||
return block
|
||||
|
||||
|
||||
class Decoder:
|
||||
"""
|
||||
An HPACK decoder object.
|
||||
|
||||
.. versionchanged:: 2.3.0
|
||||
Added ``max_header_list_size`` argument.
|
||||
|
||||
:param max_header_list_size: The maximum decompressed size we will allow
|
||||
for any single header block. This is a protection against DoS attacks
|
||||
that attempt to force the application to expand a relatively small
|
||||
amount of data into a really large header list, allowing enormous
|
||||
amounts of memory to be allocated.
|
||||
|
||||
If this amount of data is exceeded, a `OversizedHeaderListError
|
||||
<hpack.OversizedHeaderListError>` exception will be raised. At this
|
||||
point the connection should be shut down, as the HPACK state will no
|
||||
longer be usable.
|
||||
|
||||
Defaults to 64kB.
|
||||
:type max_header_list_size: ``int``
|
||||
"""
|
||||
def __init__(self, max_header_list_size=DEFAULT_MAX_HEADER_LIST_SIZE):
|
||||
self.header_table = HeaderTable()
|
||||
|
||||
#: The maximum decompressed size we will allow for any single header
|
||||
#: block. This is a protection against DoS attacks that attempt to
|
||||
#: force the application to expand a relatively small amount of data
|
||||
#: into a really large header list, allowing enormous amounts of memory
|
||||
#: to be allocated.
|
||||
#:
|
||||
#: If this amount of data is exceeded, a `OversizedHeaderListError
|
||||
#: <hpack.OversizedHeaderListError>` exception will be raised. At this
|
||||
#: point the connection should be shut down, as the HPACK state will no
|
||||
#: longer be usable.
|
||||
#:
|
||||
#: Defaults to 64kB.
|
||||
#:
|
||||
#: .. versionadded:: 2.3.0
|
||||
self.max_header_list_size = max_header_list_size
|
||||
|
||||
#: Maximum allowed header table size.
|
||||
#:
|
||||
#: A HTTP/2 implementation should set this to the most recent value of
|
||||
#: SETTINGS_HEADER_TABLE_SIZE that it sent *and has received an ACK
|
||||
#: for*. Once this setting is set, the actual header table size will be
|
||||
#: checked at the end of each decoding run and whenever it is changed,
|
||||
#: to confirm that it fits in this size.
|
||||
self.max_allowed_table_size = self.header_table.maxsize
|
||||
|
||||
@property
|
||||
def header_table_size(self):
|
||||
"""
|
||||
Controls the size of the HPACK header table.
|
||||
"""
|
||||
return self.header_table.maxsize
|
||||
|
||||
@header_table_size.setter
|
||||
def header_table_size(self, value):
|
||||
self.header_table.maxsize = value
|
||||
|
||||
def decode(self, data, raw=False):
|
||||
"""
|
||||
Takes an HPACK-encoded header block and decodes it into a header set.
|
||||
|
||||
:param data: A bytestring representing a complete HPACK-encoded header
|
||||
block.
|
||||
:param raw: (optional) Whether to return the headers as tuples of raw
|
||||
byte strings or to decode them as UTF-8 before returning
|
||||
them. The default value is False, which returns tuples of
|
||||
Unicode strings
|
||||
:returns: A list of two-tuples of ``(name, value)`` representing the
|
||||
HPACK-encoded headers, in the order they were decoded.
|
||||
:raises HPACKDecodingError: If an error is encountered while decoding
|
||||
the header block.
|
||||
"""
|
||||
log.debug("Decoding %s", data)
|
||||
|
||||
data_mem = memoryview(data)
|
||||
headers = []
|
||||
data_len = len(data)
|
||||
inflated_size = 0
|
||||
current_index = 0
|
||||
|
||||
while current_index < data_len:
|
||||
# Work out what kind of header we're decoding.
|
||||
# If the high bit is 1, it's an indexed field.
|
||||
current = data[current_index]
|
||||
indexed = True if current & 0x80 else False
|
||||
|
||||
# Otherwise, if the second-highest bit is 1 it's a field that does
|
||||
# alter the header table.
|
||||
literal_index = True if current & 0x40 else False
|
||||
|
||||
# Otherwise, if the third-highest bit is 1 it's an encoding context
|
||||
# update.
|
||||
encoding_update = True if current & 0x20 else False
|
||||
|
||||
if indexed:
|
||||
header, consumed = self._decode_indexed(
|
||||
data_mem[current_index:]
|
||||
)
|
||||
elif literal_index:
|
||||
# It's a literal header that does affect the header table.
|
||||
header, consumed = self._decode_literal_index(
|
||||
data_mem[current_index:]
|
||||
)
|
||||
elif encoding_update:
|
||||
# It's an update to the encoding context. These are forbidden
|
||||
# in a header block after any actual header.
|
||||
if headers:
|
||||
raise HPACKDecodingError(
|
||||
"Table size update not at the start of the block"
|
||||
)
|
||||
consumed = self._update_encoding_context(
|
||||
data_mem[current_index:]
|
||||
)
|
||||
header = None
|
||||
else:
|
||||
# It's a literal header that does not affect the header table.
|
||||
header, consumed = self._decode_literal_no_index(
|
||||
data_mem[current_index:]
|
||||
)
|
||||
|
||||
if header:
|
||||
headers.append(header)
|
||||
inflated_size += table_entry_size(*header)
|
||||
|
||||
if inflated_size > self.max_header_list_size:
|
||||
raise OversizedHeaderListError(
|
||||
"A header list larger than %d has been received" %
|
||||
self.max_header_list_size
|
||||
)
|
||||
|
||||
current_index += consumed
|
||||
|
||||
# Confirm that the table size is lower than the maximum. We do this
|
||||
# here to ensure that we catch when the max has been *shrunk* and the
|
||||
# remote peer hasn't actually done that.
|
||||
self._assert_valid_table_size()
|
||||
|
||||
try:
|
||||
return [_unicode_if_needed(h, raw) for h in headers]
|
||||
except UnicodeDecodeError:
|
||||
raise HPACKDecodingError("Unable to decode headers as UTF-8.")
|
||||
|
||||
def _assert_valid_table_size(self):
|
||||
"""
|
||||
Check that the table size set by the encoder is lower than the maximum
|
||||
we expect to have.
|
||||
"""
|
||||
if self.header_table_size > self.max_allowed_table_size:
|
||||
raise InvalidTableSizeError(
|
||||
"Encoder did not shrink table size to within the max"
|
||||
)
|
||||
|
||||
def _update_encoding_context(self, data):
|
||||
"""
|
||||
Handles a byte that updates the encoding context.
|
||||
"""
|
||||
# We've been asked to resize the header table.
|
||||
new_size, consumed = decode_integer(data, 5)
|
||||
if new_size > self.max_allowed_table_size:
|
||||
raise InvalidTableSizeError(
|
||||
"Encoder exceeded max allowable table size"
|
||||
)
|
||||
self.header_table_size = new_size
|
||||
return consumed
|
||||
|
||||
def _decode_indexed(self, data):
|
||||
"""
|
||||
Decodes a header represented using the indexed representation.
|
||||
"""
|
||||
index, consumed = decode_integer(data, 7)
|
||||
header = HeaderTuple(*self.header_table.get_by_index(index))
|
||||
log.debug("Decoded %s, consumed %d", header, consumed)
|
||||
return header, consumed
|
||||
|
||||
def _decode_literal_no_index(self, data):
|
||||
return self._decode_literal(data, False)
|
||||
|
||||
def _decode_literal_index(self, data):
|
||||
return self._decode_literal(data, True)
|
||||
|
||||
def _decode_literal(self, data, should_index):
|
||||
"""
|
||||
Decodes a header represented with a literal.
|
||||
"""
|
||||
total_consumed = 0
|
||||
|
||||
# When should_index is true, if the low six bits of the first byte are
|
||||
# nonzero, the header name is indexed.
|
||||
# When should_index is false, if the low four bits of the first byte
|
||||
# are nonzero the header name is indexed.
|
||||
if should_index:
|
||||
indexed_name = data[0] & 0x3F
|
||||
name_len = 6
|
||||
not_indexable = False
|
||||
else:
|
||||
high_byte = data[0]
|
||||
indexed_name = high_byte & 0x0F
|
||||
name_len = 4
|
||||
not_indexable = high_byte & 0x10
|
||||
|
||||
if indexed_name:
|
||||
# Indexed header name.
|
||||
index, consumed = decode_integer(data, name_len)
|
||||
name = self.header_table.get_by_index(index)[0]
|
||||
|
||||
total_consumed = consumed
|
||||
length = 0
|
||||
else:
|
||||
# Literal header name. The first byte was consumed, so we need to
|
||||
# move forward.
|
||||
data = data[1:]
|
||||
|
||||
length, consumed = decode_integer(data, 7)
|
||||
name = data[consumed:consumed + length]
|
||||
if len(name) != length:
|
||||
raise HPACKDecodingError("Truncated header block")
|
||||
|
||||
if data[0] & 0x80:
|
||||
name = decode_huffman(name)
|
||||
total_consumed = consumed + length + 1 # Since we moved forward 1.
|
||||
|
||||
data = data[consumed + length:]
|
||||
|
||||
# The header value is definitely length-based.
|
||||
length, consumed = decode_integer(data, 7)
|
||||
value = data[consumed:consumed + length]
|
||||
if len(value) != length:
|
||||
raise HPACKDecodingError("Truncated header block")
|
||||
|
||||
if data[0] & 0x80:
|
||||
value = decode_huffman(value)
|
||||
|
||||
# Updated the total consumed length.
|
||||
total_consumed += length + consumed
|
||||
|
||||
# If we have been told never to index the header field, encode that in
|
||||
# the tuple we use.
|
||||
if not_indexable:
|
||||
header = NeverIndexedHeaderTuple(name, value)
|
||||
else:
|
||||
header = HeaderTuple(name, value)
|
||||
|
||||
# If we've been asked to index this, add it to the header table.
|
||||
if should_index:
|
||||
self.header_table.add(name, value)
|
||||
|
||||
log.debug(
|
||||
"Decoded %s, total consumed %d bytes, indexed %s",
|
||||
header,
|
||||
total_consumed,
|
||||
should_index
|
||||
)
|
||||
|
||||
return header, total_consumed
|
||||
66
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/hpack/src/hpack/huffman.py
vendored
Normal file
66
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/hpack/src/hpack/huffman.py
vendored
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
hpack/huffman_decoder
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
An implementation of a bitwise prefix tree specially built for decoding
|
||||
Huffman-coded content where we already know the Huffman table.
|
||||
"""
|
||||
|
||||
|
||||
class HuffmanEncoder:
|
||||
"""
|
||||
Encodes a string according to the Huffman encoding table defined in the
|
||||
HPACK specification.
|
||||
"""
|
||||
def __init__(self, huffman_code_list, huffman_code_list_lengths):
|
||||
self.huffman_code_list = huffman_code_list
|
||||
self.huffman_code_list_lengths = huffman_code_list_lengths
|
||||
|
||||
def encode(self, bytes_to_encode):
|
||||
"""
|
||||
Given a string of bytes, encodes them according to the HPACK Huffman
|
||||
specification.
|
||||
"""
|
||||
# If handed the empty string, just immediately return.
|
||||
if not bytes_to_encode:
|
||||
return b''
|
||||
|
||||
final_num = 0
|
||||
final_int_len = 0
|
||||
|
||||
# Turn each byte into its huffman code. These codes aren't necessarily
|
||||
# octet aligned, so keep track of how far through an octet we are. To
|
||||
# handle this cleanly, just use a single giant integer.
|
||||
for byte in bytes_to_encode:
|
||||
bin_int_len = self.huffman_code_list_lengths[byte]
|
||||
bin_int = self.huffman_code_list[byte] & (
|
||||
2 ** (bin_int_len + 1) - 1
|
||||
)
|
||||
final_num <<= bin_int_len
|
||||
final_num |= bin_int
|
||||
final_int_len += bin_int_len
|
||||
|
||||
# Pad out to an octet with ones.
|
||||
bits_to_be_padded = (8 - (final_int_len % 8)) % 8
|
||||
final_num <<= bits_to_be_padded
|
||||
final_num |= (1 << bits_to_be_padded) - 1
|
||||
|
||||
# Convert the number to hex and strip off the leading '0x' and the
|
||||
# trailing 'L', if present.
|
||||
final_num = hex(final_num)[2:].rstrip('L')
|
||||
|
||||
# If this is odd, prepend a zero.
|
||||
final_num = '0' + final_num if len(final_num) % 2 != 0 else final_num
|
||||
|
||||
# This number should have twice as many digits as bytes. If not, we're
|
||||
# missing some leading zeroes. Work out how many bytes we want and how
|
||||
# many digits we have, then add the missing zero digits to the front.
|
||||
total_bytes = (final_int_len + bits_to_be_padded) // 8
|
||||
expected_digits = total_bytes * 2
|
||||
|
||||
if len(final_num) != expected_digits:
|
||||
missing_digits = expected_digits - len(final_num)
|
||||
final_num = ('0' * missing_digits) + final_num
|
||||
|
||||
return bytes.fromhex(final_num)
|
||||
289
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/hpack/src/hpack/huffman_constants.py
vendored
Normal file
289
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/hpack/src/hpack/huffman_constants.py
vendored
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
hpack/huffman_constants
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Defines the constant Huffman table. This takes up an upsetting amount of space,
|
||||
but c'est la vie.
|
||||
"""
|
||||
# flake8: noqa
|
||||
|
||||
REQUEST_CODES = [
|
||||
0x1ff8,
|
||||
0x7fffd8,
|
||||
0xfffffe2,
|
||||
0xfffffe3,
|
||||
0xfffffe4,
|
||||
0xfffffe5,
|
||||
0xfffffe6,
|
||||
0xfffffe7,
|
||||
0xfffffe8,
|
||||
0xffffea,
|
||||
0x3ffffffc,
|
||||
0xfffffe9,
|
||||
0xfffffea,
|
||||
0x3ffffffd,
|
||||
0xfffffeb,
|
||||
0xfffffec,
|
||||
0xfffffed,
|
||||
0xfffffee,
|
||||
0xfffffef,
|
||||
0xffffff0,
|
||||
0xffffff1,
|
||||
0xffffff2,
|
||||
0x3ffffffe,
|
||||
0xffffff3,
|
||||
0xffffff4,
|
||||
0xffffff5,
|
||||
0xffffff6,
|
||||
0xffffff7,
|
||||
0xffffff8,
|
||||
0xffffff9,
|
||||
0xffffffa,
|
||||
0xffffffb,
|
||||
0x14,
|
||||
0x3f8,
|
||||
0x3f9,
|
||||
0xffa,
|
||||
0x1ff9,
|
||||
0x15,
|
||||
0xf8,
|
||||
0x7fa,
|
||||
0x3fa,
|
||||
0x3fb,
|
||||
0xf9,
|
||||
0x7fb,
|
||||
0xfa,
|
||||
0x16,
|
||||
0x17,
|
||||
0x18,
|
||||
0x0,
|
||||
0x1,
|
||||
0x2,
|
||||
0x19,
|
||||
0x1a,
|
||||
0x1b,
|
||||
0x1c,
|
||||
0x1d,
|
||||
0x1e,
|
||||
0x1f,
|
||||
0x5c,
|
||||
0xfb,
|
||||
0x7ffc,
|
||||
0x20,
|
||||
0xffb,
|
||||
0x3fc,
|
||||
0x1ffa,
|
||||
0x21,
|
||||
0x5d,
|
||||
0x5e,
|
||||
0x5f,
|
||||
0x60,
|
||||
0x61,
|
||||
0x62,
|
||||
0x63,
|
||||
0x64,
|
||||
0x65,
|
||||
0x66,
|
||||
0x67,
|
||||
0x68,
|
||||
0x69,
|
||||
0x6a,
|
||||
0x6b,
|
||||
0x6c,
|
||||
0x6d,
|
||||
0x6e,
|
||||
0x6f,
|
||||
0x70,
|
||||
0x71,
|
||||
0x72,
|
||||
0xfc,
|
||||
0x73,
|
||||
0xfd,
|
||||
0x1ffb,
|
||||
0x7fff0,
|
||||
0x1ffc,
|
||||
0x3ffc,
|
||||
0x22,
|
||||
0x7ffd,
|
||||
0x3,
|
||||
0x23,
|
||||
0x4,
|
||||
0x24,
|
||||
0x5,
|
||||
0x25,
|
||||
0x26,
|
||||
0x27,
|
||||
0x6,
|
||||
0x74,
|
||||
0x75,
|
||||
0x28,
|
||||
0x29,
|
||||
0x2a,
|
||||
0x7,
|
||||
0x2b,
|
||||
0x76,
|
||||
0x2c,
|
||||
0x8,
|
||||
0x9,
|
||||
0x2d,
|
||||
0x77,
|
||||
0x78,
|
||||
0x79,
|
||||
0x7a,
|
||||
0x7b,
|
||||
0x7ffe,
|
||||
0x7fc,
|
||||
0x3ffd,
|
||||
0x1ffd,
|
||||
0xffffffc,
|
||||
0xfffe6,
|
||||
0x3fffd2,
|
||||
0xfffe7,
|
||||
0xfffe8,
|
||||
0x3fffd3,
|
||||
0x3fffd4,
|
||||
0x3fffd5,
|
||||
0x7fffd9,
|
||||
0x3fffd6,
|
||||
0x7fffda,
|
||||
0x7fffdb,
|
||||
0x7fffdc,
|
||||
0x7fffdd,
|
||||
0x7fffde,
|
||||
0xffffeb,
|
||||
0x7fffdf,
|
||||
0xffffec,
|
||||
0xffffed,
|
||||
0x3fffd7,
|
||||
0x7fffe0,
|
||||
0xffffee,
|
||||
0x7fffe1,
|
||||
0x7fffe2,
|
||||
0x7fffe3,
|
||||
0x7fffe4,
|
||||
0x1fffdc,
|
||||
0x3fffd8,
|
||||
0x7fffe5,
|
||||
0x3fffd9,
|
||||
0x7fffe6,
|
||||
0x7fffe7,
|
||||
0xffffef,
|
||||
0x3fffda,
|
||||
0x1fffdd,
|
||||
0xfffe9,
|
||||
0x3fffdb,
|
||||
0x3fffdc,
|
||||
0x7fffe8,
|
||||
0x7fffe9,
|
||||
0x1fffde,
|
||||
0x7fffea,
|
||||
0x3fffdd,
|
||||
0x3fffde,
|
||||
0xfffff0,
|
||||
0x1fffdf,
|
||||
0x3fffdf,
|
||||
0x7fffeb,
|
||||
0x7fffec,
|
||||
0x1fffe0,
|
||||
0x1fffe1,
|
||||
0x3fffe0,
|
||||
0x1fffe2,
|
||||
0x7fffed,
|
||||
0x3fffe1,
|
||||
0x7fffee,
|
||||
0x7fffef,
|
||||
0xfffea,
|
||||
0x3fffe2,
|
||||
0x3fffe3,
|
||||
0x3fffe4,
|
||||
0x7ffff0,
|
||||
0x3fffe5,
|
||||
0x3fffe6,
|
||||
0x7ffff1,
|
||||
0x3ffffe0,
|
||||
0x3ffffe1,
|
||||
0xfffeb,
|
||||
0x7fff1,
|
||||
0x3fffe7,
|
||||
0x7ffff2,
|
||||
0x3fffe8,
|
||||
0x1ffffec,
|
||||
0x3ffffe2,
|
||||
0x3ffffe3,
|
||||
0x3ffffe4,
|
||||
0x7ffffde,
|
||||
0x7ffffdf,
|
||||
0x3ffffe5,
|
||||
0xfffff1,
|
||||
0x1ffffed,
|
||||
0x7fff2,
|
||||
0x1fffe3,
|
||||
0x3ffffe6,
|
||||
0x7ffffe0,
|
||||
0x7ffffe1,
|
||||
0x3ffffe7,
|
||||
0x7ffffe2,
|
||||
0xfffff2,
|
||||
0x1fffe4,
|
||||
0x1fffe5,
|
||||
0x3ffffe8,
|
||||
0x3ffffe9,
|
||||
0xffffffd,
|
||||
0x7ffffe3,
|
||||
0x7ffffe4,
|
||||
0x7ffffe5,
|
||||
0xfffec,
|
||||
0xfffff3,
|
||||
0xfffed,
|
||||
0x1fffe6,
|
||||
0x3fffe9,
|
||||
0x1fffe7,
|
||||
0x1fffe8,
|
||||
0x7ffff3,
|
||||
0x3fffea,
|
||||
0x3fffeb,
|
||||
0x1ffffee,
|
||||
0x1ffffef,
|
||||
0xfffff4,
|
||||
0xfffff5,
|
||||
0x3ffffea,
|
||||
0x7ffff4,
|
||||
0x3ffffeb,
|
||||
0x7ffffe6,
|
||||
0x3ffffec,
|
||||
0x3ffffed,
|
||||
0x7ffffe7,
|
||||
0x7ffffe8,
|
||||
0x7ffffe9,
|
||||
0x7ffffea,
|
||||
0x7ffffeb,
|
||||
0xffffffe,
|
||||
0x7ffffec,
|
||||
0x7ffffed,
|
||||
0x7ffffee,
|
||||
0x7ffffef,
|
||||
0x7fffff0,
|
||||
0x3ffffee,
|
||||
0x3fffffff,
|
||||
]
|
||||
|
||||
REQUEST_CODES_LENGTH = [
|
||||
13, 23, 28, 28, 28, 28, 28, 28, 28, 24, 30, 28, 28, 30, 28, 28,
|
||||
28, 28, 28, 28, 28, 28, 30, 28, 28, 28, 28, 28, 28, 28, 28, 28,
|
||||
6, 10, 10, 12, 13, 6, 8, 11, 10, 10, 8, 11, 8, 6, 6, 6,
|
||||
5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 7, 8, 15, 6, 12, 10,
|
||||
13, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
|
||||
7, 7, 7, 7, 7, 7, 7, 7, 8, 7, 8, 13, 19, 13, 14, 6,
|
||||
15, 5, 6, 5, 6, 5, 6, 6, 6, 5, 7, 7, 6, 6, 6, 5,
|
||||
6, 7, 6, 5, 5, 6, 7, 7, 7, 7, 7, 15, 11, 14, 13, 28,
|
||||
20, 22, 20, 20, 22, 22, 22, 23, 22, 23, 23, 23, 23, 23, 24, 23,
|
||||
24, 24, 22, 23, 24, 23, 23, 23, 23, 21, 22, 23, 22, 23, 23, 24,
|
||||
22, 21, 20, 22, 22, 23, 23, 21, 23, 22, 22, 24, 21, 22, 23, 23,
|
||||
21, 21, 22, 21, 23, 22, 23, 23, 20, 22, 22, 22, 23, 22, 22, 23,
|
||||
26, 26, 20, 19, 22, 23, 22, 25, 26, 26, 26, 27, 27, 26, 24, 25,
|
||||
19, 21, 26, 27, 27, 26, 27, 24, 21, 21, 26, 26, 28, 27, 27, 27,
|
||||
20, 24, 20, 21, 22, 21, 21, 23, 22, 22, 25, 25, 24, 24, 26, 23,
|
||||
26, 27, 26, 26, 27, 27, 27, 27, 27, 28, 27, 27, 27, 27, 27, 26,
|
||||
30,
|
||||
]
|
||||
4739
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/hpack/src/hpack/huffman_table.py
vendored
Normal file
4739
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/hpack/src/hpack/huffman_table.py
vendored
Normal file
File diff suppressed because it is too large
Load diff
39
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/hpack/src/hpack/struct.py
vendored
Normal file
39
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/hpack/src/hpack/struct.py
vendored
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
hpack/struct
|
||||
~~~~~~~~~~~~
|
||||
|
||||
Contains structures for representing header fields with associated metadata.
|
||||
"""
|
||||
|
||||
|
||||
class HeaderTuple(tuple):
|
||||
"""
|
||||
A data structure that stores a single header field.
|
||||
|
||||
HTTP headers can be thought of as tuples of ``(field name, field value)``.
|
||||
A single header block is a sequence of such tuples.
|
||||
|
||||
In HTTP/2, however, certain bits of additional information are required for
|
||||
compressing these headers: in particular, whether the header field can be
|
||||
safely added to the HPACK compression context.
|
||||
|
||||
This class stores a header that can be added to the compression context. In
|
||||
all other ways it behaves exactly like a tuple.
|
||||
"""
|
||||
__slots__ = ()
|
||||
|
||||
indexable = True
|
||||
|
||||
def __new__(cls, *args):
|
||||
return tuple.__new__(cls, args)
|
||||
|
||||
|
||||
class NeverIndexedHeaderTuple(HeaderTuple):
|
||||
"""
|
||||
A data structure that stores a single header field that cannot be added to
|
||||
a HTTP/2 header compression context.
|
||||
"""
|
||||
__slots__ = ()
|
||||
|
||||
indexable = False
|
||||
235
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/hpack/src/hpack/table.py
vendored
Normal file
235
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/hpack/src/hpack/table.py
vendored
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# flake8: noqa
|
||||
from collections import deque
|
||||
import logging
|
||||
|
||||
from .exceptions import InvalidTableIndex
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def table_entry_size(name, value):
|
||||
"""
|
||||
Calculates the size of a single entry
|
||||
|
||||
This size is mostly irrelevant to us and defined
|
||||
specifically to accommodate memory management for
|
||||
lower level implementations. The 32 extra bytes are
|
||||
considered the "maximum" overhead that would be
|
||||
required to represent each entry in the table.
|
||||
|
||||
See RFC7541 Section 4.1
|
||||
"""
|
||||
return 32 + len(name) + len(value)
|
||||
|
||||
|
||||
class HeaderTable:
|
||||
"""
|
||||
Implements the combined static and dynamic header table
|
||||
|
||||
The name and value arguments for all the functions
|
||||
should ONLY be byte strings (b'') however this is not
|
||||
strictly enforced in the interface.
|
||||
|
||||
See RFC7541 Section 2.3
|
||||
"""
|
||||
#: Default maximum size of the dynamic table. See
|
||||
#: RFC7540 Section 6.5.2.
|
||||
DEFAULT_SIZE = 4096
|
||||
|
||||
#: Constant list of static headers. See RFC7541 Section
|
||||
#: 2.3.1 and Appendix A
|
||||
STATIC_TABLE = (
|
||||
(b':authority' , b'' ), # noqa
|
||||
(b':method' , b'GET' ), # noqa
|
||||
(b':method' , b'POST' ), # noqa
|
||||
(b':path' , b'/' ), # noqa
|
||||
(b':path' , b'/index.html' ), # noqa
|
||||
(b':scheme' , b'http' ), # noqa
|
||||
(b':scheme' , b'https' ), # noqa
|
||||
(b':status' , b'200' ), # noqa
|
||||
(b':status' , b'204' ), # noqa
|
||||
(b':status' , b'206' ), # noqa
|
||||
(b':status' , b'304' ), # noqa
|
||||
(b':status' , b'400' ), # noqa
|
||||
(b':status' , b'404' ), # noqa
|
||||
(b':status' , b'500' ), # noqa
|
||||
(b'accept-charset' , b'' ), # noqa
|
||||
(b'accept-encoding' , b'gzip, deflate'), # noqa
|
||||
(b'accept-language' , b'' ), # noqa
|
||||
(b'accept-ranges' , b'' ), # noqa
|
||||
(b'accept' , b'' ), # noqa
|
||||
(b'access-control-allow-origin' , b'' ), # noqa
|
||||
(b'age' , b'' ), # noqa
|
||||
(b'allow' , b'' ), # noqa
|
||||
(b'authorization' , b'' ), # noqa
|
||||
(b'cache-control' , b'' ), # noqa
|
||||
(b'content-disposition' , b'' ), # noqa
|
||||
(b'content-encoding' , b'' ), # noqa
|
||||
(b'content-language' , b'' ), # noqa
|
||||
(b'content-length' , b'' ), # noqa
|
||||
(b'content-location' , b'' ), # noqa
|
||||
(b'content-range' , b'' ), # noqa
|
||||
(b'content-type' , b'' ), # noqa
|
||||
(b'cookie' , b'' ), # noqa
|
||||
(b'date' , b'' ), # noqa
|
||||
(b'etag' , b'' ), # noqa
|
||||
(b'expect' , b'' ), # noqa
|
||||
(b'expires' , b'' ), # noqa
|
||||
(b'from' , b'' ), # noqa
|
||||
(b'host' , b'' ), # noqa
|
||||
(b'if-match' , b'' ), # noqa
|
||||
(b'if-modified-since' , b'' ), # noqa
|
||||
(b'if-none-match' , b'' ), # noqa
|
||||
(b'if-range' , b'' ), # noqa
|
||||
(b'if-unmodified-since' , b'' ), # noqa
|
||||
(b'last-modified' , b'' ), # noqa
|
||||
(b'link' , b'' ), # noqa
|
||||
(b'location' , b'' ), # noqa
|
||||
(b'max-forwards' , b'' ), # noqa
|
||||
(b'proxy-authenticate' , b'' ), # noqa
|
||||
(b'proxy-authorization' , b'' ), # noqa
|
||||
(b'range' , b'' ), # noqa
|
||||
(b'referer' , b'' ), # noqa
|
||||
(b'refresh' , b'' ), # noqa
|
||||
(b'retry-after' , b'' ), # noqa
|
||||
(b'server' , b'' ), # noqa
|
||||
(b'set-cookie' , b'' ), # noqa
|
||||
(b'strict-transport-security' , b'' ), # noqa
|
||||
(b'transfer-encoding' , b'' ), # noqa
|
||||
(b'user-agent' , b'' ), # noqa
|
||||
(b'vary' , b'' ), # noqa
|
||||
(b'via' , b'' ), # noqa
|
||||
(b'www-authenticate' , b'' ), # noqa
|
||||
) # noqa
|
||||
|
||||
STATIC_TABLE_LENGTH = len(STATIC_TABLE)
|
||||
|
||||
def __init__(self):
|
||||
self._maxsize = HeaderTable.DEFAULT_SIZE
|
||||
self._current_size = 0
|
||||
self.resized = False
|
||||
self.dynamic_entries = deque()
|
||||
|
||||
def get_by_index(self, index):
|
||||
"""
|
||||
Returns the entry specified by index
|
||||
|
||||
Note that the table is 1-based ie an index of 0 is
|
||||
invalid. This is due to the fact that a zero value
|
||||
index signals that a completely unindexed header
|
||||
follows.
|
||||
|
||||
The entry will either be from the static table or
|
||||
the dynamic table depending on the value of index.
|
||||
"""
|
||||
original_index = index
|
||||
index -= 1
|
||||
if 0 <= index:
|
||||
if index < HeaderTable.STATIC_TABLE_LENGTH:
|
||||
return HeaderTable.STATIC_TABLE[index]
|
||||
|
||||
index -= HeaderTable.STATIC_TABLE_LENGTH
|
||||
if index < len(self.dynamic_entries):
|
||||
return self.dynamic_entries[index]
|
||||
|
||||
raise InvalidTableIndex("Invalid table index %d" % original_index)
|
||||
|
||||
def __repr__(self):
|
||||
return "HeaderTable(%d, %s, %r)" % (
|
||||
self._maxsize,
|
||||
self.resized,
|
||||
self.dynamic_entries
|
||||
)
|
||||
|
||||
def add(self, name, value):
|
||||
"""
|
||||
Adds a new entry to the table
|
||||
|
||||
We reduce the table size if the entry will make the
|
||||
table size greater than maxsize.
|
||||
"""
|
||||
# We just clear the table if the entry is too big
|
||||
size = table_entry_size(name, value)
|
||||
if size > self._maxsize:
|
||||
self.dynamic_entries.clear()
|
||||
self._current_size = 0
|
||||
else:
|
||||
# Add new entry
|
||||
self.dynamic_entries.appendleft((name, value))
|
||||
self._current_size += size
|
||||
self._shrink()
|
||||
|
||||
def search(self, name, value):
|
||||
"""
|
||||
Searches the table for the entry specified by name
|
||||
and value
|
||||
|
||||
Returns one of the following:
|
||||
- ``None``, no match at all
|
||||
- ``(index, name, None)`` for partial matches on name only.
|
||||
- ``(index, name, value)`` for perfect matches.
|
||||
"""
|
||||
partial = None
|
||||
|
||||
header_name_search_result = HeaderTable.STATIC_TABLE_MAPPING.get(name)
|
||||
if header_name_search_result:
|
||||
index = header_name_search_result[1].get(value)
|
||||
if index is not None:
|
||||
return index, name, value
|
||||
else:
|
||||
partial = (header_name_search_result[0], name, None)
|
||||
|
||||
offset = HeaderTable.STATIC_TABLE_LENGTH + 1
|
||||
for (i, (n, v)) in enumerate(self.dynamic_entries):
|
||||
if n == name:
|
||||
if v == value:
|
||||
return i + offset, n, v
|
||||
elif partial is None:
|
||||
partial = (i + offset, n, None)
|
||||
return partial
|
||||
|
||||
@property
|
||||
def maxsize(self):
|
||||
return self._maxsize
|
||||
|
||||
@maxsize.setter
|
||||
def maxsize(self, newmax):
|
||||
newmax = int(newmax)
|
||||
log.debug("Resizing header table to %d from %d", newmax, self._maxsize)
|
||||
oldmax = self._maxsize
|
||||
self._maxsize = newmax
|
||||
self.resized = (newmax != oldmax)
|
||||
if newmax <= 0:
|
||||
self.dynamic_entries.clear()
|
||||
self._current_size = 0
|
||||
elif oldmax > newmax:
|
||||
self._shrink()
|
||||
|
||||
def _shrink(self):
|
||||
"""
|
||||
Shrinks the dynamic table to be at or below maxsize
|
||||
"""
|
||||
cursize = self._current_size
|
||||
while cursize > self._maxsize:
|
||||
name, value = self.dynamic_entries.pop()
|
||||
cursize -= table_entry_size(name, value)
|
||||
log.debug("Evicting %s: %s from the header table", name, value)
|
||||
self._current_size = cursize
|
||||
|
||||
|
||||
def _build_static_table_mapping():
|
||||
"""
|
||||
Build static table mapping from header name to tuple with next structure:
|
||||
(<minimal index of header>, <mapping from header value to it index>).
|
||||
|
||||
static_table_mapping used for hash searching.
|
||||
"""
|
||||
static_table_mapping = {}
|
||||
for index, (name, value) in enumerate(HeaderTable.STATIC_TABLE, 1):
|
||||
header_name_search_result = static_table_mapping.setdefault(name, (index, {}))
|
||||
header_name_search_result[1][value] = index
|
||||
return static_table_mapping
|
||||
|
||||
|
||||
HeaderTable.STATIC_TABLE_MAPPING = _build_static_table_mapping()
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
hyperframe
|
||||
~~~~~~~~~~
|
||||
|
||||
A module for providing a pure-Python HTTP/2 framing layer.
|
||||
"""
|
||||
__version__ = '6.0.1'
|
||||
67
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/hyperframe/src/hyperframe/exceptions.py
vendored
Normal file
67
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/hyperframe/src/hyperframe/exceptions.py
vendored
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
hyperframe/exceptions
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Defines the exceptions that can be thrown by hyperframe.
|
||||
"""
|
||||
|
||||
|
||||
class HyperframeError(Exception):
|
||||
"""
|
||||
The base class for all exceptions for the hyperframe module.
|
||||
|
||||
.. versionadded:: 6.0.0
|
||||
"""
|
||||
|
||||
|
||||
class UnknownFrameError(HyperframeError):
|
||||
"""
|
||||
A frame of unknown type was received.
|
||||
|
||||
.. versionchanged:: 6.0.0
|
||||
Changed base class from `ValueError` to :class:`HyperframeError`
|
||||
"""
|
||||
def __init__(self, frame_type: int, length: int) -> None:
|
||||
#: The type byte of the unknown frame that was received.
|
||||
self.frame_type = frame_type
|
||||
|
||||
#: The length of the data portion of the unknown frame.
|
||||
self.length = length
|
||||
|
||||
def __str__(self) -> str:
|
||||
return (
|
||||
"UnknownFrameError: Unknown frame type 0x%X received, "
|
||||
"length %d bytes" % (self.frame_type, self.length)
|
||||
)
|
||||
|
||||
|
||||
class InvalidPaddingError(HyperframeError):
|
||||
"""
|
||||
A frame with invalid padding was received.
|
||||
|
||||
.. versionchanged:: 6.0.0
|
||||
Changed base class from `ValueError` to :class:`HyperframeError`
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class InvalidFrameError(HyperframeError):
|
||||
"""
|
||||
Parsing a frame failed because the data was not laid out appropriately.
|
||||
|
||||
.. versionadded:: 3.0.2
|
||||
|
||||
.. versionchanged:: 6.0.0
|
||||
Changed base class from `ValueError` to :class:`HyperframeError`
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class InvalidDataError(HyperframeError):
|
||||
"""
|
||||
Content or data of a frame was is invalid or violates the specification.
|
||||
|
||||
.. versionadded:: 6.0.0
|
||||
"""
|
||||
pass
|
||||
51
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/hyperframe/src/hyperframe/flags.py
vendored
Normal file
51
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/hyperframe/src/hyperframe/flags.py
vendored
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
hyperframe/flags
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Defines basic Flag and Flags data structures.
|
||||
"""
|
||||
from collections.abc import MutableSet
|
||||
from typing import NamedTuple, Iterable, Set, Iterator
|
||||
|
||||
|
||||
class Flag(NamedTuple):
|
||||
name: str
|
||||
bit: int
|
||||
|
||||
|
||||
class Flags(MutableSet): # type: ignore
|
||||
"""
|
||||
A simple MutableSet implementation that will only accept known flags as
|
||||
elements.
|
||||
|
||||
Will behave like a regular set(), except that a ValueError will be thrown
|
||||
when .add()ing unexpected flags.
|
||||
"""
|
||||
def __init__(self, defined_flags: Iterable[Flag]):
|
||||
self._valid_flags = set(flag.name for flag in defined_flags)
|
||||
self._flags: Set[str] = set()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return repr(sorted(list(self._flags)))
|
||||
|
||||
def __contains__(self, x: object) -> bool:
|
||||
return self._flags.__contains__(x)
|
||||
|
||||
def __iter__(self) -> Iterator[str]:
|
||||
return self._flags.__iter__()
|
||||
|
||||
def __len__(self) -> int:
|
||||
return self._flags.__len__()
|
||||
|
||||
def discard(self, value: str) -> None:
|
||||
return self._flags.discard(value)
|
||||
|
||||
def add(self, value: str) -> None:
|
||||
if value not in self._valid_flags:
|
||||
raise ValueError(
|
||||
"Unexpected flag: {}. Valid flags are: {}".format(
|
||||
value, self._valid_flags
|
||||
)
|
||||
)
|
||||
return self._flags.add(value)
|
||||
969
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/hyperframe/src/hyperframe/frame.py
vendored
Normal file
969
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/hyperframe/src/hyperframe/frame.py
vendored
Normal file
|
|
@ -0,0 +1,969 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
hyperframe/frame
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Defines framing logic for HTTP/2. Provides both classes to represent framed
|
||||
data and logic for aiding the connection when it comes to reading from the
|
||||
socket.
|
||||
"""
|
||||
import struct
|
||||
import binascii
|
||||
|
||||
from .exceptions import (
|
||||
UnknownFrameError, InvalidPaddingError, InvalidFrameError, InvalidDataError
|
||||
)
|
||||
from .flags import Flag, Flags
|
||||
from typing import Optional, Tuple, List, Iterable, Any, Dict, Type
|
||||
|
||||
|
||||
# The maximum initial length of a frame. Some frames have shorter maximum
|
||||
# lengths.
|
||||
FRAME_MAX_LEN = (2 ** 14)
|
||||
|
||||
# The maximum allowed length of a frame.
|
||||
FRAME_MAX_ALLOWED_LEN = (2 ** 24) - 1
|
||||
|
||||
# Stream association enumerations.
|
||||
_STREAM_ASSOC_HAS_STREAM = "has-stream"
|
||||
_STREAM_ASSOC_NO_STREAM = "no-stream"
|
||||
_STREAM_ASSOC_EITHER = "either"
|
||||
|
||||
# Structs for packing and unpacking
|
||||
_STRUCT_HBBBL = struct.Struct(">HBBBL")
|
||||
_STRUCT_LL = struct.Struct(">LL")
|
||||
_STRUCT_HL = struct.Struct(">HL")
|
||||
_STRUCT_LB = struct.Struct(">LB")
|
||||
_STRUCT_L = struct.Struct(">L")
|
||||
_STRUCT_H = struct.Struct(">H")
|
||||
_STRUCT_B = struct.Struct(">B")
|
||||
|
||||
|
||||
class Frame:
|
||||
"""
|
||||
The base class for all HTTP/2 frames.
|
||||
"""
|
||||
#: The flags defined on this type of frame.
|
||||
defined_flags: List[Flag] = []
|
||||
|
||||
#: The byte used to define the type of the frame.
|
||||
type: Optional[int] = None
|
||||
|
||||
# If 'has-stream', the frame's stream_id must be non-zero. If 'no-stream',
|
||||
# it must be zero. If 'either', it's not checked.
|
||||
stream_association: Optional[str] = None
|
||||
|
||||
def __init__(self, stream_id: int, flags: Iterable[str] = ()) -> None:
|
||||
#: The stream identifier for the stream this frame was received on.
|
||||
#: Set to 0 for frames sent on the connection (stream-id 0).
|
||||
self.stream_id = stream_id
|
||||
|
||||
#: The flags set for this frame.
|
||||
self.flags = Flags(self.defined_flags)
|
||||
|
||||
#: The frame length, excluding the nine-byte header.
|
||||
self.body_len = 0
|
||||
|
||||
for flag in flags:
|
||||
self.flags.add(flag)
|
||||
|
||||
if (not self.stream_id and
|
||||
self.stream_association == _STREAM_ASSOC_HAS_STREAM):
|
||||
raise InvalidDataError(
|
||||
'Stream ID must be non-zero for {}'.format(
|
||||
type(self).__name__,
|
||||
)
|
||||
)
|
||||
if (self.stream_id and
|
||||
self.stream_association == _STREAM_ASSOC_NO_STREAM):
|
||||
raise InvalidDataError(
|
||||
'Stream ID must be zero for {} with stream_id={}'.format(
|
||||
type(self).__name__,
|
||||
self.stream_id,
|
||||
)
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
"{}(stream_id={}, flags={}): {}"
|
||||
).format(
|
||||
type(self).__name__,
|
||||
self.stream_id,
|
||||
repr(self.flags),
|
||||
self._body_repr(),
|
||||
)
|
||||
|
||||
def _body_repr(self) -> str:
|
||||
# More specific implementation may be provided by subclasses of Frame.
|
||||
# This fallback shows the serialized (and truncated) body content.
|
||||
return _raw_data_repr(self.serialize_body())
|
||||
|
||||
@staticmethod
|
||||
def explain(data: memoryview) -> Tuple["Frame", int]:
|
||||
"""
|
||||
Takes a bytestring and tries to parse a single frame and print it.
|
||||
|
||||
This function is only provided for debugging purposes.
|
||||
|
||||
:param data: A memoryview object containing the raw data of at least
|
||||
one complete frame (header and body).
|
||||
|
||||
.. versionadded:: 6.0.0
|
||||
"""
|
||||
frame, length = Frame.parse_frame_header(data[:9])
|
||||
frame.parse_body(data[9:9 + length])
|
||||
print(frame)
|
||||
return frame, length
|
||||
|
||||
@staticmethod
|
||||
def parse_frame_header(header: memoryview, strict: bool = False) -> Tuple["Frame", int]:
|
||||
"""
|
||||
Takes a 9-byte frame header and returns a tuple of the appropriate
|
||||
Frame object and the length that needs to be read from the socket.
|
||||
|
||||
This populates the flags field, and determines how long the body is.
|
||||
|
||||
:param header: A memoryview object containing the 9-byte frame header
|
||||
data of a frame. Must not contain more or less.
|
||||
|
||||
:param strict: Whether to raise an exception when encountering a frame
|
||||
not defined by spec and implemented by hyperframe.
|
||||
|
||||
:raises hyperframe.exceptions.UnknownFrameError: If a frame of unknown
|
||||
type is received.
|
||||
|
||||
.. versionchanged:: 5.0.0
|
||||
Added :param:`strict` to accommodate :class:`ExtensionFrame`
|
||||
"""
|
||||
try:
|
||||
fields = _STRUCT_HBBBL.unpack(header)
|
||||
except struct.error:
|
||||
raise InvalidFrameError("Invalid frame header")
|
||||
|
||||
# First 24 bits are frame length.
|
||||
length = (fields[0] << 8) + fields[1]
|
||||
type = fields[2]
|
||||
flags = fields[3]
|
||||
stream_id = fields[4] & 0x7FFFFFFF
|
||||
|
||||
try:
|
||||
frame = FRAMES[type](stream_id)
|
||||
except KeyError:
|
||||
if strict:
|
||||
raise UnknownFrameError(type, length)
|
||||
frame = ExtensionFrame(type=type, stream_id=stream_id)
|
||||
|
||||
frame.parse_flags(flags)
|
||||
return (frame, length)
|
||||
|
||||
def parse_flags(self, flag_byte: int) -> Flags:
|
||||
for flag, flag_bit in self.defined_flags:
|
||||
if flag_byte & flag_bit:
|
||||
self.flags.add(flag)
|
||||
|
||||
return self.flags
|
||||
|
||||
def serialize(self) -> bytes:
|
||||
"""
|
||||
Convert a frame into a bytestring, representing the serialized form of
|
||||
the frame.
|
||||
"""
|
||||
body = self.serialize_body()
|
||||
self.body_len = len(body)
|
||||
|
||||
# Build the common frame header.
|
||||
# First, get the flags.
|
||||
flags = 0
|
||||
|
||||
for flag, flag_bit in self.defined_flags:
|
||||
if flag in self.flags:
|
||||
flags |= flag_bit
|
||||
|
||||
header = _STRUCT_HBBBL.pack(
|
||||
(self.body_len >> 8) & 0xFFFF, # Length spread over top 24 bits
|
||||
self.body_len & 0xFF,
|
||||
self.type,
|
||||
flags,
|
||||
self.stream_id & 0x7FFFFFFF # Stream ID is 32 bits.
|
||||
)
|
||||
|
||||
return header + body
|
||||
|
||||
def serialize_body(self) -> bytes:
|
||||
raise NotImplementedError()
|
||||
|
||||
def parse_body(self, data: memoryview) -> None:
|
||||
"""
|
||||
Given the body of a frame, parses it into frame data. This populates
|
||||
the non-header parts of the frame: that is, it does not populate the
|
||||
stream ID or flags.
|
||||
|
||||
:param data: A memoryview object containing the body data of the frame.
|
||||
Must not contain *more* data than the length returned by
|
||||
:meth:`parse_frame_header
|
||||
<hyperframe.frame.Frame.parse_frame_header>`.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class Padding:
|
||||
"""
|
||||
Mixin for frames that contain padding. Defines extra fields that can be
|
||||
used and set by frames that can be padded.
|
||||
"""
|
||||
def __init__(self, stream_id: int, pad_length: int = 0, **kwargs: Any) -> None:
|
||||
super().__init__(stream_id, **kwargs) # type: ignore
|
||||
|
||||
#: The length of the padding to use.
|
||||
self.pad_length = pad_length
|
||||
|
||||
def serialize_padding_data(self) -> bytes:
|
||||
if 'PADDED' in self.flags: # type: ignore
|
||||
return _STRUCT_B.pack(self.pad_length)
|
||||
return b''
|
||||
|
||||
def parse_padding_data(self, data: memoryview) -> int:
|
||||
if 'PADDED' in self.flags: # type: ignore
|
||||
try:
|
||||
self.pad_length = struct.unpack('!B', data[:1])[0]
|
||||
except struct.error:
|
||||
raise InvalidFrameError("Invalid Padding data")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
#: .. deprecated:: 5.2.1
|
||||
#: Use self.pad_length instead.
|
||||
@property
|
||||
def total_padding(self) -> int: # pragma: no cover
|
||||
import warnings
|
||||
warnings.warn(
|
||||
"total_padding contains the same information as pad_length.",
|
||||
DeprecationWarning
|
||||
)
|
||||
return self.pad_length
|
||||
|
||||
|
||||
class Priority:
|
||||
"""
|
||||
Mixin for frames that contain priority data. Defines extra fields that can
|
||||
be used and set by frames that contain priority data.
|
||||
"""
|
||||
def __init__(self,
|
||||
stream_id: int,
|
||||
depends_on: int = 0x0,
|
||||
stream_weight: int = 0x0,
|
||||
exclusive: bool = False,
|
||||
**kwargs: Any) -> None:
|
||||
super().__init__(stream_id, **kwargs) # type: ignore
|
||||
|
||||
#: The stream ID of the stream on which this stream depends.
|
||||
self.depends_on = depends_on
|
||||
|
||||
#: The weight of the stream. This is an integer between 0 and 256.
|
||||
self.stream_weight = stream_weight
|
||||
|
||||
#: Whether the exclusive bit was set.
|
||||
self.exclusive = exclusive
|
||||
|
||||
def serialize_priority_data(self) -> bytes:
|
||||
return _STRUCT_LB.pack(
|
||||
self.depends_on + (0x80000000 if self.exclusive else 0),
|
||||
self.stream_weight
|
||||
)
|
||||
|
||||
def parse_priority_data(self, data: memoryview) -> int:
|
||||
try:
|
||||
self.depends_on, self.stream_weight = _STRUCT_LB.unpack(data[:5])
|
||||
except struct.error:
|
||||
raise InvalidFrameError("Invalid Priority data")
|
||||
|
||||
self.exclusive = True if self.depends_on >> 31 else False
|
||||
self.depends_on &= 0x7FFFFFFF
|
||||
return 5
|
||||
|
||||
|
||||
class DataFrame(Padding, Frame):
|
||||
"""
|
||||
DATA frames convey arbitrary, variable-length sequences of octets
|
||||
associated with a stream. One or more DATA frames are used, for instance,
|
||||
to carry HTTP request or response payloads.
|
||||
"""
|
||||
#: The flags defined for DATA frames.
|
||||
defined_flags = [
|
||||
Flag('END_STREAM', 0x01),
|
||||
Flag('PADDED', 0x08),
|
||||
]
|
||||
|
||||
#: The type byte for data frames.
|
||||
type = 0x0
|
||||
|
||||
stream_association = _STREAM_ASSOC_HAS_STREAM
|
||||
|
||||
def __init__(self, stream_id: int, data: bytes = b'', **kwargs: Any) -> None:
|
||||
super().__init__(stream_id, **kwargs)
|
||||
|
||||
#: The data contained on this frame.
|
||||
self.data = data
|
||||
|
||||
def serialize_body(self) -> bytes:
|
||||
padding_data = self.serialize_padding_data()
|
||||
padding = b'\0' * self.pad_length
|
||||
if isinstance(self.data, memoryview):
|
||||
self.data = self.data.tobytes()
|
||||
return b''.join([padding_data, self.data, padding])
|
||||
|
||||
def parse_body(self, data: memoryview) -> None:
|
||||
padding_data_length = self.parse_padding_data(data)
|
||||
self.data = (
|
||||
data[padding_data_length:len(data)-self.pad_length].tobytes()
|
||||
)
|
||||
self.body_len = len(data)
|
||||
|
||||
if self.pad_length and self.pad_length >= self.body_len:
|
||||
raise InvalidPaddingError("Padding is too long.")
|
||||
|
||||
@property
|
||||
def flow_controlled_length(self) -> int:
|
||||
"""
|
||||
The length of the frame that needs to be accounted for when considering
|
||||
flow control.
|
||||
"""
|
||||
padding_len = 0
|
||||
if 'PADDED' in self.flags:
|
||||
# Account for extra 1-byte padding length field, which is still
|
||||
# present if possibly zero-valued.
|
||||
padding_len = self.pad_length + 1
|
||||
return len(self.data) + padding_len
|
||||
|
||||
|
||||
class PriorityFrame(Priority, Frame):
|
||||
"""
|
||||
The PRIORITY frame specifies the sender-advised priority of a stream. It
|
||||
can be sent at any time for an existing stream. This enables
|
||||
reprioritisation of existing streams.
|
||||
"""
|
||||
#: The flags defined for PRIORITY frames.
|
||||
defined_flags: List[Flag] = []
|
||||
|
||||
#: The type byte defined for PRIORITY frames.
|
||||
type = 0x02
|
||||
|
||||
stream_association = _STREAM_ASSOC_HAS_STREAM
|
||||
|
||||
def _body_repr(self) -> str:
|
||||
return "exclusive={}, depends_on={}, stream_weight={}".format(
|
||||
self.exclusive,
|
||||
self.depends_on,
|
||||
self.stream_weight
|
||||
)
|
||||
|
||||
def serialize_body(self) -> bytes:
|
||||
return self.serialize_priority_data()
|
||||
|
||||
def parse_body(self, data: memoryview) -> None:
|
||||
if len(data) > 5:
|
||||
raise InvalidFrameError(
|
||||
"PRIORITY must have 5 byte body: actual length %s." %
|
||||
len(data)
|
||||
)
|
||||
|
||||
self.parse_priority_data(data)
|
||||
self.body_len = 5
|
||||
|
||||
|
||||
class RstStreamFrame(Frame):
|
||||
"""
|
||||
The RST_STREAM frame allows for abnormal termination of a stream. When sent
|
||||
by the initiator of a stream, it indicates that they wish to cancel the
|
||||
stream or that an error condition has occurred. When sent by the receiver
|
||||
of a stream, it indicates that either the receiver is rejecting the stream,
|
||||
requesting that the stream be cancelled or that an error condition has
|
||||
occurred.
|
||||
"""
|
||||
#: The flags defined for RST_STREAM frames.
|
||||
defined_flags: List[Flag] = []
|
||||
|
||||
#: The type byte defined for RST_STREAM frames.
|
||||
type = 0x03
|
||||
|
||||
stream_association = _STREAM_ASSOC_HAS_STREAM
|
||||
|
||||
def __init__(self, stream_id: int, error_code: int = 0, **kwargs: Any) -> None:
|
||||
super().__init__(stream_id, **kwargs)
|
||||
|
||||
#: The error code used when resetting the stream.
|
||||
self.error_code = error_code
|
||||
|
||||
def _body_repr(self) -> str:
|
||||
return "error_code={}".format(
|
||||
self.error_code,
|
||||
)
|
||||
|
||||
def serialize_body(self) -> bytes:
|
||||
return _STRUCT_L.pack(self.error_code)
|
||||
|
||||
def parse_body(self, data: memoryview) -> None:
|
||||
if len(data) != 4:
|
||||
raise InvalidFrameError(
|
||||
"RST_STREAM must have 4 byte body: actual length %s." %
|
||||
len(data)
|
||||
)
|
||||
|
||||
try:
|
||||
self.error_code = _STRUCT_L.unpack(data)[0]
|
||||
except struct.error: # pragma: no cover
|
||||
raise InvalidFrameError("Invalid RST_STREAM body")
|
||||
|
||||
self.body_len = 4
|
||||
|
||||
|
||||
class SettingsFrame(Frame):
|
||||
"""
|
||||
The SETTINGS frame conveys configuration parameters that affect how
|
||||
endpoints communicate. The parameters are either constraints on peer
|
||||
behavior or preferences.
|
||||
|
||||
Settings are not negotiated. Settings describe characteristics of the
|
||||
sending peer, which are used by the receiving peer. Different values for
|
||||
the same setting can be advertised by each peer. For example, a client
|
||||
might set a high initial flow control window, whereas a server might set a
|
||||
lower value to conserve resources.
|
||||
"""
|
||||
#: The flags defined for SETTINGS frames.
|
||||
defined_flags = [Flag('ACK', 0x01)]
|
||||
|
||||
#: The type byte defined for SETTINGS frames.
|
||||
type = 0x04
|
||||
|
||||
stream_association = _STREAM_ASSOC_NO_STREAM
|
||||
|
||||
# We need to define the known settings, they may as well be class
|
||||
# attributes.
|
||||
#: The byte that signals the SETTINGS_HEADER_TABLE_SIZE setting.
|
||||
HEADER_TABLE_SIZE = 0x01
|
||||
#: The byte that signals the SETTINGS_ENABLE_PUSH setting.
|
||||
ENABLE_PUSH = 0x02
|
||||
#: The byte that signals the SETTINGS_MAX_CONCURRENT_STREAMS setting.
|
||||
MAX_CONCURRENT_STREAMS = 0x03
|
||||
#: The byte that signals the SETTINGS_INITIAL_WINDOW_SIZE setting.
|
||||
INITIAL_WINDOW_SIZE = 0x04
|
||||
#: The byte that signals the SETTINGS_MAX_FRAME_SIZE setting.
|
||||
MAX_FRAME_SIZE = 0x05
|
||||
#: The byte that signals the SETTINGS_MAX_HEADER_LIST_SIZE setting.
|
||||
MAX_HEADER_LIST_SIZE = 0x06
|
||||
#: The byte that signals SETTINGS_ENABLE_CONNECT_PROTOCOL setting.
|
||||
ENABLE_CONNECT_PROTOCOL = 0x08
|
||||
|
||||
def __init__(self, stream_id: int = 0, settings: Optional[Dict[int, int]] = None, **kwargs: Any) -> None:
|
||||
super().__init__(stream_id, **kwargs)
|
||||
|
||||
if settings and "ACK" in kwargs.get("flags", ()):
|
||||
raise InvalidDataError(
|
||||
"Settings must be empty if ACK flag is set."
|
||||
)
|
||||
|
||||
#: A dictionary of the setting type byte to the value of the setting.
|
||||
self.settings = settings or {}
|
||||
|
||||
def _body_repr(self) -> str:
|
||||
return "settings={}".format(
|
||||
self.settings,
|
||||
)
|
||||
|
||||
def serialize_body(self) -> bytes:
|
||||
return b''.join([_STRUCT_HL.pack(setting & 0xFF, value)
|
||||
for setting, value in self.settings.items()])
|
||||
|
||||
def parse_body(self, data: memoryview) -> None:
|
||||
if 'ACK' in self.flags and len(data) > 0:
|
||||
raise InvalidDataError(
|
||||
"SETTINGS ack frame must not have payload: got %s bytes" %
|
||||
len(data)
|
||||
)
|
||||
|
||||
body_len = 0
|
||||
for i in range(0, len(data), 6):
|
||||
try:
|
||||
name, value = _STRUCT_HL.unpack(data[i:i+6])
|
||||
except struct.error:
|
||||
raise InvalidFrameError("Invalid SETTINGS body")
|
||||
|
||||
self.settings[name] = value
|
||||
body_len += 6
|
||||
|
||||
self.body_len = body_len
|
||||
|
||||
|
||||
class PushPromiseFrame(Padding, Frame):
|
||||
"""
|
||||
The PUSH_PROMISE frame is used to notify the peer endpoint in advance of
|
||||
streams the sender intends to initiate.
|
||||
"""
|
||||
#: The flags defined for PUSH_PROMISE frames.
|
||||
defined_flags = [
|
||||
Flag('END_HEADERS', 0x04),
|
||||
Flag('PADDED', 0x08)
|
||||
]
|
||||
|
||||
#: The type byte defined for PUSH_PROMISE frames.
|
||||
type = 0x05
|
||||
|
||||
stream_association = _STREAM_ASSOC_HAS_STREAM
|
||||
|
||||
def __init__(self, stream_id: int, promised_stream_id: int = 0, data: bytes = b'', **kwargs: Any) -> None:
|
||||
super().__init__(stream_id, **kwargs)
|
||||
|
||||
#: The stream ID that is promised by this frame.
|
||||
self.promised_stream_id = promised_stream_id
|
||||
|
||||
#: The HPACK-encoded header block for the simulated request on the new
|
||||
#: stream.
|
||||
self.data = data
|
||||
|
||||
def _body_repr(self) -> str:
|
||||
return "promised_stream_id={}, data={}".format(
|
||||
self.promised_stream_id,
|
||||
_raw_data_repr(self.data),
|
||||
)
|
||||
|
||||
def serialize_body(self) -> bytes:
|
||||
padding_data = self.serialize_padding_data()
|
||||
padding = b'\0' * self.pad_length
|
||||
data = _STRUCT_L.pack(self.promised_stream_id)
|
||||
return b''.join([padding_data, data, self.data, padding])
|
||||
|
||||
def parse_body(self, data: memoryview) -> None:
|
||||
padding_data_length = self.parse_padding_data(data)
|
||||
|
||||
try:
|
||||
self.promised_stream_id = _STRUCT_L.unpack(
|
||||
data[padding_data_length:padding_data_length + 4]
|
||||
)[0]
|
||||
except struct.error:
|
||||
raise InvalidFrameError("Invalid PUSH_PROMISE body")
|
||||
|
||||
self.data = (
|
||||
data[padding_data_length + 4:len(data)-self.pad_length].tobytes()
|
||||
)
|
||||
self.body_len = len(data)
|
||||
|
||||
if self.promised_stream_id == 0 or self.promised_stream_id % 2 != 0:
|
||||
raise InvalidDataError(
|
||||
"Invalid PUSH_PROMISE promised stream id: %s" %
|
||||
self.promised_stream_id
|
||||
)
|
||||
|
||||
if self.pad_length and self.pad_length >= self.body_len:
|
||||
raise InvalidPaddingError("Padding is too long.")
|
||||
|
||||
|
||||
class PingFrame(Frame):
|
||||
"""
|
||||
The PING frame is a mechanism for measuring a minimal round-trip time from
|
||||
the sender, as well as determining whether an idle connection is still
|
||||
functional. PING frames can be sent from any endpoint.
|
||||
"""
|
||||
#: The flags defined for PING frames.
|
||||
defined_flags = [Flag('ACK', 0x01)]
|
||||
|
||||
#: The type byte defined for PING frames.
|
||||
type = 0x06
|
||||
|
||||
stream_association = _STREAM_ASSOC_NO_STREAM
|
||||
|
||||
def __init__(self, stream_id: int = 0, opaque_data: bytes = b'', **kwargs: Any) -> None:
|
||||
super().__init__(stream_id, **kwargs)
|
||||
|
||||
#: The opaque data sent in this PING frame, as a bytestring.
|
||||
self.opaque_data = opaque_data
|
||||
|
||||
def _body_repr(self) -> str:
|
||||
return "opaque_data={!r}".format(
|
||||
self.opaque_data,
|
||||
)
|
||||
|
||||
def serialize_body(self) -> bytes:
|
||||
if len(self.opaque_data) > 8:
|
||||
raise InvalidFrameError(
|
||||
"PING frame may not have more than 8 bytes of data, got %r" %
|
||||
self.opaque_data
|
||||
)
|
||||
|
||||
data = self.opaque_data
|
||||
data += b'\x00' * (8 - len(self.opaque_data))
|
||||
return data
|
||||
|
||||
def parse_body(self, data: memoryview) -> None:
|
||||
if len(data) != 8:
|
||||
raise InvalidFrameError(
|
||||
"PING frame must have 8 byte length: got %s" % len(data)
|
||||
)
|
||||
|
||||
self.opaque_data = data.tobytes()
|
||||
self.body_len = 8
|
||||
|
||||
|
||||
class GoAwayFrame(Frame):
|
||||
"""
|
||||
The GOAWAY frame informs the remote peer to stop creating streams on this
|
||||
connection. It can be sent from the client or the server. Once sent, the
|
||||
sender will ignore frames sent on new streams for the remainder of the
|
||||
connection.
|
||||
"""
|
||||
#: The flags defined for GOAWAY frames.
|
||||
defined_flags: List[Flag] = []
|
||||
|
||||
#: The type byte defined for GOAWAY frames.
|
||||
type = 0x07
|
||||
|
||||
stream_association = _STREAM_ASSOC_NO_STREAM
|
||||
|
||||
def __init__(self,
|
||||
stream_id: int = 0,
|
||||
last_stream_id: int = 0,
|
||||
error_code: int = 0,
|
||||
additional_data: bytes = b'',
|
||||
**kwargs: Any) -> None:
|
||||
super().__init__(stream_id, **kwargs)
|
||||
|
||||
#: The last stream ID definitely seen by the remote peer.
|
||||
self.last_stream_id = last_stream_id
|
||||
|
||||
#: The error code for connection teardown.
|
||||
self.error_code = error_code
|
||||
|
||||
#: Any additional data sent in the GOAWAY.
|
||||
self.additional_data = additional_data
|
||||
|
||||
def _body_repr(self) -> str:
|
||||
return "last_stream_id={}, error_code={}, additional_data={!r}".format(
|
||||
self.last_stream_id,
|
||||
self.error_code,
|
||||
self.additional_data,
|
||||
)
|
||||
|
||||
def serialize_body(self) -> bytes:
|
||||
data = _STRUCT_LL.pack(
|
||||
self.last_stream_id & 0x7FFFFFFF,
|
||||
self.error_code
|
||||
)
|
||||
data += self.additional_data
|
||||
|
||||
return data
|
||||
|
||||
def parse_body(self, data: memoryview) -> None:
|
||||
try:
|
||||
self.last_stream_id, self.error_code = _STRUCT_LL.unpack(
|
||||
data[:8]
|
||||
)
|
||||
except struct.error:
|
||||
raise InvalidFrameError("Invalid GOAWAY body.")
|
||||
|
||||
self.body_len = len(data)
|
||||
|
||||
if len(data) > 8:
|
||||
self.additional_data = data[8:].tobytes()
|
||||
|
||||
|
||||
class WindowUpdateFrame(Frame):
|
||||
"""
|
||||
The WINDOW_UPDATE frame is used to implement flow control.
|
||||
|
||||
Flow control operates at two levels: on each individual stream and on the
|
||||
entire connection.
|
||||
|
||||
Both types of flow control are hop by hop; that is, only between the two
|
||||
endpoints. Intermediaries do not forward WINDOW_UPDATE frames between
|
||||
dependent connections. However, throttling of data transfer by any receiver
|
||||
can indirectly cause the propagation of flow control information toward the
|
||||
original sender.
|
||||
"""
|
||||
#: The flags defined for WINDOW_UPDATE frames.
|
||||
defined_flags: List[Flag] = []
|
||||
|
||||
#: The type byte defined for WINDOW_UPDATE frames.
|
||||
type = 0x08
|
||||
|
||||
stream_association = _STREAM_ASSOC_EITHER
|
||||
|
||||
def __init__(self, stream_id: int, window_increment: int = 0, **kwargs: Any) -> None:
|
||||
super().__init__(stream_id, **kwargs)
|
||||
|
||||
#: The amount the flow control window is to be incremented.
|
||||
self.window_increment = window_increment
|
||||
|
||||
def _body_repr(self) -> str:
|
||||
return "window_increment={}".format(
|
||||
self.window_increment,
|
||||
)
|
||||
|
||||
def serialize_body(self) -> bytes:
|
||||
return _STRUCT_L.pack(self.window_increment & 0x7FFFFFFF)
|
||||
|
||||
def parse_body(self, data: memoryview) -> None:
|
||||
if len(data) > 4:
|
||||
raise InvalidFrameError(
|
||||
"WINDOW_UPDATE frame must have 4 byte length: got %s" %
|
||||
len(data)
|
||||
)
|
||||
|
||||
try:
|
||||
self.window_increment = _STRUCT_L.unpack(data)[0]
|
||||
except struct.error:
|
||||
raise InvalidFrameError("Invalid WINDOW_UPDATE body")
|
||||
|
||||
if not 1 <= self.window_increment <= 2**31-1:
|
||||
raise InvalidDataError(
|
||||
"WINDOW_UPDATE increment must be between 1 to 2^31-1"
|
||||
)
|
||||
|
||||
self.body_len = 4
|
||||
|
||||
|
||||
class HeadersFrame(Padding, Priority, Frame):
|
||||
"""
|
||||
The HEADERS frame carries name-value pairs. It is used to open a stream.
|
||||
HEADERS frames can be sent on a stream in the "open" or "half closed
|
||||
(remote)" states.
|
||||
|
||||
The HeadersFrame class is actually basically a data frame in this
|
||||
implementation, because of the requirement to control the sizes of frames.
|
||||
A header block fragment that doesn't fit in an entire HEADERS frame needs
|
||||
to be followed with CONTINUATION frames. From the perspective of the frame
|
||||
building code the header block is an opaque data segment.
|
||||
"""
|
||||
#: The flags defined for HEADERS frames.
|
||||
defined_flags = [
|
||||
Flag('END_STREAM', 0x01),
|
||||
Flag('END_HEADERS', 0x04),
|
||||
Flag('PADDED', 0x08),
|
||||
Flag('PRIORITY', 0x20),
|
||||
]
|
||||
|
||||
#: The type byte defined for HEADERS frames.
|
||||
type = 0x01
|
||||
|
||||
stream_association = _STREAM_ASSOC_HAS_STREAM
|
||||
|
||||
def __init__(self, stream_id: int, data: bytes = b'', **kwargs: Any) -> None:
|
||||
super().__init__(stream_id, **kwargs)
|
||||
|
||||
#: The HPACK-encoded header block.
|
||||
self.data = data
|
||||
|
||||
def _body_repr(self) -> str:
|
||||
return "exclusive={}, depends_on={}, stream_weight={}, data={}".format(
|
||||
self.exclusive,
|
||||
self.depends_on,
|
||||
self.stream_weight,
|
||||
_raw_data_repr(self.data),
|
||||
)
|
||||
|
||||
def serialize_body(self) -> bytes:
|
||||
padding_data = self.serialize_padding_data()
|
||||
padding = b'\0' * self.pad_length
|
||||
|
||||
if 'PRIORITY' in self.flags:
|
||||
priority_data = self.serialize_priority_data()
|
||||
else:
|
||||
priority_data = b''
|
||||
|
||||
return b''.join([padding_data, priority_data, self.data, padding])
|
||||
|
||||
def parse_body(self, data: memoryview) -> None:
|
||||
padding_data_length = self.parse_padding_data(data)
|
||||
data = data[padding_data_length:]
|
||||
|
||||
if 'PRIORITY' in self.flags:
|
||||
priority_data_length = self.parse_priority_data(data)
|
||||
else:
|
||||
priority_data_length = 0
|
||||
|
||||
self.body_len = len(data)
|
||||
self.data = (
|
||||
data[priority_data_length:len(data)-self.pad_length].tobytes()
|
||||
)
|
||||
|
||||
if self.pad_length and self.pad_length >= self.body_len:
|
||||
raise InvalidPaddingError("Padding is too long.")
|
||||
|
||||
|
||||
class ContinuationFrame(Frame):
|
||||
"""
|
||||
The CONTINUATION frame is used to continue a sequence of header block
|
||||
fragments. Any number of CONTINUATION frames can be sent on an existing
|
||||
stream, as long as the preceding frame on the same stream is one of
|
||||
HEADERS, PUSH_PROMISE or CONTINUATION without the END_HEADERS flag set.
|
||||
|
||||
Much like the HEADERS frame, hyper treats this as an opaque data frame with
|
||||
different flags and a different type.
|
||||
"""
|
||||
#: The flags defined for CONTINUATION frames.
|
||||
defined_flags = [Flag('END_HEADERS', 0x04)]
|
||||
|
||||
#: The type byte defined for CONTINUATION frames.
|
||||
type = 0x09
|
||||
|
||||
stream_association = _STREAM_ASSOC_HAS_STREAM
|
||||
|
||||
def __init__(self, stream_id: int, data: bytes = b'', **kwargs: Any) -> None:
|
||||
super().__init__(stream_id, **kwargs)
|
||||
|
||||
#: The HPACK-encoded header block.
|
||||
self.data = data
|
||||
|
||||
def _body_repr(self) -> str:
|
||||
return "data={}".format(
|
||||
_raw_data_repr(self.data),
|
||||
)
|
||||
|
||||
def serialize_body(self) -> bytes:
|
||||
return self.data
|
||||
|
||||
def parse_body(self, data: memoryview) -> None:
|
||||
self.data = data.tobytes()
|
||||
self.body_len = len(data)
|
||||
|
||||
|
||||
class AltSvcFrame(Frame):
|
||||
"""
|
||||
The ALTSVC frame is used to advertise alternate services that the current
|
||||
host, or a different one, can understand. This frame is standardised as
|
||||
part of RFC 7838.
|
||||
|
||||
This frame does no work to validate that the ALTSVC field parameter is
|
||||
acceptable per the rules of RFC 7838.
|
||||
|
||||
.. note:: If the ``stream_id`` of this frame is nonzero, the origin field
|
||||
must have zero length. Conversely, if the ``stream_id`` of this
|
||||
frame is zero, the origin field must have nonzero length. Put
|
||||
another way, a valid ALTSVC frame has ``stream_id != 0`` XOR
|
||||
``len(origin) != 0``.
|
||||
"""
|
||||
type = 0xA
|
||||
|
||||
stream_association = _STREAM_ASSOC_EITHER
|
||||
|
||||
def __init__(self, stream_id: int, origin: bytes = b'', field: bytes = b'', **kwargs: Any) -> None:
|
||||
super().__init__(stream_id, **kwargs)
|
||||
|
||||
if not isinstance(origin, bytes):
|
||||
raise InvalidDataError("AltSvc origin must be bytestring.")
|
||||
if not isinstance(field, bytes):
|
||||
raise InvalidDataError("AltSvc field must be a bytestring.")
|
||||
self.origin = origin
|
||||
self.field = field
|
||||
|
||||
def _body_repr(self) -> str:
|
||||
return "origin={!r}, field={!r}".format(
|
||||
self.origin,
|
||||
self.field,
|
||||
)
|
||||
|
||||
def serialize_body(self) -> bytes:
|
||||
origin_len = _STRUCT_H.pack(len(self.origin))
|
||||
return b''.join([origin_len, self.origin, self.field])
|
||||
|
||||
def parse_body(self, data: memoryview) -> None:
|
||||
try:
|
||||
origin_len = _STRUCT_H.unpack(data[0:2])[0]
|
||||
self.origin = data[2:2+origin_len].tobytes()
|
||||
|
||||
if len(self.origin) != origin_len:
|
||||
raise InvalidFrameError("Invalid ALTSVC frame body.")
|
||||
|
||||
self.field = data[2+origin_len:].tobytes()
|
||||
except (struct.error, ValueError):
|
||||
raise InvalidFrameError("Invalid ALTSVC frame body.")
|
||||
|
||||
self.body_len = len(data)
|
||||
|
||||
|
||||
class ExtensionFrame(Frame):
|
||||
"""
|
||||
ExtensionFrame is used to wrap frames which are not natively interpretable
|
||||
by hyperframe.
|
||||
|
||||
Although certain byte prefixes are ordained by specification to have
|
||||
certain contextual meanings, frames with other prefixes are not prohibited,
|
||||
and may be used to communicate arbitrary meaning between HTTP/2 peers.
|
||||
|
||||
Thus, hyperframe, rather than raising an exception when such a frame is
|
||||
encountered, wraps it in a generic frame to be properly acted upon by
|
||||
upstream consumers which might have additional context on how to use it.
|
||||
|
||||
.. versionadded:: 5.0.0
|
||||
"""
|
||||
|
||||
stream_association = _STREAM_ASSOC_EITHER
|
||||
|
||||
def __init__(self, type: int, stream_id: int, flag_byte: int = 0x0, body: bytes = b'', **kwargs: Any) -> None:
|
||||
super().__init__(stream_id, **kwargs)
|
||||
self.type = type
|
||||
self.flag_byte = flag_byte
|
||||
self.body = body
|
||||
|
||||
def _body_repr(self) -> str:
|
||||
return "type={}, flag_byte={}, body={}".format(
|
||||
self.type,
|
||||
self.flag_byte,
|
||||
_raw_data_repr(self.body),
|
||||
)
|
||||
|
||||
def parse_flags(self, flag_byte: int) -> None: # type: ignore
|
||||
"""
|
||||
For extension frames, we parse the flags by just storing a flag byte.
|
||||
"""
|
||||
self.flag_byte = flag_byte
|
||||
|
||||
def parse_body(self, data: memoryview) -> None:
|
||||
self.body = data.tobytes()
|
||||
self.body_len = len(data)
|
||||
|
||||
def serialize(self) -> bytes:
|
||||
"""
|
||||
A broad override of the serialize method that ensures that the data
|
||||
comes back out exactly as it came in. This should not be used in most
|
||||
user code: it exists only as a helper method if frames need to be
|
||||
reconstituted.
|
||||
"""
|
||||
# Build the frame header.
|
||||
# First, get the flags.
|
||||
flags = self.flag_byte
|
||||
|
||||
header = _STRUCT_HBBBL.pack(
|
||||
(self.body_len >> 8) & 0xFFFF, # Length spread over top 24 bits
|
||||
self.body_len & 0xFF,
|
||||
self.type,
|
||||
flags,
|
||||
self.stream_id & 0x7FFFFFFF # Stream ID is 32 bits.
|
||||
)
|
||||
|
||||
return header + self.body
|
||||
|
||||
|
||||
def _raw_data_repr(data: Optional[bytes]) -> str:
|
||||
if not data:
|
||||
return "None"
|
||||
r = binascii.hexlify(data).decode('ascii')
|
||||
if len(r) > 20:
|
||||
r = r[:20] + "..."
|
||||
return "<hex:" + r + ">"
|
||||
|
||||
|
||||
_FRAME_CLASSES: List[Type[Frame]] = [
|
||||
DataFrame,
|
||||
HeadersFrame,
|
||||
PriorityFrame,
|
||||
RstStreamFrame,
|
||||
SettingsFrame,
|
||||
PushPromiseFrame,
|
||||
PingFrame,
|
||||
GoAwayFrame,
|
||||
WindowUpdateFrame,
|
||||
ContinuationFrame,
|
||||
AltSvcFrame,
|
||||
]
|
||||
#: FRAMES maps the type byte for each frame to the class used to represent that
|
||||
#: frame.
|
||||
FRAMES = {cls.type: cls for cls in _FRAME_CLASSES}
|
||||
0
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/hyperframe/src/hyperframe/py.typed
vendored
Normal file
0
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/hyperframe/src/hyperframe/py.typed
vendored
Normal file
172
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/pywebsocket3/pywebsocket3/__init__.py
vendored
Normal file
172
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/pywebsocket3/pywebsocket3/__init__.py
vendored
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
# Copyright 2011, Google Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following disclaimer
|
||||
# in the documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
# * Neither the name of Google Inc. nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
""" A Standalone WebSocket Server for testing purposes
|
||||
|
||||
pywebsocket3 is an API that provides WebSocket functionalities with
|
||||
a standalone WebSocket server. It is intended for testing or
|
||||
experimental purposes.
|
||||
|
||||
Installation
|
||||
============
|
||||
1. Follow standalone server documentation to start running the
|
||||
standalone server. It can be read by running the following command:
|
||||
|
||||
$ pydoc pywebsocket3.standalone
|
||||
|
||||
2. Once the standalone server is launched verify it by accessing
|
||||
http://localhost[:port]/console.html. Include the port number when
|
||||
specified on launch. If everything is working correctly, you
|
||||
will see a simple echo console.
|
||||
|
||||
|
||||
Writing WebSocket handlers
|
||||
==========================
|
||||
|
||||
When a WebSocket request comes in, the resource name
|
||||
specified in the handshake is considered as if it is a file path under
|
||||
<websock_handlers> and the handler defined in
|
||||
<websock_handlers>/<resource_name>_wsh.py is invoked.
|
||||
|
||||
For example, if the resource name is /example/chat, the handler defined in
|
||||
<websock_handlers>/example/chat_wsh.py is invoked.
|
||||
|
||||
A WebSocket handler is composed of the following three functions:
|
||||
|
||||
web_socket_do_extra_handshake(request)
|
||||
web_socket_transfer_data(request)
|
||||
web_socket_passive_closing_handshake(request)
|
||||
|
||||
where:
|
||||
request: mod_python request.
|
||||
|
||||
web_socket_do_extra_handshake is called during the handshake after the
|
||||
headers are successfully parsed and WebSocket properties (ws_origin,
|
||||
and ws_resource) are added to request. A handler
|
||||
can reject the request by raising an exception.
|
||||
|
||||
A request object has the following properties that you can use during the
|
||||
extra handshake (web_socket_do_extra_handshake):
|
||||
- ws_resource
|
||||
- ws_origin
|
||||
- ws_version
|
||||
- ws_extensions
|
||||
- ws_deflate
|
||||
- ws_protocol
|
||||
- ws_requested_protocols
|
||||
|
||||
The last two are a bit tricky. See the next subsection.
|
||||
|
||||
|
||||
Subprotocol Negotiation
|
||||
-----------------------
|
||||
|
||||
ws_protocol is always set to None when
|
||||
web_socket_do_extra_handshake is called. If ws_requested_protocols is not
|
||||
None, you must choose one subprotocol from this list and set it to
|
||||
ws_protocol.
|
||||
|
||||
Data Transfer
|
||||
-------------
|
||||
|
||||
web_socket_transfer_data is called after the handshake completed
|
||||
successfully. A handler can receive/send messages from/to the client
|
||||
using request. pywebsocket3.msgutil module provides utilities
|
||||
for data transfer.
|
||||
|
||||
You can receive a message by the following statement.
|
||||
|
||||
message = request.ws_stream.receive_message()
|
||||
|
||||
This call blocks until any complete text frame arrives, and the payload data
|
||||
of the incoming frame will be stored into message. When you're using IETF
|
||||
HyBi 00 or later protocol, receive_message() will return None on receiving
|
||||
client-initiated closing handshake. When any error occurs, receive_message()
|
||||
will raise some exception.
|
||||
|
||||
You can send a message by the following statement.
|
||||
|
||||
request.ws_stream.send_message(message)
|
||||
|
||||
|
||||
Closing Connection
|
||||
------------------
|
||||
|
||||
Executing the following statement or just return-ing from
|
||||
web_socket_transfer_data cause connection close.
|
||||
|
||||
request.ws_stream.close_connection()
|
||||
|
||||
close_connection will wait
|
||||
for closing handshake acknowledgement coming from the client. When it
|
||||
couldn't receive a valid acknowledgement, raises an exception.
|
||||
|
||||
web_socket_passive_closing_handshake is called after the server receives
|
||||
incoming closing frame from the client peer immediately. You can specify
|
||||
code and reason by return values. They are sent as a outgoing closing frame
|
||||
from the server. A request object has the following properties that you can
|
||||
use in web_socket_passive_closing_handshake.
|
||||
- ws_close_code
|
||||
- ws_close_reason
|
||||
|
||||
|
||||
Threading
|
||||
---------
|
||||
|
||||
A WebSocket handler must be thread-safe. The standalone
|
||||
server uses threads by default.
|
||||
|
||||
|
||||
Configuring WebSocket Extension Processors
|
||||
------------------------------------------
|
||||
|
||||
See extensions.py for supported WebSocket extensions. Note that they are
|
||||
unstable and their APIs are subject to change substantially.
|
||||
|
||||
A request object has these extension processing related attributes.
|
||||
|
||||
- ws_requested_extensions:
|
||||
|
||||
A list of common.ExtensionParameter instances representing extension
|
||||
parameters received from the client in the client's opening handshake.
|
||||
You shouldn't modify it manually.
|
||||
|
||||
- ws_extensions:
|
||||
|
||||
A list of common.ExtensionParameter instances representing extension
|
||||
parameters to send back to the client in the server's opening handshake.
|
||||
You shouldn't touch it directly. Instead, call methods on extension
|
||||
processors.
|
||||
|
||||
- ws_extension_processors:
|
||||
|
||||
A list of loaded extension processors. Find the processor for the
|
||||
extension you want to configure from it, and call its methods.
|
||||
"""
|
||||
|
||||
# vi:sts=4 sw=4 et tw=72
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
# Copyright 2020, Google Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following disclaimer
|
||||
# in the documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
# * Neither the name of Google Inc. nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
"""Stream Exceptions.
|
||||
"""
|
||||
|
||||
# Note: request.connection.write/read are used in this module, even though
|
||||
# mod_python document says that they should be used only in connection
|
||||
# handlers. Unfortunately, we have no other options. For example,
|
||||
# request.write/read are not suitable because they don't allow direct raw bytes
|
||||
# writing/reading.
|
||||
|
||||
|
||||
# Exceptions
|
||||
class ConnectionTerminatedException(Exception):
|
||||
"""This exception will be raised when a connection is terminated
|
||||
unexpectedly.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class InvalidFrameException(ConnectionTerminatedException):
|
||||
"""This exception will be raised when we received an invalid frame we
|
||||
cannot parse.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class BadOperationException(Exception):
|
||||
"""This exception will be raised when send_message() is called on
|
||||
server-terminated connection or receive_message() is called on
|
||||
client-terminated connection.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class UnsupportedFrameException(Exception):
|
||||
"""This exception will be raised when we receive a frame with flag, opcode
|
||||
we cannot handle. Handlers can just catch and ignore this exception and
|
||||
call receive_message() again to continue processing the next frame.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class InvalidUTF8Exception(Exception):
|
||||
"""This exception will be raised when we receive a text frame which
|
||||
contains invalid UTF-8 strings.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
||||
275
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/pywebsocket3/pywebsocket3/common.py
vendored
Normal file
275
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/pywebsocket3/pywebsocket3/common.py
vendored
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
# Copyright 2012, Google Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following disclaimer
|
||||
# in the documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
# * Neither the name of Google Inc. nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
"""This file must not depend on any module specific to the WebSocket protocol.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from pywebsocket3 import http_header_util
|
||||
|
||||
|
||||
# Additional log level definitions.
|
||||
LOGLEVEL_FINE = 9
|
||||
|
||||
# Constants indicating WebSocket protocol version.
|
||||
VERSION_HYBI13 = 13
|
||||
VERSION_HYBI14 = 13
|
||||
VERSION_HYBI15 = 13
|
||||
VERSION_HYBI16 = 13
|
||||
VERSION_HYBI17 = 13
|
||||
|
||||
# Constants indicating WebSocket protocol latest version.
|
||||
VERSION_HYBI_LATEST = VERSION_HYBI13
|
||||
|
||||
# Port numbers
|
||||
DEFAULT_WEB_SOCKET_PORT = 80
|
||||
DEFAULT_WEB_SOCKET_SECURE_PORT = 443
|
||||
|
||||
# Schemes
|
||||
WEB_SOCKET_SCHEME = 'ws'
|
||||
WEB_SOCKET_SECURE_SCHEME = 'wss'
|
||||
|
||||
# Frame opcodes defined in the spec.
|
||||
OPCODE_CONTINUATION = 0x0
|
||||
OPCODE_TEXT = 0x1
|
||||
OPCODE_BINARY = 0x2
|
||||
OPCODE_CLOSE = 0x8
|
||||
OPCODE_PING = 0x9
|
||||
OPCODE_PONG = 0xa
|
||||
|
||||
# UUID for the opening handshake and frame masking.
|
||||
WEBSOCKET_ACCEPT_UUID = b'258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
|
||||
|
||||
# Opening handshake header names and expected values.
|
||||
UPGRADE_HEADER = 'Upgrade'
|
||||
WEBSOCKET_UPGRADE_TYPE = 'websocket'
|
||||
CONNECTION_HEADER = 'Connection'
|
||||
UPGRADE_CONNECTION_TYPE = 'Upgrade'
|
||||
HOST_HEADER = 'Host'
|
||||
ORIGIN_HEADER = 'Origin'
|
||||
SEC_WEBSOCKET_KEY_HEADER = 'Sec-WebSocket-Key'
|
||||
SEC_WEBSOCKET_ACCEPT_HEADER = 'Sec-WebSocket-Accept'
|
||||
SEC_WEBSOCKET_VERSION_HEADER = 'Sec-WebSocket-Version'
|
||||
SEC_WEBSOCKET_PROTOCOL_HEADER = 'Sec-WebSocket-Protocol'
|
||||
SEC_WEBSOCKET_EXTENSIONS_HEADER = 'Sec-WebSocket-Extensions'
|
||||
|
||||
# Extensions
|
||||
PERMESSAGE_DEFLATE_EXTENSION = 'permessage-deflate'
|
||||
|
||||
# Status codes
|
||||
# Code STATUS_NO_STATUS_RECEIVED, STATUS_ABNORMAL_CLOSURE, and
|
||||
# STATUS_TLS_HANDSHAKE are pseudo codes to indicate specific error cases.
|
||||
# Could not be used for codes in actual closing frames.
|
||||
# Application level errors must use codes in the range
|
||||
# STATUS_USER_REGISTERED_BASE to STATUS_USER_PRIVATE_MAX. The codes in the
|
||||
# range STATUS_USER_REGISTERED_BASE to STATUS_USER_REGISTERED_MAX are managed
|
||||
# by IANA. Usually application must define user protocol level errors in the
|
||||
# range STATUS_USER_PRIVATE_BASE to STATUS_USER_PRIVATE_MAX.
|
||||
STATUS_NORMAL_CLOSURE = 1000
|
||||
STATUS_GOING_AWAY = 1001
|
||||
STATUS_PROTOCOL_ERROR = 1002
|
||||
STATUS_UNSUPPORTED_DATA = 1003
|
||||
STATUS_NO_STATUS_RECEIVED = 1005
|
||||
STATUS_ABNORMAL_CLOSURE = 1006
|
||||
STATUS_INVALID_FRAME_PAYLOAD_DATA = 1007
|
||||
STATUS_POLICY_VIOLATION = 1008
|
||||
STATUS_MESSAGE_TOO_BIG = 1009
|
||||
STATUS_MANDATORY_EXTENSION = 1010
|
||||
STATUS_INTERNAL_ENDPOINT_ERROR = 1011
|
||||
STATUS_TLS_HANDSHAKE = 1015
|
||||
STATUS_USER_REGISTERED_BASE = 3000
|
||||
STATUS_USER_REGISTERED_MAX = 3999
|
||||
STATUS_USER_PRIVATE_BASE = 4000
|
||||
STATUS_USER_PRIVATE_MAX = 4999
|
||||
# Following definitions are aliases to keep compatibility. Applications must
|
||||
# not use these obsoleted definitions anymore.
|
||||
STATUS_NORMAL = STATUS_NORMAL_CLOSURE
|
||||
STATUS_UNSUPPORTED = STATUS_UNSUPPORTED_DATA
|
||||
STATUS_CODE_NOT_AVAILABLE = STATUS_NO_STATUS_RECEIVED
|
||||
STATUS_ABNORMAL_CLOSE = STATUS_ABNORMAL_CLOSURE
|
||||
STATUS_INVALID_FRAME_PAYLOAD = STATUS_INVALID_FRAME_PAYLOAD_DATA
|
||||
STATUS_MANDATORY_EXT = STATUS_MANDATORY_EXTENSION
|
||||
|
||||
# HTTP status codes
|
||||
HTTP_STATUS_BAD_REQUEST = 400
|
||||
HTTP_STATUS_FORBIDDEN = 403
|
||||
HTTP_STATUS_NOT_FOUND = 404
|
||||
|
||||
|
||||
def is_control_opcode(opcode):
|
||||
return (opcode >> 3) == 1
|
||||
|
||||
|
||||
class ExtensionParameter(object):
|
||||
"""This is exchanged on extension negotiation in opening handshake."""
|
||||
def __init__(self, name):
|
||||
self._name = name
|
||||
# TODO(tyoshino): Change the data structure to more efficient one such
|
||||
# as dict when the spec changes to say like
|
||||
# - Parameter names must be unique
|
||||
# - The order of parameters is not significant
|
||||
self._parameters = []
|
||||
|
||||
def name(self):
|
||||
"""Return the extension name."""
|
||||
return self._name
|
||||
|
||||
def add_parameter(self, name, value):
|
||||
"""Add a parameter."""
|
||||
self._parameters.append((name, value))
|
||||
|
||||
def get_parameters(self):
|
||||
"""Return the parameters."""
|
||||
return self._parameters
|
||||
|
||||
def get_parameter_names(self):
|
||||
"""Return the names of the parameters."""
|
||||
return [name for name, unused_value in self._parameters]
|
||||
|
||||
def has_parameter(self, name):
|
||||
"""Test if a parameter exists."""
|
||||
for param_name, param_value in self._parameters:
|
||||
if param_name == name:
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_parameter_value(self, name):
|
||||
"""Get the value of a specific parameter."""
|
||||
for param_name, param_value in self._parameters:
|
||||
if param_name == name:
|
||||
return param_value
|
||||
|
||||
|
||||
class ExtensionParsingException(Exception):
|
||||
"""Exception to handle errors in extension parsing."""
|
||||
def __init__(self, name):
|
||||
super(ExtensionParsingException, self).__init__(name)
|
||||
|
||||
|
||||
def _parse_extension_param(state, definition):
|
||||
param_name = http_header_util.consume_token(state)
|
||||
|
||||
if param_name is None:
|
||||
raise ExtensionParsingException('No valid parameter name found')
|
||||
|
||||
http_header_util.consume_lwses(state)
|
||||
|
||||
if not http_header_util.consume_string(state, '='):
|
||||
definition.add_parameter(param_name, None)
|
||||
return
|
||||
|
||||
http_header_util.consume_lwses(state)
|
||||
|
||||
# TODO(tyoshino): Add code to validate that parsed param_value is token
|
||||
param_value = http_header_util.consume_token_or_quoted_string(state)
|
||||
if param_value is None:
|
||||
raise ExtensionParsingException(
|
||||
'No valid parameter value found on the right-hand side of '
|
||||
'parameter %r' % param_name)
|
||||
|
||||
definition.add_parameter(param_name, param_value)
|
||||
|
||||
|
||||
def _parse_extension(state):
|
||||
extension_token = http_header_util.consume_token(state)
|
||||
if extension_token is None:
|
||||
return None
|
||||
|
||||
extension = ExtensionParameter(extension_token)
|
||||
|
||||
while True:
|
||||
http_header_util.consume_lwses(state)
|
||||
|
||||
if not http_header_util.consume_string(state, ';'):
|
||||
break
|
||||
|
||||
http_header_util.consume_lwses(state)
|
||||
|
||||
try:
|
||||
_parse_extension_param(state, extension)
|
||||
except ExtensionParsingException as e:
|
||||
raise ExtensionParsingException(
|
||||
'Failed to parse parameter for %r (%r)' % (extension_token, e))
|
||||
|
||||
return extension
|
||||
|
||||
|
||||
def parse_extensions(data):
|
||||
"""Parse Sec-WebSocket-Extensions header value.
|
||||
|
||||
Returns a list of ExtensionParameter objects.
|
||||
Leading LWSes must be trimmed.
|
||||
"""
|
||||
state = http_header_util.ParsingState(data)
|
||||
|
||||
extension_list = []
|
||||
while True:
|
||||
extension = _parse_extension(state)
|
||||
if extension is not None:
|
||||
extension_list.append(extension)
|
||||
|
||||
http_header_util.consume_lwses(state)
|
||||
|
||||
if http_header_util.peek(state) is None:
|
||||
break
|
||||
|
||||
if not http_header_util.consume_string(state, ','):
|
||||
raise ExtensionParsingException(
|
||||
'Failed to parse Sec-WebSocket-Extensions header: '
|
||||
'Expected a comma but found %r' % http_header_util.peek(state))
|
||||
|
||||
http_header_util.consume_lwses(state)
|
||||
|
||||
if len(extension_list) == 0:
|
||||
raise ExtensionParsingException('No valid extension entry found')
|
||||
|
||||
return extension_list
|
||||
|
||||
|
||||
def format_extension(extension):
|
||||
"""Format an ExtensionParameter object."""
|
||||
formatted_params = [extension.name()]
|
||||
for param_name, param_value in extension.get_parameters():
|
||||
if param_value is None:
|
||||
formatted_params.append(param_name)
|
||||
else:
|
||||
quoted_value = http_header_util.quote_if_necessary(param_value)
|
||||
formatted_params.append('%s=%s' % (param_name, quoted_value))
|
||||
return '; '.join(formatted_params)
|
||||
|
||||
|
||||
def format_extensions(extension_list):
|
||||
"""Format a list of ExtensionParameter objects."""
|
||||
formatted_extension_list = []
|
||||
for extension in extension_list:
|
||||
formatted_extension_list.append(format_extension(extension))
|
||||
return ', '.join(formatted_extension_list)
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
||||
391
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/pywebsocket3/pywebsocket3/dispatch.py
vendored
Normal file
391
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/pywebsocket3/pywebsocket3/dispatch.py
vendored
Normal file
|
|
@ -0,0 +1,391 @@
|
|||
# Copyright 2012, Google Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following disclaimer
|
||||
# in the documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
# * Neither the name of Google Inc. nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
"""Dispatch WebSocket request.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import traceback
|
||||
|
||||
from pywebsocket3 import (
|
||||
common,
|
||||
handshake,
|
||||
msgutil,
|
||||
stream,
|
||||
util
|
||||
)
|
||||
|
||||
_SOURCE_PATH_PATTERN = re.compile(r'(?i)_wsh\.py$')
|
||||
_SOURCE_SUFFIX = '_wsh.py'
|
||||
_DO_EXTRA_HANDSHAKE_HANDLER_NAME = 'web_socket_do_extra_handshake'
|
||||
_TRANSFER_DATA_HANDLER_NAME = 'web_socket_transfer_data'
|
||||
_PASSIVE_CLOSING_HANDSHAKE_HANDLER_NAME = (
|
||||
'web_socket_passive_closing_handshake')
|
||||
|
||||
|
||||
class DispatchException(Exception):
|
||||
"""Exception in dispatching WebSocket request."""
|
||||
def __init__(self, name, status=common.HTTP_STATUS_NOT_FOUND):
|
||||
super(DispatchException, self).__init__(name)
|
||||
self.status = status
|
||||
|
||||
|
||||
def _default_passive_closing_handshake_handler(request):
|
||||
"""Default web_socket_passive_closing_handshake handler."""
|
||||
|
||||
return common.STATUS_NORMAL_CLOSURE, ''
|
||||
|
||||
|
||||
def _normalize_path(path):
|
||||
"""Normalize path.
|
||||
|
||||
Args:
|
||||
path: the path to normalize.
|
||||
|
||||
Path is converted to the absolute path.
|
||||
The input path can use either '\\' or '/' as the separator.
|
||||
The normalized path always uses '/' regardless of the platform.
|
||||
"""
|
||||
|
||||
path = path.replace('\\', os.path.sep)
|
||||
path = os.path.realpath(path)
|
||||
path = path.replace('\\', '/')
|
||||
return path
|
||||
|
||||
|
||||
def _create_path_to_resource_converter(base_dir):
|
||||
"""Returns a function that converts the path of a WebSocket handler source
|
||||
file to a resource string by removing the path to the base directory from
|
||||
its head, removing _SOURCE_SUFFIX from its tail, and replacing path
|
||||
separators in it with '/'.
|
||||
|
||||
Args:
|
||||
base_dir: the path to the base directory.
|
||||
"""
|
||||
|
||||
base_dir = _normalize_path(base_dir)
|
||||
|
||||
base_len = len(base_dir)
|
||||
suffix_len = len(_SOURCE_SUFFIX)
|
||||
|
||||
def converter(path):
|
||||
if not path.endswith(_SOURCE_SUFFIX):
|
||||
return None
|
||||
# _normalize_path must not be used because resolving symlink breaks
|
||||
# following path check.
|
||||
path = path.replace('\\', '/')
|
||||
if not path.startswith(base_dir):
|
||||
return None
|
||||
return path[base_len:-suffix_len]
|
||||
|
||||
return converter
|
||||
|
||||
|
||||
def _enumerate_handler_file_paths(directory):
|
||||
"""Returns a generator that enumerates WebSocket Handler source file names
|
||||
in the given directory.
|
||||
"""
|
||||
|
||||
for root, unused_dirs, files in os.walk(directory):
|
||||
for base in files:
|
||||
path = os.path.join(root, base)
|
||||
if _SOURCE_PATH_PATTERN.search(path):
|
||||
yield path
|
||||
|
||||
|
||||
class _HandlerSuite(object):
|
||||
"""A handler suite holder class."""
|
||||
def __init__(self, do_extra_handshake, transfer_data,
|
||||
passive_closing_handshake):
|
||||
self.do_extra_handshake = do_extra_handshake
|
||||
self.transfer_data = transfer_data
|
||||
self.passive_closing_handshake = passive_closing_handshake
|
||||
|
||||
|
||||
def _source_handler_file(handler_definition):
|
||||
"""Source a handler definition string.
|
||||
|
||||
Args:
|
||||
handler_definition: a string containing Python statements that define
|
||||
handler functions.
|
||||
"""
|
||||
|
||||
global_dic = {}
|
||||
try:
|
||||
# This statement is gramatically different in python 2 and 3.
|
||||
# Hence, yapf will complain about this. To overcome this, we disable
|
||||
# yapf for this line.
|
||||
exec(handler_definition, global_dic) # yapf: disable
|
||||
except Exception:
|
||||
raise DispatchException('Error in sourcing handler:' +
|
||||
traceback.format_exc())
|
||||
passive_closing_handshake_handler = None
|
||||
try:
|
||||
passive_closing_handshake_handler = _extract_handler(
|
||||
global_dic, _PASSIVE_CLOSING_HANDSHAKE_HANDLER_NAME)
|
||||
except Exception:
|
||||
passive_closing_handshake_handler = (
|
||||
_default_passive_closing_handshake_handler)
|
||||
return _HandlerSuite(
|
||||
_extract_handler(global_dic, _DO_EXTRA_HANDSHAKE_HANDLER_NAME),
|
||||
_extract_handler(global_dic, _TRANSFER_DATA_HANDLER_NAME),
|
||||
passive_closing_handshake_handler)
|
||||
|
||||
|
||||
def _extract_handler(dic, name):
|
||||
"""Extracts a callable with the specified name from the given dictionary
|
||||
dic.
|
||||
"""
|
||||
|
||||
if name not in dic:
|
||||
raise DispatchException('%s is not defined.' % name)
|
||||
handler = dic[name]
|
||||
if not callable(handler):
|
||||
raise DispatchException('%s is not callable.' % name)
|
||||
return handler
|
||||
|
||||
|
||||
class Dispatcher(object):
|
||||
"""Dispatches WebSocket requests.
|
||||
|
||||
This class maintains a map from resource name to handlers.
|
||||
"""
|
||||
def __init__(self,
|
||||
root_dir,
|
||||
scan_dir=None,
|
||||
allow_handlers_outside_root_dir=True,
|
||||
handler_encoding=None):
|
||||
"""Construct an instance.
|
||||
|
||||
Args:
|
||||
root_dir: The directory where handler definition files are
|
||||
placed.
|
||||
scan_dir: The directory where handler definition files are
|
||||
searched. scan_dir must be a directory under root_dir,
|
||||
including root_dir itself. If scan_dir is None,
|
||||
root_dir is used as scan_dir. scan_dir can be useful
|
||||
in saving scan time when root_dir contains many
|
||||
subdirectories.
|
||||
allow_handlers_outside_root_dir: Scans handler files even if their
|
||||
canonical path is not under root_dir.
|
||||
"""
|
||||
|
||||
self._logger = util.get_class_logger(self)
|
||||
|
||||
self._handler_suite_map = {}
|
||||
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)):
|
||||
raise DispatchException('scan_dir:%s must be a directory under '
|
||||
'root_dir:%s.' % (scan_dir, root_dir))
|
||||
self._source_handler_files_in_dir(root_dir, scan_dir,
|
||||
allow_handlers_outside_root_dir,
|
||||
handler_encoding)
|
||||
|
||||
def add_resource_path_alias(self, alias_resource_path,
|
||||
existing_resource_path):
|
||||
"""Add resource path alias.
|
||||
|
||||
Once added, request to alias_resource_path would be handled by
|
||||
handler registered for existing_resource_path.
|
||||
|
||||
Args:
|
||||
alias_resource_path: alias resource path
|
||||
existing_resource_path: existing resource path
|
||||
"""
|
||||
try:
|
||||
handler_suite = self._handler_suite_map[existing_resource_path]
|
||||
self._handler_suite_map[alias_resource_path] = handler_suite
|
||||
except KeyError:
|
||||
raise DispatchException('No handler for: %r' %
|
||||
existing_resource_path)
|
||||
|
||||
def source_warnings(self):
|
||||
"""Return warnings in sourcing handlers."""
|
||||
|
||||
return self._source_warnings
|
||||
|
||||
def do_extra_handshake(self, request):
|
||||
"""Do extra checking in WebSocket handshake.
|
||||
|
||||
Select a handler based on request.uri and call its
|
||||
web_socket_do_extra_handshake function.
|
||||
|
||||
Args:
|
||||
request: mod_python request.
|
||||
|
||||
Raises:
|
||||
DispatchException: when handler was not found
|
||||
AbortedByUserException: when user handler abort connection
|
||||
HandshakeException: when opening handshake failed
|
||||
"""
|
||||
|
||||
handler_suite = self.get_handler_suite(request.ws_resource)
|
||||
if handler_suite is None:
|
||||
raise DispatchException('No handler for: %r' % request.ws_resource)
|
||||
do_extra_handshake_ = handler_suite.do_extra_handshake
|
||||
try:
|
||||
do_extra_handshake_(request)
|
||||
except handshake.AbortedByUserException as e:
|
||||
# Re-raise to tell the caller of this function to finish this
|
||||
# connection without sending any error.
|
||||
self._logger.debug('%s', traceback.format_exc())
|
||||
raise
|
||||
except Exception as e:
|
||||
util.prepend_message_to_exception(
|
||||
'%s raised exception for %s: ' %
|
||||
(_DO_EXTRA_HANDSHAKE_HANDLER_NAME, request.ws_resource), e)
|
||||
raise handshake.HandshakeException(e, common.HTTP_STATUS_FORBIDDEN)
|
||||
|
||||
def transfer_data(self, request):
|
||||
"""Let a handler transfer_data with a WebSocket client.
|
||||
|
||||
Select a handler based on request.ws_resource and call its
|
||||
web_socket_transfer_data function.
|
||||
|
||||
Args:
|
||||
request: mod_python request.
|
||||
|
||||
Raises:
|
||||
DispatchException: when handler was not found
|
||||
AbortedByUserException: when user handler abort connection
|
||||
"""
|
||||
|
||||
# TODO(tyoshino): Terminate underlying TCP connection if possible.
|
||||
try:
|
||||
handler_suite = self.get_handler_suite(request.ws_resource)
|
||||
if handler_suite is None:
|
||||
raise DispatchException('No handler for: %r' %
|
||||
request.ws_resource)
|
||||
transfer_data_ = handler_suite.transfer_data
|
||||
transfer_data_(request)
|
||||
|
||||
if not request.server_terminated:
|
||||
request.ws_stream.close_connection()
|
||||
# Catch non-critical exceptions the handler didn't handle.
|
||||
except handshake.AbortedByUserException as e:
|
||||
self._logger.debug('%s', traceback.format_exc())
|
||||
raise
|
||||
except msgutil.BadOperationException as e:
|
||||
self._logger.debug('%s', e)
|
||||
request.ws_stream.close_connection(
|
||||
common.STATUS_INTERNAL_ENDPOINT_ERROR)
|
||||
except msgutil.InvalidFrameException as e:
|
||||
# InvalidFrameException must be caught before
|
||||
# ConnectionTerminatedException that catches InvalidFrameException.
|
||||
self._logger.debug('%s', e)
|
||||
request.ws_stream.close_connection(common.STATUS_PROTOCOL_ERROR)
|
||||
except msgutil.UnsupportedFrameException as e:
|
||||
self._logger.debug('%s', e)
|
||||
request.ws_stream.close_connection(common.STATUS_UNSUPPORTED_DATA)
|
||||
except stream.InvalidUTF8Exception as e:
|
||||
self._logger.debug('%s', e)
|
||||
request.ws_stream.close_connection(
|
||||
common.STATUS_INVALID_FRAME_PAYLOAD_DATA)
|
||||
except msgutil.ConnectionTerminatedException as e:
|
||||
self._logger.debug('%s', e)
|
||||
except Exception as e:
|
||||
# Any other exceptions are forwarded to the caller of this
|
||||
# function.
|
||||
util.prepend_message_to_exception(
|
||||
'%s raised exception for %s: ' %
|
||||
(_TRANSFER_DATA_HANDLER_NAME, request.ws_resource), e)
|
||||
raise
|
||||
|
||||
def passive_closing_handshake(self, request):
|
||||
"""Prepare code and reason for responding client initiated closing
|
||||
handshake.
|
||||
"""
|
||||
|
||||
handler_suite = self.get_handler_suite(request.ws_resource)
|
||||
if handler_suite is None:
|
||||
return _default_passive_closing_handshake_handler(request)
|
||||
return handler_suite.passive_closing_handshake(request)
|
||||
|
||||
def get_handler_suite(self, resource):
|
||||
"""Retrieves two handlers (one for extra handshake processing, and one
|
||||
for data transfer) for the given request as a HandlerSuite object.
|
||||
"""
|
||||
|
||||
fragment = None
|
||||
if '#' in resource:
|
||||
resource, fragment = resource.split('#', 1)
|
||||
if '?' in resource:
|
||||
resource = resource.split('?', 1)[0]
|
||||
handler_suite = self._handler_suite_map.get(resource)
|
||||
if handler_suite and fragment:
|
||||
raise DispatchException(
|
||||
'Fragment identifiers MUST NOT be used on WebSocket URIs',
|
||||
common.HTTP_STATUS_BAD_REQUEST)
|
||||
return handler_suite
|
||||
|
||||
def _source_handler_files_in_dir(self, root_dir, scan_dir,
|
||||
allow_handlers_outside_root_dir,
|
||||
handler_encoding):
|
||||
"""Source all the handler source files in the scan_dir directory.
|
||||
|
||||
The resource path is determined relative to root_dir.
|
||||
"""
|
||||
|
||||
# We build a map from resource to handler code assuming that there's
|
||||
# only one path from root_dir to scan_dir and it can be obtained by
|
||||
# comparing realpath of them.
|
||||
|
||||
# Here we cannot use abspath. See
|
||||
# https://bugs.webkit.org/show_bug.cgi?id=31603
|
||||
|
||||
convert = _create_path_to_resource_converter(root_dir)
|
||||
scan_realpath = os.path.realpath(scan_dir)
|
||||
root_realpath = os.path.realpath(root_dir)
|
||||
for path in _enumerate_handler_file_paths(scan_realpath):
|
||||
if (not allow_handlers_outside_root_dir and
|
||||
(not os.path.realpath(path).startswith(root_realpath))):
|
||||
self._logger.debug(
|
||||
'Canonical path of %s is not under root directory' % path)
|
||||
continue
|
||||
try:
|
||||
with io.open(path, encoding=handler_encoding) as handler_file:
|
||||
handler_suite = _source_handler_file(handler_file.read())
|
||||
except DispatchException as e:
|
||||
self._source_warnings.append('%s: %s' % (path, e))
|
||||
continue
|
||||
resource = convert(path)
|
||||
if resource is None:
|
||||
self._logger.debug('Path to resource conversion on %s failed' %
|
||||
path)
|
||||
else:
|
||||
self._handler_suite_map[convert(path)] = handler_suite
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
||||
474
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/pywebsocket3/pywebsocket3/extensions.py
vendored
Normal file
474
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/pywebsocket3/pywebsocket3/extensions.py
vendored
Normal file
|
|
@ -0,0 +1,474 @@
|
|||
# Copyright 2012, Google Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following disclaimer
|
||||
# in the documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
# * Neither the name of Google Inc. nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from pywebsocket3 import common, util
|
||||
from pywebsocket3.http_header_util import quote_if_necessary
|
||||
|
||||
# The list of available server side extension processor classes.
|
||||
_available_processors = {}
|
||||
|
||||
|
||||
class ExtensionProcessorInterface(object):
|
||||
def __init__(self, request):
|
||||
self._logger = util.get_class_logger(self)
|
||||
|
||||
self._request = request
|
||||
self._active = True
|
||||
|
||||
def request(self):
|
||||
return self._request
|
||||
|
||||
def name(self):
|
||||
return None
|
||||
|
||||
def check_consistency_with_other_processors(self, processors):
|
||||
pass
|
||||
|
||||
def set_active(self, active):
|
||||
self._active = active
|
||||
|
||||
def is_active(self):
|
||||
return self._active
|
||||
|
||||
def _get_extension_response_internal(self):
|
||||
return None
|
||||
|
||||
def get_extension_response(self):
|
||||
if not self._active:
|
||||
self._logger.debug('Extension %s is deactivated', self.name())
|
||||
return None
|
||||
|
||||
response = self._get_extension_response_internal()
|
||||
if response is None:
|
||||
self._active = False
|
||||
return response
|
||||
|
||||
def _setup_stream_options_internal(self, stream_options):
|
||||
pass
|
||||
|
||||
def setup_stream_options(self, stream_options):
|
||||
if self._active:
|
||||
self._setup_stream_options_internal(stream_options)
|
||||
|
||||
|
||||
def _log_outgoing_compression_ratio(logger, original_bytes, filtered_bytes,
|
||||
average_ratio):
|
||||
# Print inf when ratio is not available.
|
||||
ratio = float('inf')
|
||||
if original_bytes != 0:
|
||||
ratio = float(filtered_bytes) / original_bytes
|
||||
|
||||
logger.debug('Outgoing compression ratio: %f (average: %f)' %
|
||||
(ratio, average_ratio))
|
||||
|
||||
|
||||
def _log_incoming_compression_ratio(logger, received_bytes, filtered_bytes,
|
||||
average_ratio):
|
||||
# Print inf when ratio is not available.
|
||||
ratio = float('inf')
|
||||
if filtered_bytes != 0:
|
||||
ratio = float(received_bytes) / filtered_bytes
|
||||
|
||||
logger.debug('Incoming compression ratio: %f (average: %f)' %
|
||||
(ratio, average_ratio))
|
||||
|
||||
|
||||
def _parse_window_bits(bits):
|
||||
"""Return parsed integer value iff the given string conforms to the
|
||||
grammar of the window bits extension parameters.
|
||||
"""
|
||||
|
||||
if bits is None:
|
||||
raise ValueError('Value is required')
|
||||
|
||||
# For non integer values such as "10.0", ValueError will be raised.
|
||||
int_bits = int(bits)
|
||||
|
||||
# First condition is to drop leading zero case e.g. "08".
|
||||
if bits != str(int_bits) or int_bits < 8 or int_bits > 15:
|
||||
raise ValueError('Invalid value: %r' % bits)
|
||||
|
||||
return int_bits
|
||||
|
||||
|
||||
class _AverageRatioCalculator(object):
|
||||
"""Stores total bytes of original and result data, and calculates average
|
||||
result / original ratio.
|
||||
"""
|
||||
def __init__(self):
|
||||
self._total_original_bytes = 0
|
||||
self._total_result_bytes = 0
|
||||
|
||||
def add_original_bytes(self, value):
|
||||
self._total_original_bytes += value
|
||||
|
||||
def add_result_bytes(self, value):
|
||||
self._total_result_bytes += value
|
||||
|
||||
def get_average_ratio(self):
|
||||
if self._total_original_bytes != 0:
|
||||
return (float(self._total_result_bytes) /
|
||||
self._total_original_bytes)
|
||||
else:
|
||||
return float('inf')
|
||||
|
||||
|
||||
class PerMessageDeflateExtensionProcessor(ExtensionProcessorInterface):
|
||||
"""permessage-deflate extension processor.
|
||||
|
||||
Specification:
|
||||
http://tools.ietf.org/html/draft-ietf-hybi-permessage-compression-08
|
||||
"""
|
||||
|
||||
_SERVER_MAX_WINDOW_BITS_PARAM = 'server_max_window_bits'
|
||||
_SERVER_NO_CONTEXT_TAKEOVER_PARAM = 'server_no_context_takeover'
|
||||
_CLIENT_MAX_WINDOW_BITS_PARAM = 'client_max_window_bits'
|
||||
_CLIENT_NO_CONTEXT_TAKEOVER_PARAM = 'client_no_context_takeover'
|
||||
|
||||
def __init__(self, request):
|
||||
"""Construct PerMessageDeflateExtensionProcessor."""
|
||||
|
||||
ExtensionProcessorInterface.__init__(self, request)
|
||||
self._logger = util.get_class_logger(self)
|
||||
|
||||
self._preferred_client_max_window_bits = None
|
||||
self._client_no_context_takeover = False
|
||||
|
||||
def name(self):
|
||||
# This method returns "deflate" (not "permessage-deflate") for
|
||||
# compatibility.
|
||||
return 'deflate'
|
||||
|
||||
def _get_extension_response_internal(self):
|
||||
for name in self._request.get_parameter_names():
|
||||
if name not in [
|
||||
self._SERVER_MAX_WINDOW_BITS_PARAM,
|
||||
self._SERVER_NO_CONTEXT_TAKEOVER_PARAM,
|
||||
self._CLIENT_MAX_WINDOW_BITS_PARAM
|
||||
]:
|
||||
self._logger.debug('Unknown parameter: %r', name)
|
||||
return None
|
||||
|
||||
server_max_window_bits = None
|
||||
if self._request.has_parameter(self._SERVER_MAX_WINDOW_BITS_PARAM):
|
||||
server_max_window_bits = self._request.get_parameter_value(
|
||||
self._SERVER_MAX_WINDOW_BITS_PARAM)
|
||||
try:
|
||||
server_max_window_bits = _parse_window_bits(
|
||||
server_max_window_bits)
|
||||
except ValueError as e:
|
||||
self._logger.debug('Bad %s parameter: %r',
|
||||
self._SERVER_MAX_WINDOW_BITS_PARAM, e)
|
||||
return None
|
||||
|
||||
server_no_context_takeover = self._request.has_parameter(
|
||||
self._SERVER_NO_CONTEXT_TAKEOVER_PARAM)
|
||||
if (server_no_context_takeover and self._request.get_parameter_value(
|
||||
self._SERVER_NO_CONTEXT_TAKEOVER_PARAM) is not None):
|
||||
self._logger.debug('%s parameter must not have a value: %r',
|
||||
self._SERVER_NO_CONTEXT_TAKEOVER_PARAM,
|
||||
server_no_context_takeover)
|
||||
return None
|
||||
|
||||
# client_max_window_bits from a client indicates whether the client can
|
||||
# accept client_max_window_bits from a server or not.
|
||||
client_client_max_window_bits = self._request.has_parameter(
|
||||
self._CLIENT_MAX_WINDOW_BITS_PARAM)
|
||||
if (client_client_max_window_bits
|
||||
and self._request.get_parameter_value(
|
||||
self._CLIENT_MAX_WINDOW_BITS_PARAM) is not None):
|
||||
self._logger.debug(
|
||||
'%s parameter must not have a value in a '
|
||||
'client\'s opening handshake: %r',
|
||||
self._CLIENT_MAX_WINDOW_BITS_PARAM,
|
||||
client_client_max_window_bits)
|
||||
return None
|
||||
|
||||
self._rfc1979_deflater = util._RFC1979Deflater(
|
||||
server_max_window_bits, server_no_context_takeover)
|
||||
|
||||
# Note that we prepare for incoming messages compressed with window
|
||||
# bits upto 15 regardless of the client_max_window_bits value to be
|
||||
# sent to the client.
|
||||
self._rfc1979_inflater = util._RFC1979Inflater()
|
||||
|
||||
self._framer = _PerMessageDeflateFramer(server_max_window_bits,
|
||||
server_no_context_takeover)
|
||||
self._framer.set_bfinal(False)
|
||||
self._framer.set_compress_outgoing_enabled(True)
|
||||
|
||||
response = common.ExtensionParameter(self._request.name())
|
||||
|
||||
if server_max_window_bits is not None:
|
||||
response.add_parameter(self._SERVER_MAX_WINDOW_BITS_PARAM,
|
||||
str(server_max_window_bits))
|
||||
|
||||
if server_no_context_takeover:
|
||||
response.add_parameter(self._SERVER_NO_CONTEXT_TAKEOVER_PARAM,
|
||||
None)
|
||||
|
||||
if self._preferred_client_max_window_bits is not None:
|
||||
if not client_client_max_window_bits:
|
||||
self._logger.debug(
|
||||
'Processor is configured to use %s but '
|
||||
'the client cannot accept it',
|
||||
self._CLIENT_MAX_WINDOW_BITS_PARAM)
|
||||
return None
|
||||
response.add_parameter(self._CLIENT_MAX_WINDOW_BITS_PARAM,
|
||||
str(self._preferred_client_max_window_bits))
|
||||
|
||||
if self._client_no_context_takeover:
|
||||
response.add_parameter(self._CLIENT_NO_CONTEXT_TAKEOVER_PARAM,
|
||||
None)
|
||||
|
||||
self._logger.debug('Enable %s extension ('
|
||||
'request: server_max_window_bits=%s; '
|
||||
'server_no_context_takeover=%r, '
|
||||
'response: client_max_window_bits=%s; '
|
||||
'client_no_context_takeover=%r)' %
|
||||
(self._request.name(), server_max_window_bits,
|
||||
server_no_context_takeover,
|
||||
self._preferred_client_max_window_bits,
|
||||
self._client_no_context_takeover))
|
||||
|
||||
return response
|
||||
|
||||
def _setup_stream_options_internal(self, stream_options):
|
||||
self._framer.setup_stream_options(stream_options)
|
||||
|
||||
def set_client_max_window_bits(self, value):
|
||||
"""If this option is specified, this class adds the
|
||||
client_max_window_bits extension parameter to the handshake response,
|
||||
but doesn't reduce the LZ77 sliding window size of its inflater.
|
||||
I.e., you can use this for testing client implementation but cannot
|
||||
reduce memory usage of this class.
|
||||
|
||||
If this method has been called with True and an offer without the
|
||||
client_max_window_bits extension parameter is received,
|
||||
|
||||
- (When processing the permessage-deflate extension) this processor
|
||||
declines the request.
|
||||
- (When processing the permessage-compress extension) this processor
|
||||
accepts the request.
|
||||
"""
|
||||
|
||||
self._preferred_client_max_window_bits = value
|
||||
|
||||
def set_client_no_context_takeover(self, value):
|
||||
"""If this option is specified, this class adds the
|
||||
client_no_context_takeover extension parameter to the handshake
|
||||
response, but doesn't reset inflater for each message. I.e., you can
|
||||
use this for testing client implementation but cannot reduce memory
|
||||
usage of this class.
|
||||
"""
|
||||
|
||||
self._client_no_context_takeover = value
|
||||
|
||||
def set_bfinal(self, value):
|
||||
self._framer.set_bfinal(value)
|
||||
|
||||
def enable_outgoing_compression(self):
|
||||
self._framer.set_compress_outgoing_enabled(True)
|
||||
|
||||
def disable_outgoing_compression(self):
|
||||
self._framer.set_compress_outgoing_enabled(False)
|
||||
|
||||
|
||||
class _PerMessageDeflateFramer(object):
|
||||
"""A framer for extensions with per-message DEFLATE feature."""
|
||||
def __init__(self, deflate_max_window_bits, deflate_no_context_takeover):
|
||||
self._logger = util.get_class_logger(self)
|
||||
|
||||
self._rfc1979_deflater = util._RFC1979Deflater(
|
||||
deflate_max_window_bits, deflate_no_context_takeover)
|
||||
|
||||
self._rfc1979_inflater = util._RFC1979Inflater()
|
||||
|
||||
self._bfinal = False
|
||||
|
||||
self._compress_outgoing_enabled = False
|
||||
|
||||
# True if a message is fragmented and compression is ongoing.
|
||||
self._compress_ongoing = False
|
||||
|
||||
# Calculates
|
||||
# (Total outgoing bytes supplied to this filter) /
|
||||
# (Total bytes sent to the network after applying this filter)
|
||||
self._outgoing_average_ratio_calculator = _AverageRatioCalculator()
|
||||
|
||||
# Calculates
|
||||
# (Total bytes received from the network) /
|
||||
# (Total incoming bytes obtained after applying this filter)
|
||||
self._incoming_average_ratio_calculator = _AverageRatioCalculator()
|
||||
|
||||
def set_bfinal(self, value):
|
||||
self._bfinal = value
|
||||
|
||||
def set_compress_outgoing_enabled(self, value):
|
||||
self._compress_outgoing_enabled = value
|
||||
|
||||
def _process_incoming_message(self, message, decompress):
|
||||
if not decompress:
|
||||
return message
|
||||
|
||||
received_payload_size = len(message)
|
||||
self._incoming_average_ratio_calculator.add_result_bytes(
|
||||
received_payload_size)
|
||||
|
||||
message = self._rfc1979_inflater.filter(message)
|
||||
|
||||
filtered_payload_size = len(message)
|
||||
self._incoming_average_ratio_calculator.add_original_bytes(
|
||||
filtered_payload_size)
|
||||
|
||||
_log_incoming_compression_ratio(
|
||||
self._logger, received_payload_size, filtered_payload_size,
|
||||
self._incoming_average_ratio_calculator.get_average_ratio())
|
||||
|
||||
return message
|
||||
|
||||
def _process_outgoing_message(self, message, end, binary):
|
||||
if not binary:
|
||||
message = message.encode('utf-8')
|
||||
|
||||
if not self._compress_outgoing_enabled:
|
||||
return message
|
||||
|
||||
original_payload_size = len(message)
|
||||
self._outgoing_average_ratio_calculator.add_original_bytes(
|
||||
original_payload_size)
|
||||
|
||||
message = self._rfc1979_deflater.filter(message,
|
||||
end=end,
|
||||
bfinal=self._bfinal)
|
||||
|
||||
filtered_payload_size = len(message)
|
||||
self._outgoing_average_ratio_calculator.add_result_bytes(
|
||||
filtered_payload_size)
|
||||
|
||||
_log_outgoing_compression_ratio(
|
||||
self._logger, original_payload_size, filtered_payload_size,
|
||||
self._outgoing_average_ratio_calculator.get_average_ratio())
|
||||
|
||||
if not self._compress_ongoing:
|
||||
self._outgoing_frame_filter.set_compression_bit()
|
||||
self._compress_ongoing = not end
|
||||
return message
|
||||
|
||||
def _process_incoming_frame(self, frame):
|
||||
if frame.rsv1 == 1 and not common.is_control_opcode(frame.opcode):
|
||||
self._incoming_message_filter.decompress_next_message()
|
||||
frame.rsv1 = 0
|
||||
|
||||
def _process_outgoing_frame(self, frame, compression_bit):
|
||||
if (not compression_bit or common.is_control_opcode(frame.opcode)):
|
||||
return
|
||||
|
||||
frame.rsv1 = 1
|
||||
|
||||
def setup_stream_options(self, stream_options):
|
||||
"""Creates filters and sets them to the StreamOptions."""
|
||||
class _OutgoingMessageFilter(object):
|
||||
def __init__(self, parent):
|
||||
self._parent = parent
|
||||
|
||||
def filter(self, message, end=True, binary=False):
|
||||
return self._parent._process_outgoing_message(
|
||||
message, end, binary)
|
||||
|
||||
class _IncomingMessageFilter(object):
|
||||
def __init__(self, parent):
|
||||
self._parent = parent
|
||||
self._decompress_next_message = False
|
||||
|
||||
def decompress_next_message(self):
|
||||
self._decompress_next_message = True
|
||||
|
||||
def filter(self, message):
|
||||
message = self._parent._process_incoming_message(
|
||||
message, self._decompress_next_message)
|
||||
self._decompress_next_message = False
|
||||
return message
|
||||
|
||||
self._outgoing_message_filter = _OutgoingMessageFilter(self)
|
||||
self._incoming_message_filter = _IncomingMessageFilter(self)
|
||||
stream_options.outgoing_message_filters.append(
|
||||
self._outgoing_message_filter)
|
||||
stream_options.incoming_message_filters.append(
|
||||
self._incoming_message_filter)
|
||||
|
||||
class _OutgoingFrameFilter(object):
|
||||
def __init__(self, parent):
|
||||
self._parent = parent
|
||||
self._set_compression_bit = False
|
||||
|
||||
def set_compression_bit(self):
|
||||
self._set_compression_bit = True
|
||||
|
||||
def filter(self, frame):
|
||||
self._parent._process_outgoing_frame(frame,
|
||||
self._set_compression_bit)
|
||||
self._set_compression_bit = False
|
||||
|
||||
class _IncomingFrameFilter(object):
|
||||
def __init__(self, parent):
|
||||
self._parent = parent
|
||||
|
||||
def filter(self, frame):
|
||||
self._parent._process_incoming_frame(frame)
|
||||
|
||||
self._outgoing_frame_filter = _OutgoingFrameFilter(self)
|
||||
self._incoming_frame_filter = _IncomingFrameFilter(self)
|
||||
stream_options.outgoing_frame_filters.append(
|
||||
self._outgoing_frame_filter)
|
||||
stream_options.incoming_frame_filters.append(
|
||||
self._incoming_frame_filter)
|
||||
|
||||
stream_options.encode_text_message_to_utf8 = False
|
||||
|
||||
|
||||
_available_processors[common.PERMESSAGE_DEFLATE_EXTENSION] = (
|
||||
PerMessageDeflateExtensionProcessor)
|
||||
|
||||
|
||||
def get_extension_processor(extension_request):
|
||||
"""Given an ExtensionParameter representing an extension offer received
|
||||
from a client, configures and returns an instance of the corresponding
|
||||
extension processor class.
|
||||
"""
|
||||
|
||||
processor_class = _available_processors.get(extension_request.name())
|
||||
if processor_class is None:
|
||||
return None
|
||||
return processor_class(extension_request)
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
# Copyright 2011, Google Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following disclaimer
|
||||
# in the documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
# * Neither the name of Google Inc. nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
"""WebSocket opening handshake processor. This class try to apply available
|
||||
opening handshake processors for each protocol version until a connection is
|
||||
successfully established.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
|
||||
from pywebsocket3 import common
|
||||
from pywebsocket3.handshake import hybi
|
||||
# Export AbortedByUserException, HandshakeException, and VersionException
|
||||
# symbol from this module.
|
||||
from pywebsocket3.handshake.base import (
|
||||
AbortedByUserException,
|
||||
HandshakeException,
|
||||
VersionException
|
||||
)
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def do_handshake(request, dispatcher):
|
||||
"""Performs WebSocket handshake.
|
||||
|
||||
Args:
|
||||
request: mod_python request.
|
||||
dispatcher: Dispatcher (dispatch.Dispatcher).
|
||||
|
||||
Handshaker will add attributes such as ws_resource in performing
|
||||
handshake.
|
||||
"""
|
||||
|
||||
_LOGGER.debug('Client\'s opening handshake resource: %r', request.uri)
|
||||
# To print mimetools.Message as escaped one-line string, we converts
|
||||
# headers_in to dict object. Without conversion, if we use %r, it just
|
||||
# prints the type and address, and if we use %s, it prints the original
|
||||
# header string as multiple lines.
|
||||
#
|
||||
# Both mimetools.Message and MpTable_Type of mod_python can be
|
||||
# converted to dict.
|
||||
#
|
||||
# mimetools.Message.__str__ returns the original header string.
|
||||
# dict(mimetools.Message object) returns the map from header names to
|
||||
# header values. While MpTable_Type doesn't have such __str__ but just
|
||||
# __repr__ which formats itself as well as dictionary object.
|
||||
_LOGGER.debug('Client\'s opening handshake headers: %r',
|
||||
dict(request.headers_in))
|
||||
|
||||
handshakers = []
|
||||
handshakers.append(('RFC 6455', hybi.Handshaker(request, dispatcher)))
|
||||
|
||||
for name, handshaker in handshakers:
|
||||
_LOGGER.debug('Trying protocol version %s', name)
|
||||
try:
|
||||
handshaker.do_handshake()
|
||||
_LOGGER.info('Established (%s protocol)', name)
|
||||
return
|
||||
except HandshakeException as e:
|
||||
_LOGGER.debug(
|
||||
'Failed to complete opening handshake as %s protocol: %r',
|
||||
name, e)
|
||||
if e.status:
|
||||
raise e
|
||||
except AbortedByUserException as e:
|
||||
raise
|
||||
except VersionException as e:
|
||||
raise
|
||||
|
||||
# TODO(toyoshim): Add a test to cover the case all handshakers fail.
|
||||
raise HandshakeException(
|
||||
'Failed to complete opening handshake for all available protocols',
|
||||
status=common.HTTP_STATUS_BAD_REQUEST)
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
||||
393
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/pywebsocket3/pywebsocket3/handshake/base.py
vendored
Normal file
393
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/pywebsocket3/pywebsocket3/handshake/base.py
vendored
Normal file
|
|
@ -0,0 +1,393 @@
|
|||
# Copyright 2012, Google Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following disclaimer
|
||||
# in the documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
# * Neither the name of Google Inc. nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
"""Common functions and exceptions used by WebSocket opening handshake
|
||||
processors.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from pywebsocket3 import common, http_header_util, util
|
||||
from pywebsocket3.extensions import get_extension_processor
|
||||
from pywebsocket3.stream import Stream, StreamOptions
|
||||
|
||||
from six.moves import map, range
|
||||
|
||||
|
||||
# Defining aliases for values used frequently.
|
||||
_VERSION_LATEST = common.VERSION_HYBI_LATEST
|
||||
_VERSION_LATEST_STRING = str(_VERSION_LATEST)
|
||||
_SUPPORTED_VERSIONS = [
|
||||
_VERSION_LATEST,
|
||||
]
|
||||
|
||||
|
||||
class AbortedByUserException(Exception):
|
||||
"""Exception for aborting a connection intentionally.
|
||||
|
||||
If this exception is raised in do_extra_handshake handler, the connection
|
||||
will be abandoned. No other WebSocket or HTTP(S) handler will be invoked.
|
||||
|
||||
If this exception is raised in transfer_data_handler, the connection will
|
||||
be closed without closing handshake. No other WebSocket or HTTP(S) handler
|
||||
will be invoked.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class HandshakeException(Exception):
|
||||
"""This exception will be raised when an error occurred while processing
|
||||
WebSocket initial handshake.
|
||||
"""
|
||||
def __init__(self, name, status=None):
|
||||
super(HandshakeException, self).__init__(name)
|
||||
self.status = status
|
||||
|
||||
|
||||
class VersionException(Exception):
|
||||
"""This exception will be raised when a version of client request does not
|
||||
match with version the server supports.
|
||||
"""
|
||||
def __init__(self, name, supported_versions=''):
|
||||
"""Construct an instance.
|
||||
|
||||
Args:
|
||||
supported_version: a str object to show supported hybi versions.
|
||||
(e.g. '13')
|
||||
"""
|
||||
super(VersionException, self).__init__(name)
|
||||
self.supported_versions = supported_versions
|
||||
|
||||
|
||||
def get_default_port(is_secure):
|
||||
if is_secure:
|
||||
return common.DEFAULT_WEB_SOCKET_SECURE_PORT
|
||||
else:
|
||||
return common.DEFAULT_WEB_SOCKET_PORT
|
||||
|
||||
|
||||
def validate_subprotocol(subprotocol):
|
||||
"""Validate a value in the Sec-WebSocket-Protocol field.
|
||||
|
||||
See the Section 4.1., 4.2.2., and 4.3. of RFC 6455.
|
||||
"""
|
||||
|
||||
if not subprotocol:
|
||||
raise HandshakeException('Invalid subprotocol name: empty')
|
||||
|
||||
# Parameter should be encoded HTTP token.
|
||||
state = http_header_util.ParsingState(subprotocol)
|
||||
token = http_header_util.consume_token(state)
|
||||
rest = http_header_util.peek(state)
|
||||
# If |rest| is not None, |subprotocol| is not one token or invalid. If
|
||||
# |rest| is None, |token| must not be None because |subprotocol| is
|
||||
# concatenation of |token| and |rest| and is not None.
|
||||
if rest is not None:
|
||||
raise HandshakeException('Invalid non-token string in subprotocol '
|
||||
'name: %r' % rest)
|
||||
|
||||
|
||||
def parse_host_header(request):
|
||||
fields = request.headers_in[common.HOST_HEADER].split(':', 1)
|
||||
if len(fields) == 1:
|
||||
return fields[0], get_default_port(request.is_https())
|
||||
try:
|
||||
return fields[0], int(fields[1])
|
||||
except ValueError as e:
|
||||
raise HandshakeException('Invalid port number format: %r' % e)
|
||||
|
||||
|
||||
def get_mandatory_header(request, key):
|
||||
value = request.headers_in.get(key)
|
||||
if value is None:
|
||||
raise HandshakeException('Header %s is not defined' % key)
|
||||
return value
|
||||
|
||||
|
||||
def validate_mandatory_header(request, key, expected_value, fail_status=None):
|
||||
value = get_mandatory_header(request, key)
|
||||
|
||||
if value.lower() != expected_value.lower():
|
||||
raise HandshakeException(
|
||||
'Expected %r for header %s but found %r (case-insensitive)' %
|
||||
(expected_value, key, value),
|
||||
status=fail_status)
|
||||
|
||||
|
||||
def parse_token_list(data):
|
||||
"""Parses a header value which follows 1#token and returns parsed elements
|
||||
as a list of strings.
|
||||
|
||||
Leading LWSes must be trimmed.
|
||||
"""
|
||||
|
||||
state = http_header_util.ParsingState(data)
|
||||
|
||||
token_list = []
|
||||
|
||||
while True:
|
||||
token = http_header_util.consume_token(state)
|
||||
if token is not None:
|
||||
token_list.append(token)
|
||||
|
||||
http_header_util.consume_lwses(state)
|
||||
|
||||
if http_header_util.peek(state) is None:
|
||||
break
|
||||
|
||||
if not http_header_util.consume_string(state, ','):
|
||||
raise HandshakeException('Expected a comma but found %r' %
|
||||
http_header_util.peek(state))
|
||||
|
||||
http_header_util.consume_lwses(state)
|
||||
|
||||
if len(token_list) == 0:
|
||||
raise HandshakeException('No valid token found')
|
||||
|
||||
return token_list
|
||||
|
||||
|
||||
class HandshakerBase(object):
|
||||
def __init__(self, request, dispatcher):
|
||||
self._logger = util.get_class_logger(self)
|
||||
self._request = request
|
||||
self._dispatcher = dispatcher
|
||||
|
||||
""" subclasses must implement the five following methods """
|
||||
|
||||
def _protocol_rfc(self):
|
||||
""" Return the name of the RFC that the handshake class is implementing.
|
||||
"""
|
||||
|
||||
raise AssertionError("subclasses should implement this method")
|
||||
|
||||
def _transform_header(self, header):
|
||||
"""
|
||||
:param header: header name
|
||||
|
||||
transform the header name if needed. For example, HTTP/2 subclass will
|
||||
return the name of the header in lower case.
|
||||
"""
|
||||
|
||||
raise AssertionError("subclasses should implement this method")
|
||||
|
||||
def _validate_request(self):
|
||||
""" validate that all the mandatory fields are set """
|
||||
|
||||
raise AssertionError("subclasses should implement this method")
|
||||
|
||||
def _set_accept(self):
|
||||
""" Computes accept value based on Sec-WebSocket-Accept if needed. """
|
||||
|
||||
raise AssertionError("subclasses should implement this method")
|
||||
|
||||
def _send_handshake(self):
|
||||
""" Prepare and send the response after it has been parsed and processed.
|
||||
"""
|
||||
|
||||
raise AssertionError("subclasses should implement this method")
|
||||
|
||||
def do_handshake(self):
|
||||
self._request.ws_close_code = None
|
||||
self._request.ws_close_reason = None
|
||||
|
||||
# Parsing.
|
||||
self._validate_request()
|
||||
self._request.ws_resource = self._request.uri
|
||||
self._request.ws_version = self._check_version()
|
||||
|
||||
try:
|
||||
self._get_origin()
|
||||
self._set_protocol()
|
||||
self._parse_extensions()
|
||||
|
||||
self._set_accept()
|
||||
|
||||
self._logger.debug('Protocol version is ' + self._protocol_rfc())
|
||||
|
||||
# Setup extension processors.
|
||||
self._request.ws_extension_processors = self._get_extension_processors_requested(
|
||||
)
|
||||
|
||||
# List of extra headers. The extra handshake handler may add header
|
||||
# data as name/value pairs to this list and pywebsocket appends
|
||||
# them to the WebSocket handshake.
|
||||
self._request.extra_headers = []
|
||||
|
||||
# Extra handshake handler may modify/remove processors.
|
||||
self._dispatcher.do_extra_handshake(self._request)
|
||||
|
||||
stream_options = StreamOptions()
|
||||
self._process_extensions(stream_options)
|
||||
|
||||
self._request.ws_stream = Stream(self._request, stream_options)
|
||||
|
||||
if self._request.ws_requested_protocols is not None:
|
||||
if self._request.ws_protocol is None:
|
||||
raise HandshakeException(
|
||||
'do_extra_handshake must choose one subprotocol from '
|
||||
'ws_requested_protocols and set it to ws_protocol')
|
||||
validate_subprotocol(self._request.ws_protocol)
|
||||
|
||||
self._logger.debug('Subprotocol accepted: %r',
|
||||
self._request.ws_protocol)
|
||||
else:
|
||||
if self._request.ws_protocol is not None:
|
||||
raise HandshakeException(
|
||||
'ws_protocol must be None when the client didn\'t '
|
||||
'request any subprotocol')
|
||||
|
||||
self._send_handshake()
|
||||
except HandshakeException as e:
|
||||
if not e.status:
|
||||
# Fallback to 400 bad request by default.
|
||||
e.status = common.HTTP_STATUS_BAD_REQUEST
|
||||
raise e
|
||||
|
||||
def _check_version(self):
|
||||
sec_websocket_version_header = self._transform_header(
|
||||
common.SEC_WEBSOCKET_VERSION_HEADER)
|
||||
version = get_mandatory_header(self._request,
|
||||
sec_websocket_version_header)
|
||||
if version == _VERSION_LATEST_STRING:
|
||||
return _VERSION_LATEST
|
||||
|
||||
if version.find(',') >= 0:
|
||||
raise HandshakeException(
|
||||
'Multiple versions (%r) are not allowed for header %s' %
|
||||
(version, sec_websocket_version_header),
|
||||
status=common.HTTP_STATUS_BAD_REQUEST)
|
||||
raise VersionException('Unsupported version %r for header %s' %
|
||||
(version, sec_websocket_version_header),
|
||||
supported_versions=', '.join(
|
||||
map(str, _SUPPORTED_VERSIONS)))
|
||||
|
||||
def _get_origin(self):
|
||||
origin_header = self._transform_header(common.ORIGIN_HEADER)
|
||||
origin = self._request.headers_in.get(origin_header)
|
||||
if origin is None:
|
||||
self._logger.debug('Client request does not have origin header')
|
||||
self._request.ws_origin = origin
|
||||
|
||||
def _set_protocol(self):
|
||||
self._request.ws_protocol = None
|
||||
|
||||
sec_websocket_protocol_header = self._transform_header(
|
||||
common.SEC_WEBSOCKET_PROTOCOL_HEADER)
|
||||
protocol_header = self._request.headers_in.get(
|
||||
sec_websocket_protocol_header)
|
||||
|
||||
if protocol_header is None:
|
||||
self._request.ws_requested_protocols = None
|
||||
return
|
||||
|
||||
self._request.ws_requested_protocols = parse_token_list(
|
||||
protocol_header)
|
||||
self._logger.debug('Subprotocols requested: %r',
|
||||
self._request.ws_requested_protocols)
|
||||
|
||||
def _parse_extensions(self):
|
||||
sec_websocket_extensions_header = self._transform_header(
|
||||
common.SEC_WEBSOCKET_EXTENSIONS_HEADER)
|
||||
extensions_header = self._request.headers_in.get(
|
||||
sec_websocket_extensions_header)
|
||||
if not extensions_header:
|
||||
self._request.ws_requested_extensions = None
|
||||
return
|
||||
|
||||
try:
|
||||
self._request.ws_requested_extensions = common.parse_extensions(
|
||||
extensions_header)
|
||||
except common.ExtensionParsingException as e:
|
||||
raise HandshakeException(
|
||||
'Failed to parse sec-websocket-extensions header: %r' % e)
|
||||
|
||||
self._logger.debug(
|
||||
'Extensions requested: %r',
|
||||
list(
|
||||
map(common.ExtensionParameter.name,
|
||||
self._request.ws_requested_extensions)))
|
||||
|
||||
def _get_extension_processors_requested(self):
|
||||
processors = []
|
||||
if self._request.ws_requested_extensions is not None:
|
||||
for extension_request in self._request.ws_requested_extensions:
|
||||
processor = get_extension_processor(extension_request)
|
||||
# Unknown extension requests are just ignored.
|
||||
if processor is not None:
|
||||
processors.append(processor)
|
||||
return processors
|
||||
|
||||
def _process_extensions(self, stream_options):
|
||||
processors = [
|
||||
processor for processor in self._request.ws_extension_processors
|
||||
if processor is not None
|
||||
]
|
||||
|
||||
# Ask each processor if there are extensions on the request which
|
||||
# cannot co-exist. When processor decided other processors cannot
|
||||
# co-exist with it, the processor marks them (or itself) as
|
||||
# "inactive". The first extension processor has the right to
|
||||
# make the final call.
|
||||
for processor in reversed(processors):
|
||||
if processor.is_active():
|
||||
processor.check_consistency_with_other_processors(processors)
|
||||
processors = [
|
||||
processor for processor in processors if processor.is_active()
|
||||
]
|
||||
|
||||
accepted_extensions = []
|
||||
|
||||
for index, processor in enumerate(processors):
|
||||
if not processor.is_active():
|
||||
continue
|
||||
|
||||
extension_response = processor.get_extension_response()
|
||||
if extension_response is None:
|
||||
# Rejected.
|
||||
continue
|
||||
|
||||
accepted_extensions.append(extension_response)
|
||||
|
||||
processor.setup_stream_options(stream_options)
|
||||
|
||||
# Inactivate all of the following compression extensions.
|
||||
for j in range(index + 1, len(processors)):
|
||||
processors[j].set_active(False)
|
||||
|
||||
if len(accepted_extensions) > 0:
|
||||
self._request.ws_extensions = accepted_extensions
|
||||
self._logger.debug(
|
||||
'Extensions accepted: %r',
|
||||
list(map(common.ExtensionParameter.name, accepted_extensions)))
|
||||
else:
|
||||
self._request.ws_extensions = None
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
||||
226
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/pywebsocket3/pywebsocket3/handshake/hybi.py
vendored
Normal file
226
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/pywebsocket3/pywebsocket3/handshake/hybi.py
vendored
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
# Copyright 2012, Google Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following disclaimer
|
||||
# in the documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
# * Neither the name of Google Inc. nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
"""This file provides the opening handshake processor for the WebSocket
|
||||
protocol (RFC 6455).
|
||||
|
||||
Specification:
|
||||
http://tools.ietf.org/html/rfc6455
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import base64
|
||||
import re
|
||||
from hashlib import sha1
|
||||
|
||||
from pywebsocket3 import common, util
|
||||
from pywebsocket3.handshake.base import (
|
||||
get_mandatory_header,
|
||||
HandshakeException,
|
||||
parse_token_list,
|
||||
validate_mandatory_header,
|
||||
HandshakerBase
|
||||
)
|
||||
|
||||
|
||||
# Used to validate the value in the Sec-WebSocket-Key header strictly. RFC 4648
|
||||
# disallows non-zero padding, so the character right before == must be any of
|
||||
# A, Q, g and w.
|
||||
_SEC_WEBSOCKET_KEY_REGEX = re.compile('^[+/0-9A-Za-z]{21}[AQgw]==$')
|
||||
|
||||
|
||||
def check_request_line(request):
|
||||
# 5.1 1. The three character UTF-8 string "GET".
|
||||
# 5.1 2. A UTF-8-encoded U+0020 SPACE character (0x20 byte).
|
||||
if request.method != u'GET':
|
||||
raise HandshakeException('Method is not GET: %r' % request.method)
|
||||
|
||||
if request.protocol != u'HTTP/1.1':
|
||||
raise HandshakeException('Version is not HTTP/1.1: %r' %
|
||||
request.protocol)
|
||||
|
||||
|
||||
def compute_accept(key):
|
||||
"""Computes value for the Sec-WebSocket-Accept header from value of the
|
||||
Sec-WebSocket-Key header.
|
||||
"""
|
||||
|
||||
accept_binary = sha1(key + common.WEBSOCKET_ACCEPT_UUID).digest()
|
||||
accept = base64.b64encode(accept_binary)
|
||||
|
||||
return accept
|
||||
|
||||
|
||||
def compute_accept_from_unicode(unicode_key):
|
||||
"""A wrapper function for compute_accept which takes a unicode string as an
|
||||
argument, and encodes it to byte string. It then passes it on to
|
||||
compute_accept.
|
||||
"""
|
||||
|
||||
key = unicode_key.encode('UTF-8')
|
||||
return compute_accept(key)
|
||||
|
||||
|
||||
def format_header(name, value):
|
||||
return u'%s: %s\r\n' % (name, value)
|
||||
|
||||
|
||||
class Handshaker(HandshakerBase):
|
||||
"""Opening handshake processor for the WebSocket protocol (RFC 6455)."""
|
||||
def __init__(self, request, dispatcher):
|
||||
"""Construct an instance.
|
||||
|
||||
Args:
|
||||
request: mod_python request.
|
||||
dispatcher: Dispatcher (dispatch.Dispatcher).
|
||||
|
||||
Handshaker will add attributes such as ws_resource during handshake.
|
||||
"""
|
||||
super(Handshaker, self).__init__(request, dispatcher)
|
||||
|
||||
def _transform_header(self, header):
|
||||
return header
|
||||
|
||||
def _protocol_rfc(self):
|
||||
return 'RFC 6455'
|
||||
|
||||
def _validate_connection_header(self):
|
||||
connection = get_mandatory_header(self._request,
|
||||
common.CONNECTION_HEADER)
|
||||
|
||||
try:
|
||||
connection_tokens = parse_token_list(connection)
|
||||
except HandshakeException as e:
|
||||
raise HandshakeException('Failed to parse %s: %s' %
|
||||
(common.CONNECTION_HEADER, e))
|
||||
|
||||
connection_is_valid = False
|
||||
for token in connection_tokens:
|
||||
if token.lower() == common.UPGRADE_CONNECTION_TYPE.lower():
|
||||
connection_is_valid = True
|
||||
break
|
||||
if not connection_is_valid:
|
||||
raise HandshakeException(
|
||||
'%s header doesn\'t contain "%s"' %
|
||||
(common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE))
|
||||
|
||||
def _validate_request(self):
|
||||
check_request_line(self._request)
|
||||
validate_mandatory_header(self._request, common.UPGRADE_HEADER,
|
||||
common.WEBSOCKET_UPGRADE_TYPE)
|
||||
self._validate_connection_header()
|
||||
unused_host = get_mandatory_header(self._request, common.HOST_HEADER)
|
||||
|
||||
def _set_accept(self):
|
||||
# Key validation, response generation.
|
||||
key = self._get_key()
|
||||
accept = compute_accept(key)
|
||||
self._logger.debug('%s: %r (%s)', common.SEC_WEBSOCKET_ACCEPT_HEADER,
|
||||
accept, util.hexify(base64.b64decode(accept)))
|
||||
self._request._accept = accept
|
||||
|
||||
def _validate_key(self, key):
|
||||
if key.find(',') >= 0:
|
||||
raise HandshakeException('Request has multiple %s header lines or '
|
||||
'contains illegal character \',\': %r' %
|
||||
(common.SEC_WEBSOCKET_KEY_HEADER, key))
|
||||
|
||||
# Validate
|
||||
key_is_valid = False
|
||||
try:
|
||||
# Validate key by quick regex match before parsing by base64
|
||||
# module. Because base64 module skips invalid characters, we have
|
||||
# to do this in advance to make this server strictly reject illegal
|
||||
# keys.
|
||||
if _SEC_WEBSOCKET_KEY_REGEX.match(key):
|
||||
decoded_key = base64.b64decode(key)
|
||||
if len(decoded_key) == 16:
|
||||
key_is_valid = True
|
||||
except TypeError as e:
|
||||
pass
|
||||
|
||||
if not key_is_valid:
|
||||
raise HandshakeException('Illegal value for header %s: %r' %
|
||||
(common.SEC_WEBSOCKET_KEY_HEADER, key))
|
||||
|
||||
return decoded_key
|
||||
|
||||
def _get_key(self):
|
||||
key = get_mandatory_header(self._request,
|
||||
common.SEC_WEBSOCKET_KEY_HEADER)
|
||||
|
||||
decoded_key = self._validate_key(key)
|
||||
|
||||
self._logger.debug('%s: %r (%s)', common.SEC_WEBSOCKET_KEY_HEADER, key,
|
||||
util.hexify(decoded_key))
|
||||
|
||||
return key.encode('UTF-8')
|
||||
|
||||
def _create_handshake_response(self, accept):
|
||||
response = []
|
||||
|
||||
response.append(u'HTTP/1.1 101 Switching Protocols\r\n')
|
||||
|
||||
# WebSocket headers
|
||||
response.append(
|
||||
format_header(common.UPGRADE_HEADER,
|
||||
common.WEBSOCKET_UPGRADE_TYPE))
|
||||
response.append(
|
||||
format_header(common.CONNECTION_HEADER,
|
||||
common.UPGRADE_CONNECTION_TYPE))
|
||||
response.append(
|
||||
format_header(common.SEC_WEBSOCKET_ACCEPT_HEADER,
|
||||
accept.decode('UTF-8')))
|
||||
if self._request.ws_protocol is not None:
|
||||
response.append(
|
||||
format_header(common.SEC_WEBSOCKET_PROTOCOL_HEADER,
|
||||
self._request.ws_protocol))
|
||||
if (self._request.ws_extensions is not None
|
||||
and len(self._request.ws_extensions) != 0):
|
||||
response.append(
|
||||
format_header(
|
||||
common.SEC_WEBSOCKET_EXTENSIONS_HEADER,
|
||||
common.format_extensions(self._request.ws_extensions)))
|
||||
|
||||
# Headers not specific for WebSocket
|
||||
for name, value in self._request.extra_headers:
|
||||
response.append(format_header(name, value))
|
||||
|
||||
response.append(u'\r\n')
|
||||
|
||||
return u''.join(response)
|
||||
|
||||
def _send_handshake(self):
|
||||
raw_response = self._create_handshake_response(self._request._accept)
|
||||
self._request.connection.write(raw_response.encode('UTF-8'))
|
||||
self._logger.debug('Sent server\'s opening handshake: %r',
|
||||
raw_response)
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
||||
|
|
@ -0,0 +1,256 @@
|
|||
# Copyright 2011, Google Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following disclaimer
|
||||
# in the documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
# * Neither the name of Google Inc. nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
"""Utilities for parsing and formatting headers that follow the grammar defined
|
||||
in HTTP RFC http://www.ietf.org/rfc/rfc2616.txt.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import six.moves.urllib.parse
|
||||
|
||||
|
||||
_SEPARATORS = '()<>@,;:\\"/[]?={} \t'
|
||||
|
||||
|
||||
def _is_char(c):
|
||||
"""Returns true iff c is in CHAR as specified in HTTP RFC."""
|
||||
|
||||
return ord(c) <= 127
|
||||
|
||||
|
||||
def _is_ctl(c):
|
||||
"""Returns true iff c is in CTL as specified in HTTP RFC."""
|
||||
|
||||
return ord(c) <= 31 or ord(c) == 127
|
||||
|
||||
|
||||
class ParsingState(object):
|
||||
def __init__(self, data):
|
||||
self.data = data
|
||||
self.head = 0
|
||||
|
||||
|
||||
def peek(state, pos=0):
|
||||
"""Peeks the character at pos from the head of data."""
|
||||
|
||||
if state.head + pos >= len(state.data):
|
||||
return None
|
||||
|
||||
return state.data[state.head + pos]
|
||||
|
||||
|
||||
def consume(state, amount=1):
|
||||
"""Consumes specified amount of bytes from the head and returns the
|
||||
consumed bytes. If there's not enough bytes to consume, returns None.
|
||||
"""
|
||||
|
||||
if state.head + amount > len(state.data):
|
||||
return None
|
||||
|
||||
result = state.data[state.head:state.head + amount]
|
||||
state.head = state.head + amount
|
||||
return result
|
||||
|
||||
|
||||
def consume_string(state, expected):
|
||||
"""Given a parsing state and a expected string, consumes the string from
|
||||
the head. Returns True if consumed successfully. Otherwise, returns
|
||||
False.
|
||||
"""
|
||||
|
||||
pos = 0
|
||||
|
||||
for c in expected:
|
||||
if c != peek(state, pos):
|
||||
return False
|
||||
pos += 1
|
||||
|
||||
consume(state, pos)
|
||||
return True
|
||||
|
||||
|
||||
def consume_lws(state):
|
||||
"""Consumes a LWS from the head. Returns True if any LWS is consumed.
|
||||
Otherwise, returns False.
|
||||
|
||||
LWS = [CRLF] 1*( SP | HT )
|
||||
"""
|
||||
|
||||
original_head = state.head
|
||||
|
||||
consume_string(state, '\r\n')
|
||||
|
||||
pos = 0
|
||||
|
||||
while True:
|
||||
c = peek(state, pos)
|
||||
if c == ' ' or c == '\t':
|
||||
pos += 1
|
||||
else:
|
||||
if pos == 0:
|
||||
state.head = original_head
|
||||
return False
|
||||
else:
|
||||
consume(state, pos)
|
||||
return True
|
||||
|
||||
|
||||
def consume_lwses(state):
|
||||
r"""Consumes \*LWS from the head."""
|
||||
|
||||
while consume_lws(state):
|
||||
pass
|
||||
|
||||
|
||||
def consume_token(state):
|
||||
"""Consumes a token from the head. Returns the token or None if no token
|
||||
was found.
|
||||
"""
|
||||
|
||||
pos = 0
|
||||
|
||||
while True:
|
||||
c = peek(state, pos)
|
||||
if c is None or c in _SEPARATORS or _is_ctl(c) or not _is_char(c):
|
||||
if pos == 0:
|
||||
return None
|
||||
|
||||
return consume(state, pos)
|
||||
else:
|
||||
pos += 1
|
||||
|
||||
|
||||
def consume_token_or_quoted_string(state):
|
||||
"""Consumes a token or a quoted-string, and returns the token or unquoted
|
||||
string. If no token or quoted-string was found, returns None.
|
||||
"""
|
||||
|
||||
original_head = state.head
|
||||
|
||||
if not consume_string(state, '"'):
|
||||
return consume_token(state)
|
||||
|
||||
result = []
|
||||
|
||||
expect_quoted_pair = False
|
||||
|
||||
while True:
|
||||
if not expect_quoted_pair and consume_lws(state):
|
||||
result.append(' ')
|
||||
continue
|
||||
|
||||
c = consume(state)
|
||||
if c is None:
|
||||
# quoted-string is not enclosed with double quotation
|
||||
state.head = original_head
|
||||
return None
|
||||
elif expect_quoted_pair:
|
||||
expect_quoted_pair = False
|
||||
if _is_char(c):
|
||||
result.append(c)
|
||||
else:
|
||||
# Non CHAR character found in quoted-pair
|
||||
state.head = original_head
|
||||
return None
|
||||
elif c == '\\':
|
||||
expect_quoted_pair = True
|
||||
elif c == '"':
|
||||
return ''.join(result)
|
||||
elif _is_ctl(c):
|
||||
# Invalid character %r found in qdtext
|
||||
state.head = original_head
|
||||
return None
|
||||
else:
|
||||
result.append(c)
|
||||
|
||||
|
||||
def quote_if_necessary(s):
|
||||
"""Quotes arbitrary string into quoted-string."""
|
||||
|
||||
quote = False
|
||||
if s == '':
|
||||
return '""'
|
||||
|
||||
result = []
|
||||
for c in s:
|
||||
if c == '"' or c in _SEPARATORS or _is_ctl(c) or not _is_char(c):
|
||||
quote = True
|
||||
|
||||
if c == '"' or _is_ctl(c):
|
||||
result.append('\\' + c)
|
||||
else:
|
||||
result.append(c)
|
||||
|
||||
if quote:
|
||||
return '"' + ''.join(result) + '"'
|
||||
else:
|
||||
return ''.join(result)
|
||||
|
||||
|
||||
def parse_uri(uri):
|
||||
"""Parse absolute URI then return host, port and resource."""
|
||||
|
||||
parsed = six.moves.urllib.parse.urlsplit(uri)
|
||||
if parsed.scheme != 'wss' and parsed.scheme != 'ws':
|
||||
# |uri| must be a relative URI.
|
||||
# TODO(toyoshim): Should validate |uri|.
|
||||
return None, None, uri
|
||||
|
||||
if parsed.hostname is None:
|
||||
return None, None, None
|
||||
|
||||
port = None
|
||||
try:
|
||||
port = parsed.port
|
||||
except ValueError:
|
||||
# The port property cause ValueError on invalid null port descriptions
|
||||
# like 'ws://host:INVALID_PORT/path', where the assigned port is not
|
||||
# *DIGIT. For python 3.6 and later, ValueError also raises when
|
||||
# assigning invalid port numbers such as 'ws://host:-1/path'. Earlier
|
||||
# versions simply return None and ignore invalid port attributes.
|
||||
return None, None, None
|
||||
|
||||
if port is None:
|
||||
if parsed.scheme == 'ws':
|
||||
port = 80
|
||||
else:
|
||||
port = 443
|
||||
|
||||
path = parsed.path
|
||||
if not path:
|
||||
path += '/'
|
||||
if parsed.query:
|
||||
path += '?' + parsed.query
|
||||
if parsed.fragment:
|
||||
path += '#' + parsed.fragment
|
||||
|
||||
return parsed.hostname, port, path
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
||||
100
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/pywebsocket3/pywebsocket3/memorizingfile.py
vendored
Normal file
100
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/pywebsocket3/pywebsocket3/memorizingfile.py
vendored
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright 2011, Google Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following disclaimer
|
||||
# in the documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
# * Neither the name of Google Inc. nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
"""Memorizing file.
|
||||
|
||||
A memorizing file wraps a file and memorizes lines read by readline.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
class MemorizingFile(object):
|
||||
"""MemorizingFile wraps a file and memorizes lines read by readline.
|
||||
|
||||
Note that data read by other methods are not memorized. This behavior
|
||||
is good enough for memorizing lines SimpleHTTPServer reads before
|
||||
the control reaches WebSocketRequestHandler.
|
||||
"""
|
||||
def __init__(self, file_, max_memorized_lines=sys.maxsize):
|
||||
"""Construct an instance.
|
||||
|
||||
Args:
|
||||
file_: the file object to wrap.
|
||||
max_memorized_lines: the maximum number of lines to memorize.
|
||||
Only the first max_memorized_lines are memorized.
|
||||
Default: sys.maxint.
|
||||
"""
|
||||
self._file = file_
|
||||
self._memorized_lines = []
|
||||
self._max_memorized_lines = max_memorized_lines
|
||||
self._buffered = False
|
||||
self._buffered_line = None
|
||||
|
||||
def __getattribute__(self, name):
|
||||
"""Return a file attribute.
|
||||
|
||||
Returns the value overridden by this class for some attributes,
|
||||
and forwards the call to _file for the other attributes.
|
||||
"""
|
||||
if name in ('_file', '_memorized_lines', '_max_memorized_lines',
|
||||
'_buffered', '_buffered_line', 'readline',
|
||||
'get_memorized_lines'):
|
||||
return object.__getattribute__(self, name)
|
||||
return self._file.__getattribute__(name)
|
||||
|
||||
def readline(self, size=-1):
|
||||
"""Override file.readline and memorize the line read.
|
||||
|
||||
Note that even if size is specified and smaller than actual size,
|
||||
the whole line will be read out from underlying file object by
|
||||
subsequent readline calls.
|
||||
"""
|
||||
if self._buffered:
|
||||
line = self._buffered_line
|
||||
self._buffered = False
|
||||
else:
|
||||
line = self._file.readline()
|
||||
if line and len(self._memorized_lines) < self._max_memorized_lines:
|
||||
self._memorized_lines.append(line)
|
||||
if size >= 0 and size < len(line):
|
||||
self._buffered = True
|
||||
self._buffered_line = line[size:]
|
||||
return line[:size]
|
||||
return line
|
||||
|
||||
def get_memorized_lines(self):
|
||||
"""Get lines memorized so far."""
|
||||
return self._memorized_lines
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
||||
218
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/pywebsocket3/pywebsocket3/msgutil.py
vendored
Normal file
218
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/pywebsocket3/pywebsocket3/msgutil.py
vendored
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
# Copyright 2011, Google Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following disclaimer
|
||||
# in the documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
# * Neither the name of Google Inc. nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
"""Message related utilities.
|
||||
|
||||
Note: request.connection.write/read are used in this module, even though
|
||||
mod_python document says that they should be used only in connection
|
||||
handlers. Unfortunately, we have no other options. For example,
|
||||
request.write/read are not suitable because they don't allow direct raw
|
||||
bytes writing/reading.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import threading
|
||||
|
||||
import six.moves.queue
|
||||
|
||||
# Export Exception symbols from msgutil for backward compatibility
|
||||
from pywebsocket3._stream_exceptions import (
|
||||
ConnectionTerminatedException,
|
||||
InvalidFrameException,
|
||||
BadOperationException,
|
||||
UnsupportedFrameException
|
||||
)
|
||||
|
||||
|
||||
# An API for handler to send/receive WebSocket messages.
|
||||
def close_connection(request):
|
||||
"""Close connection.
|
||||
|
||||
Args:
|
||||
request: mod_python request.
|
||||
"""
|
||||
request.ws_stream.close_connection()
|
||||
|
||||
|
||||
def send_message(request, payload_data, end=True, binary=False):
|
||||
"""Send a message (or part of a message).
|
||||
|
||||
Args:
|
||||
request: mod_python request.
|
||||
payload_data: unicode text or str binary to send.
|
||||
end: True to terminate a message.
|
||||
False to send payload_data as part of a message that is to be
|
||||
terminated by next or later send_message call with end=True.
|
||||
binary: send payload_data as binary frame(s).
|
||||
Raises:
|
||||
BadOperationException: when server already terminated.
|
||||
"""
|
||||
request.ws_stream.send_message(payload_data, end, binary)
|
||||
|
||||
|
||||
def receive_message(request):
|
||||
"""Receive a WebSocket frame and return its payload as a text in
|
||||
unicode or a binary in str.
|
||||
|
||||
Args:
|
||||
request: mod_python request.
|
||||
Raises:
|
||||
InvalidFrameException: when client send invalid frame.
|
||||
UnsupportedFrameException: when client send unsupported frame e.g. some
|
||||
of reserved bit is set but no extension can
|
||||
recognize it.
|
||||
InvalidUTF8Exception: when client send a text frame containing any
|
||||
invalid UTF-8 string.
|
||||
ConnectionTerminatedException: when the connection is closed
|
||||
unexpectedly.
|
||||
BadOperationException: when client already terminated.
|
||||
"""
|
||||
return request.ws_stream.receive_message()
|
||||
|
||||
|
||||
def send_ping(request, body):
|
||||
request.ws_stream.send_ping(body)
|
||||
|
||||
|
||||
class MessageReceiver(threading.Thread):
|
||||
"""This class receives messages from the client.
|
||||
|
||||
This class provides three ways to receive messages: blocking,
|
||||
non-blocking, and via callback. Callback has the highest precedence.
|
||||
|
||||
Note: This class should not be used with the standalone server for wss
|
||||
because pyOpenSSL used by the server raises a fatal error if the socket
|
||||
is accessed from multiple threads.
|
||||
"""
|
||||
def __init__(self, request, onmessage=None):
|
||||
"""Construct an instance.
|
||||
|
||||
Args:
|
||||
request: mod_python request.
|
||||
onmessage: a function to be called when a message is received.
|
||||
May be None. If not None, the function is called on
|
||||
another thread. In that case, MessageReceiver.receive
|
||||
and MessageReceiver.receive_nowait are useless
|
||||
because they will never return any messages.
|
||||
"""
|
||||
|
||||
threading.Thread.__init__(self)
|
||||
self._request = request
|
||||
self._queue = six.moves.queue.Queue()
|
||||
self._onmessage = onmessage
|
||||
self._stop_requested = False
|
||||
self.setDaemon(True)
|
||||
self.start()
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
while not self._stop_requested:
|
||||
message = receive_message(self._request)
|
||||
if self._onmessage:
|
||||
self._onmessage(message)
|
||||
else:
|
||||
self._queue.put(message)
|
||||
finally:
|
||||
close_connection(self._request)
|
||||
|
||||
def receive(self):
|
||||
""" Receive a message from the channel, blocking.
|
||||
|
||||
Returns:
|
||||
message as a unicode string.
|
||||
"""
|
||||
return self._queue.get()
|
||||
|
||||
def receive_nowait(self):
|
||||
""" Receive a message from the channel, non-blocking.
|
||||
|
||||
Returns:
|
||||
message as a unicode string if available. None otherwise.
|
||||
"""
|
||||
try:
|
||||
message = self._queue.get_nowait()
|
||||
except six.moves.queue.Empty:
|
||||
message = None
|
||||
return message
|
||||
|
||||
def stop(self):
|
||||
"""Request to stop this instance.
|
||||
|
||||
The instance will be stopped after receiving the next message.
|
||||
This method may not be very useful, but there is no clean way
|
||||
in Python to forcefully stop a running thread.
|
||||
"""
|
||||
self._stop_requested = True
|
||||
|
||||
|
||||
class MessageSender(threading.Thread):
|
||||
"""This class sends messages to the client.
|
||||
|
||||
This class provides both synchronous and asynchronous ways to send
|
||||
messages.
|
||||
|
||||
Note: This class should not be used with the standalone server for wss
|
||||
because pyOpenSSL used by the server raises a fatal error if the socket
|
||||
is accessed from multiple threads.
|
||||
"""
|
||||
def __init__(self, request):
|
||||
"""Construct an instance.
|
||||
|
||||
Args:
|
||||
request: mod_python request.
|
||||
"""
|
||||
threading.Thread.__init__(self)
|
||||
self._request = request
|
||||
self._queue = six.moves.queue.Queue()
|
||||
self.setDaemon(True)
|
||||
self.start()
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
message, condition = self._queue.get()
|
||||
condition.acquire()
|
||||
send_message(self._request, message)
|
||||
condition.notify()
|
||||
condition.release()
|
||||
|
||||
def send(self, message):
|
||||
"""Send a message, blocking."""
|
||||
|
||||
condition = threading.Condition()
|
||||
condition.acquire()
|
||||
self._queue.put((message, condition))
|
||||
condition.wait()
|
||||
|
||||
def send_nowait(self, message):
|
||||
"""Send a message, non-blocking."""
|
||||
|
||||
self._queue.put((message, threading.Condition()))
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
||||
|
|
@ -0,0 +1,322 @@
|
|||
# Copyright 2020, Google Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following disclaimer
|
||||
# in the documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
# * Neither the name of Google Inc. nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
"""Request Handler and Request/Connection classes for standalone server.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from six.moves import CGIHTTPServer
|
||||
from six.moves import http_client
|
||||
|
||||
from pywebsocket3 import (
|
||||
common,
|
||||
dispatch,
|
||||
handshake,
|
||||
http_header_util,
|
||||
memorizingfile,
|
||||
util
|
||||
)
|
||||
|
||||
# 1024 is practically large enough to contain WebSocket handshake lines.
|
||||
_MAX_MEMORIZED_LINES = 1024
|
||||
|
||||
|
||||
class _StandaloneConnection(object):
|
||||
"""Mimic mod_python mp_conn."""
|
||||
def __init__(self, request_handler):
|
||||
"""Construct an instance.
|
||||
|
||||
Args:
|
||||
request_handler: A WebSocketRequestHandler instance.
|
||||
"""
|
||||
|
||||
self._request_handler = request_handler
|
||||
|
||||
def get_local_addr(self):
|
||||
"""Getter to mimic mp_conn.local_addr."""
|
||||
|
||||
return (self._request_handler.server.server_name,
|
||||
self._request_handler.server.server_port)
|
||||
|
||||
local_addr = property(get_local_addr)
|
||||
|
||||
def get_remote_addr(self):
|
||||
"""Getter to mimic mp_conn.remote_addr.
|
||||
|
||||
Setting the property in __init__ won't work because the request
|
||||
handler is not initialized yet there."""
|
||||
|
||||
return self._request_handler.client_address
|
||||
|
||||
remote_addr = property(get_remote_addr)
|
||||
|
||||
def write(self, data):
|
||||
"""Mimic mp_conn.write()."""
|
||||
|
||||
return self._request_handler.wfile.write(data)
|
||||
|
||||
def read(self, length):
|
||||
"""Mimic mp_conn.read()."""
|
||||
|
||||
return self._request_handler.rfile.read(length)
|
||||
|
||||
def get_memorized_lines(self):
|
||||
"""Get memorized lines."""
|
||||
|
||||
return self._request_handler.rfile.get_memorized_lines()
|
||||
|
||||
|
||||
class _StandaloneRequest(object):
|
||||
"""Mimic mod_python request."""
|
||||
def __init__(self, request_handler, use_tls):
|
||||
"""Construct an instance.
|
||||
|
||||
Args:
|
||||
request_handler: A WebSocketRequestHandler instance.
|
||||
"""
|
||||
|
||||
self._logger = util.get_class_logger(self)
|
||||
|
||||
self._request_handler = request_handler
|
||||
self.connection = _StandaloneConnection(request_handler)
|
||||
self._use_tls = use_tls
|
||||
self.headers_in = request_handler.headers
|
||||
|
||||
def get_uri(self):
|
||||
"""Getter to mimic request.uri.
|
||||
|
||||
This method returns the raw data at the Request-URI part of the
|
||||
Request-Line, while the uri method on the request object of mod_python
|
||||
returns the path portion after parsing the raw data. This behavior is
|
||||
kept for compatibility.
|
||||
"""
|
||||
|
||||
return self._request_handler.path
|
||||
|
||||
uri = property(get_uri)
|
||||
|
||||
def get_unparsed_uri(self):
|
||||
"""Getter to mimic request.unparsed_uri."""
|
||||
|
||||
return self._request_handler.path
|
||||
|
||||
unparsed_uri = property(get_unparsed_uri)
|
||||
|
||||
def get_method(self):
|
||||
"""Getter to mimic request.method."""
|
||||
|
||||
return self._request_handler.command
|
||||
|
||||
method = property(get_method)
|
||||
|
||||
def get_protocol(self):
|
||||
"""Getter to mimic request.protocol."""
|
||||
|
||||
return self._request_handler.request_version
|
||||
|
||||
protocol = property(get_protocol)
|
||||
|
||||
def is_https(self):
|
||||
"""Mimic request.is_https()."""
|
||||
|
||||
return self._use_tls
|
||||
|
||||
|
||||
class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler):
|
||||
"""CGIHTTPRequestHandler specialized for WebSocket."""
|
||||
|
||||
# Use httplib.HTTPMessage instead of mimetools.Message.
|
||||
MessageClass = http_client.HTTPMessage
|
||||
|
||||
def setup(self):
|
||||
"""Override SocketServer.StreamRequestHandler.setup to wrap rfile
|
||||
with MemorizingFile.
|
||||
|
||||
This method will be called by BaseRequestHandler's constructor
|
||||
before calling BaseHTTPRequestHandler.handle.
|
||||
BaseHTTPRequestHandler.handle will call
|
||||
BaseHTTPRequestHandler.handle_one_request and it will call
|
||||
WebSocketRequestHandler.parse_request.
|
||||
"""
|
||||
|
||||
# Call superclass's setup to prepare rfile, wfile, etc. See setup
|
||||
# definition on the root class SocketServer.StreamRequestHandler to
|
||||
# understand what this does.
|
||||
CGIHTTPServer.CGIHTTPRequestHandler.setup(self)
|
||||
|
||||
self.rfile = memorizingfile.MemorizingFile(
|
||||
self.rfile, max_memorized_lines=_MAX_MEMORIZED_LINES)
|
||||
|
||||
def __init__(self, request, client_address, server):
|
||||
self._logger = util.get_class_logger(self)
|
||||
|
||||
self._options = server.websocket_server_options
|
||||
|
||||
# Overrides CGIHTTPServerRequestHandler.cgi_directories.
|
||||
self.cgi_directories = self._options.cgi_directories
|
||||
# Replace CGIHTTPRequestHandler.is_executable method.
|
||||
if self._options.is_executable_method is not None:
|
||||
self.is_executable = self._options.is_executable_method
|
||||
|
||||
# This actually calls BaseRequestHandler.__init__.
|
||||
CGIHTTPServer.CGIHTTPRequestHandler.__init__(self, request,
|
||||
client_address, server)
|
||||
|
||||
def parse_request(self):
|
||||
"""Override BaseHTTPServer.BaseHTTPRequestHandler.parse_request.
|
||||
|
||||
Return True to continue processing for HTTP(S), False otherwise.
|
||||
|
||||
See BaseHTTPRequestHandler.handle_one_request method which calls
|
||||
this method to understand how the return value will be handled.
|
||||
"""
|
||||
|
||||
# We hook parse_request method, but also call the original
|
||||
# CGIHTTPRequestHandler.parse_request since when we return False,
|
||||
# CGIHTTPRequestHandler.handle_one_request continues processing and
|
||||
# it needs variables set by CGIHTTPRequestHandler.parse_request.
|
||||
#
|
||||
# Variables set by this method will be also used by WebSocket request
|
||||
# handling (self.path, self.command, self.requestline, etc. See also
|
||||
# how _StandaloneRequest's members are implemented using these
|
||||
# attributes).
|
||||
if not CGIHTTPServer.CGIHTTPRequestHandler.parse_request(self):
|
||||
return False
|
||||
|
||||
if self._options.use_basic_auth:
|
||||
auth = self.headers.get('Authorization')
|
||||
if auth != self._options.basic_auth_credential:
|
||||
self.send_response(401)
|
||||
self.send_header('WWW-Authenticate',
|
||||
'Basic realm="Pywebsocket"')
|
||||
self.end_headers()
|
||||
self._logger.info('Request basic authentication')
|
||||
return False
|
||||
|
||||
whole_path = self.path
|
||||
host, port, resource = http_header_util.parse_uri(self.path)
|
||||
if resource is None:
|
||||
self._logger.info('Invalid URI: %r', self.path)
|
||||
self._logger.info('Fallback to CGIHTTPRequestHandler')
|
||||
return True
|
||||
server_options = self.server.websocket_server_options
|
||||
if host is not None:
|
||||
validation_host = server_options.validation_host
|
||||
if validation_host is not None and host != validation_host:
|
||||
self._logger.info('Invalid host: %r (expected: %r)', host,
|
||||
validation_host)
|
||||
self._logger.info('Fallback to CGIHTTPRequestHandler')
|
||||
return True
|
||||
if port is not None:
|
||||
validation_port = server_options.validation_port
|
||||
if validation_port is not None and port != validation_port:
|
||||
self._logger.info('Invalid port: %r (expected: %r)', port,
|
||||
validation_port)
|
||||
self._logger.info('Fallback to CGIHTTPRequestHandler')
|
||||
return True
|
||||
self.path = resource
|
||||
|
||||
request = _StandaloneRequest(self, self._options.use_tls)
|
||||
|
||||
try:
|
||||
# Fallback to default http handler for request paths for which
|
||||
# we don't have request handlers.
|
||||
if not self._options.dispatcher.get_handler_suite(self.path):
|
||||
self._logger.info('No handler for resource: %r', whole_path)
|
||||
self._logger.info('Fallback to CGIHTTPRequestHandler')
|
||||
return True
|
||||
except dispatch.DispatchException as e:
|
||||
self._logger.info('Dispatch failed for error: %s', e)
|
||||
self.send_error(e.status)
|
||||
return False
|
||||
|
||||
# If any Exceptions without except clause setup (including
|
||||
# DispatchException) is raised below this point, it will be caught
|
||||
# and logged by WebSocketServer.
|
||||
|
||||
try:
|
||||
try:
|
||||
handshake.do_handshake(request, self._options.dispatcher)
|
||||
except handshake.VersionException as e:
|
||||
self._logger.info('Handshake failed for version error: %s', e)
|
||||
self.send_response(common.HTTP_STATUS_BAD_REQUEST)
|
||||
self.send_header(common.SEC_WEBSOCKET_VERSION_HEADER,
|
||||
e.supported_versions)
|
||||
self.end_headers()
|
||||
return False
|
||||
except handshake.HandshakeException as e:
|
||||
# Handshake for ws(s) failed.
|
||||
self._logger.info('Handshake failed for error: %s', e)
|
||||
self.send_error(e.status)
|
||||
return False
|
||||
|
||||
request._dispatcher = self._options.dispatcher
|
||||
self._options.dispatcher.transfer_data(request)
|
||||
except handshake.AbortedByUserException as e:
|
||||
self._logger.info('Aborted: %s', e)
|
||||
return False
|
||||
|
||||
def log_request(self, code='-', size='-'):
|
||||
"""Override BaseHTTPServer.log_request."""
|
||||
|
||||
self._logger.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.
|
||||
self._logger.warning('%s - %s', self.address_string(),
|
||||
args[0] % args[1:])
|
||||
|
||||
def is_cgi(self):
|
||||
"""Test whether self.path corresponds to a CGI script.
|
||||
|
||||
Add extra check that self.path doesn't contains ..
|
||||
Also check if the file is a executable file or not.
|
||||
If the file is not executable, it is handled as static file or dir
|
||||
rather than a CGI script.
|
||||
"""
|
||||
|
||||
if CGIHTTPServer.CGIHTTPRequestHandler.is_cgi(self):
|
||||
if '..' in self.path:
|
||||
return False
|
||||
# strip query parameter from request path
|
||||
resource_name = self.path.split('?', 2)[0]
|
||||
# convert resource_name into real path name in filesystem.
|
||||
scriptfile = self.translate_path(resource_name)
|
||||
if not os.path.isfile(scriptfile):
|
||||
return False
|
||||
if not self.is_executable(scriptfile):
|
||||
return False
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
# Copyright 2020, Google Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following disclaimer
|
||||
# in the documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
# * Neither the name of Google Inc. nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
"""Server related utilities."""
|
||||
|
||||
import logging
|
||||
import logging.handlers
|
||||
import threading
|
||||
import time
|
||||
|
||||
from pywebsocket3 import common, util
|
||||
|
||||
|
||||
def _get_logger_from_class(c):
|
||||
return logging.getLogger('%s.%s' % (c.__module__, c.__name__))
|
||||
|
||||
|
||||
def configure_logging(options):
|
||||
logging.addLevelName(common.LOGLEVEL_FINE, 'FINE')
|
||||
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(logging.getLevelName(options.log_level.upper()))
|
||||
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)
|
||||
|
||||
deflate_log_level_name = logging.getLevelName(
|
||||
options.deflate_log_level.upper())
|
||||
_get_logger_from_class(util._Deflater).setLevel(deflate_log_level_name)
|
||||
_get_logger_from_class(util._Inflater).setLevel(deflate_log_level_name)
|
||||
|
||||
|
||||
class ThreadMonitor(threading.Thread):
|
||||
daemon = True
|
||||
|
||||
def __init__(self, interval_in_sec):
|
||||
threading.Thread.__init__(self, name='ThreadMonitor')
|
||||
|
||||
self._logger = util.get_class_logger(self)
|
||||
|
||||
self._interval_in_sec = interval_in_sec
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
thread_name_list = []
|
||||
for thread in threading.enumerate():
|
||||
thread_name_list.append(thread.name)
|
||||
self._logger.info("%d active threads: %s",
|
||||
threading.active_count(),
|
||||
', '.join(thread_name_list))
|
||||
time.sleep(self._interval_in_sec)
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
||||
492
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/pywebsocket3/pywebsocket3/standalone.py
vendored
Executable file
492
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/pywebsocket3/pywebsocket3/standalone.py
vendored
Executable file
|
|
@ -0,0 +1,492 @@
|
|||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright 2012, Google Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following disclaimer
|
||||
# in the documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
# * Neither the name of Google Inc. nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
"""Standalone WebSocket server.
|
||||
|
||||
Use this file to launch pywebsocket as a standalone server.
|
||||
|
||||
|
||||
BASIC USAGE
|
||||
===========
|
||||
|
||||
Go to the src directory and run
|
||||
|
||||
$ python pywebsocket3/standalone.py [-p <ws_port>]
|
||||
[-w <websock_handlers>]
|
||||
[-d <document_root>]
|
||||
|
||||
<ws_port> is the port number to use for ws:// connection.
|
||||
|
||||
<document_root> is the path to the root directory of HTML files.
|
||||
|
||||
<websock_handlers> is the path to the root directory of WebSocket handlers.
|
||||
If not specified, <document_root> will be used. See __init__.py (or
|
||||
run $ pydoc pywebsocket3) for how to write WebSocket handlers.
|
||||
|
||||
For more detail and other options, run
|
||||
|
||||
$ python pywebsocket3/standalone.py --help
|
||||
|
||||
or see _build_option_parser method below.
|
||||
|
||||
For trouble shooting, adding "--log_level debug" might help you.
|
||||
|
||||
|
||||
TRY DEMO
|
||||
========
|
||||
|
||||
Go to the src directory and run standalone.py with -d option to set the
|
||||
document root to the directory containing example HTMLs and handlers like this:
|
||||
|
||||
$ cd src
|
||||
$ PYTHONPATH=. python pywebsocket3/standalone.py -d example
|
||||
|
||||
to launch pywebsocket with the sample handler and html on port 80. Open
|
||||
http://localhost/console.html, click the connect button, type something into
|
||||
the text box next to the send button and click the send button. If everything
|
||||
is working, you'll see the message you typed echoed by the server.
|
||||
|
||||
|
||||
USING TLS
|
||||
=========
|
||||
|
||||
To run the standalone server with TLS support, run it with -t, -k, and -c
|
||||
options. When TLS is enabled, the standalone server accepts only TLS connection.
|
||||
|
||||
Note that when ssl module is used and the key/cert location is incorrect,
|
||||
TLS connection silently fails while pyOpenSSL fails on startup.
|
||||
|
||||
Example:
|
||||
|
||||
$ PYTHONPATH=. python pywebsocket3/standalone.py \
|
||||
-d example \
|
||||
-p 10443 \
|
||||
-t \
|
||||
-c ../test/cert/cert.pem \
|
||||
-k ../test/cert/key.pem \
|
||||
|
||||
Note that when passing a relative path to -c and -k option, it will be resolved
|
||||
using the document root directory as the base.
|
||||
|
||||
|
||||
USING CLIENT AUTHENTICATION
|
||||
===========================
|
||||
|
||||
To run the standalone server with TLS client authentication support, run it with
|
||||
--tls-client-auth and --tls-client-ca options in addition to ones required for
|
||||
TLS support.
|
||||
|
||||
Example:
|
||||
|
||||
$ PYTHONPATH=. python pywebsocket3/standalone.py -d example -p 10443 -t \
|
||||
-c ../test/cert/cert.pem -k ../test/cert/key.pem \
|
||||
--tls-client-auth \
|
||||
--tls-client-ca=../test/cert/cacert.pem
|
||||
|
||||
Note that when passing a relative path to --tls-client-ca option, it will be
|
||||
resolved using the document root directory as the base.
|
||||
|
||||
|
||||
CONFIGURATION FILE
|
||||
==================
|
||||
|
||||
You can also write a configuration file and use it by specifying the path to
|
||||
the configuration file by --config option. Please write a configuration file
|
||||
following the documentation of the Python ConfigParser library. Name of each
|
||||
entry must be the long version argument name. E.g. to set log level to debug,
|
||||
add the following line:
|
||||
|
||||
log_level=debug
|
||||
|
||||
For options which doesn't take value, please add some fake value. E.g. for
|
||||
--tls option, add the following line:
|
||||
|
||||
tls=True
|
||||
|
||||
Note that tls will be enabled even if you write tls=False as the value part is
|
||||
fake.
|
||||
|
||||
When both a command line argument and a configuration file entry are set for
|
||||
the same configuration item, the command line value will override one in the
|
||||
configuration file.
|
||||
|
||||
|
||||
THREADING
|
||||
=========
|
||||
|
||||
This server is derived from SocketServer.ThreadingMixIn. Hence a thread is
|
||||
used for each request.
|
||||
|
||||
|
||||
SECURITY WARNING
|
||||
================
|
||||
|
||||
This uses CGIHTTPServer and CGIHTTPServer is not secure.
|
||||
It may execute arbitrary Python code or external programs. It should not be
|
||||
used outside a firewall.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import six
|
||||
from six.moves import configparser
|
||||
|
||||
from pywebsocket3 import common, server_util, util
|
||||
from pywebsocket3.websocket_server import WebSocketServer
|
||||
|
||||
|
||||
_DEFAULT_LOG_MAX_BYTES = 1024 * 256
|
||||
_DEFAULT_LOG_BACKUP_COUNT = 5
|
||||
|
||||
_DEFAULT_REQUEST_QUEUE_SIZE = 128
|
||||
|
||||
|
||||
def _build_option_parser():
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
parser.add_argument(
|
||||
'--config',
|
||||
dest='config_file',
|
||||
type=six.text_type,
|
||||
default=None,
|
||||
help=('Path to configuration file. See the file comment '
|
||||
'at the top of this file for the configuration '
|
||||
'file format'))
|
||||
parser.add_argument('-H',
|
||||
'--server-host',
|
||||
'--server_host',
|
||||
dest='server_host',
|
||||
default='',
|
||||
help='server hostname to listen to')
|
||||
parser.add_argument('-V',
|
||||
'--validation-host',
|
||||
'--validation_host',
|
||||
dest='validation_host',
|
||||
default=None,
|
||||
help='server hostname to validate in absolute path.')
|
||||
parser.add_argument('-p',
|
||||
'--port',
|
||||
dest='port',
|
||||
type=int,
|
||||
default=common.DEFAULT_WEB_SOCKET_PORT,
|
||||
help='port to listen to')
|
||||
parser.add_argument('-P',
|
||||
'--validation-port',
|
||||
'--validation_port',
|
||||
dest='validation_port',
|
||||
type=int,
|
||||
default=None,
|
||||
help='server port to validate in absolute path.')
|
||||
parser.add_argument(
|
||||
'-w',
|
||||
'--websock-handlers',
|
||||
'--websock_handlers',
|
||||
dest='websock_handlers',
|
||||
default='.',
|
||||
help=('The root directory of WebSocket handler files. '
|
||||
'If the path is relative, --document-root is used '
|
||||
'as the base.'))
|
||||
parser.add_argument('-m',
|
||||
'--websock-handlers-map-file',
|
||||
'--websock_handlers_map_file',
|
||||
dest='websock_handlers_map_file',
|
||||
default=None,
|
||||
help=('WebSocket handlers map file. '
|
||||
'Each line consists of alias_resource_path and '
|
||||
'existing_resource_path, separated by spaces.'))
|
||||
parser.add_argument('-s',
|
||||
'--scan-dir',
|
||||
'--scan_dir',
|
||||
dest='scan_dir',
|
||||
default=None,
|
||||
help=('Must be a directory under --websock-handlers. '
|
||||
'Only handlers under this directory are scanned '
|
||||
'and registered to the server. '
|
||||
'Useful for saving scan time when the handler '
|
||||
'root directory contains lots of files that are '
|
||||
'not handler file or are handler files but you '
|
||||
'don\'t want them to be registered. '))
|
||||
parser.add_argument(
|
||||
'--allow-handlers-outside-root-dir',
|
||||
'--allow_handlers_outside_root_dir',
|
||||
dest='allow_handlers_outside_root_dir',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help=('Scans WebSocket handlers even if their canonical '
|
||||
'path is not under --websock-handlers.'))
|
||||
parser.add_argument('-d',
|
||||
'--document-root',
|
||||
'--document_root',
|
||||
dest='document_root',
|
||||
default='.',
|
||||
help='Document root directory.')
|
||||
parser.add_argument('-x',
|
||||
'--cgi-paths',
|
||||
'--cgi_paths',
|
||||
dest='cgi_paths',
|
||||
default=None,
|
||||
help=('CGI paths relative to document_root.'
|
||||
'Comma-separated. (e.g -x /cgi,/htbin) '
|
||||
'Files under document_root/cgi_path are handled '
|
||||
'as CGI programs. Must be executable.'))
|
||||
parser.add_argument('-t',
|
||||
'--tls',
|
||||
dest='use_tls',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='use TLS (wss://)')
|
||||
parser.add_argument('-k',
|
||||
'--private-key',
|
||||
'--private_key',
|
||||
dest='private_key',
|
||||
default='',
|
||||
help='TLS private key file.')
|
||||
parser.add_argument('-c',
|
||||
'--certificate',
|
||||
dest='certificate',
|
||||
default='',
|
||||
help='TLS certificate file.')
|
||||
parser.add_argument('--tls-client-auth',
|
||||
dest='tls_client_auth',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Requests TLS client auth on every connection.')
|
||||
parser.add_argument('--tls-client-cert-optional',
|
||||
dest='tls_client_cert_optional',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help=('Makes client certificate optional even though '
|
||||
'TLS client auth is enabled.'))
|
||||
parser.add_argument('--tls-client-ca',
|
||||
dest='tls_client_ca',
|
||||
default='',
|
||||
help=('Specifies a pem file which contains a set of '
|
||||
'concatenated CA certificates which are used to '
|
||||
'validate certificates passed from clients'))
|
||||
parser.add_argument('--basic-auth',
|
||||
dest='use_basic_auth',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Requires Basic authentication.')
|
||||
parser.add_argument(
|
||||
'--basic-auth-credential',
|
||||
dest='basic_auth_credential',
|
||||
default='test:test',
|
||||
help='Specifies the credential of basic authentication '
|
||||
'by username:password pair (e.g. test:test).')
|
||||
parser.add_argument('-l',
|
||||
'--log-file',
|
||||
'--log_file',
|
||||
dest='log_file',
|
||||
default='',
|
||||
help='Log file.')
|
||||
# Custom log level:
|
||||
# - FINE: Prints status of each frame processing step
|
||||
parser.add_argument('--log-level',
|
||||
'--log_level',
|
||||
type=six.text_type,
|
||||
dest='log_level',
|
||||
default='warn',
|
||||
choices=[
|
||||
'fine', 'debug', 'info', 'warning', 'warn',
|
||||
'error', 'critical'
|
||||
],
|
||||
help='Log level.')
|
||||
parser.add_argument(
|
||||
'--deflate-log-level',
|
||||
'--deflate_log_level',
|
||||
type=six.text_type,
|
||||
dest='deflate_log_level',
|
||||
default='warn',
|
||||
choices=['debug', 'info', 'warning', 'warn', 'error', 'critical'],
|
||||
help='Log level for _Deflater and _Inflater.')
|
||||
parser.add_argument('--thread-monitor-interval-in-sec',
|
||||
'--thread_monitor_interval_in_sec',
|
||||
dest='thread_monitor_interval_in_sec',
|
||||
type=int,
|
||||
default=-1,
|
||||
help=('If positive integer is specified, run a thread '
|
||||
'monitor to show the status of server threads '
|
||||
'periodically in the specified inteval in '
|
||||
'second. If non-positive integer is specified, '
|
||||
'disable the thread monitor.'))
|
||||
parser.add_argument('--log-max',
|
||||
'--log_max',
|
||||
dest='log_max',
|
||||
type=int,
|
||||
default=_DEFAULT_LOG_MAX_BYTES,
|
||||
help='Log maximum bytes')
|
||||
parser.add_argument('--log-count',
|
||||
'--log_count',
|
||||
dest='log_count',
|
||||
type=int,
|
||||
default=_DEFAULT_LOG_BACKUP_COUNT,
|
||||
help='Log backup count')
|
||||
parser.add_argument('-q',
|
||||
'--queue',
|
||||
dest='request_queue_size',
|
||||
type=int,
|
||||
default=_DEFAULT_REQUEST_QUEUE_SIZE,
|
||||
help='request queue size')
|
||||
parser.add_argument(
|
||||
'--handler-encoding',
|
||||
'--handler_encoding',
|
||||
dest='handler_encoding',
|
||||
type=six.text_type,
|
||||
default=None,
|
||||
help=('Text encoding used for loading handlers. '
|
||||
'By default, the encoding from the locale is used when '
|
||||
'reading handler files, but this option can override it. '
|
||||
'Any encoding supported by the codecs module may be used.'))
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def _parse_args_and_config(args):
|
||||
parser = _build_option_parser()
|
||||
|
||||
# First, parse options without configuration file.
|
||||
temporary_options, temporary_args = parser.parse_known_args(args=args)
|
||||
if temporary_args:
|
||||
logging.critical('Unrecognized positional arguments: %r',
|
||||
temporary_args)
|
||||
sys.exit(1)
|
||||
|
||||
if temporary_options.config_file:
|
||||
try:
|
||||
config_fp = open(temporary_options.config_file, 'r')
|
||||
except IOError as e:
|
||||
logging.critical('Failed to open configuration file %r: %r',
|
||||
temporary_options.config_file, e)
|
||||
sys.exit(1)
|
||||
|
||||
config_parser = configparser.SafeConfigParser()
|
||||
config_parser.readfp(config_fp)
|
||||
config_fp.close()
|
||||
|
||||
args_from_config = []
|
||||
for name, value in config_parser.items('pywebsocket'):
|
||||
args_from_config.append('--' + name)
|
||||
args_from_config.append(value)
|
||||
if args is None:
|
||||
args = args_from_config
|
||||
else:
|
||||
args = args_from_config + args
|
||||
return parser.parse_known_args(args=args)
|
||||
else:
|
||||
return temporary_options, temporary_args
|
||||
|
||||
|
||||
def _main(args=None):
|
||||
"""You can call this function from your own program, but please note that
|
||||
this function has some side-effects that might affect your program. For
|
||||
example, it changes the current directory.
|
||||
"""
|
||||
|
||||
options, args = _parse_args_and_config(args=args)
|
||||
|
||||
os.chdir(options.document_root)
|
||||
|
||||
server_util.configure_logging(options)
|
||||
|
||||
# TODO(tyoshino): Clean up initialization of CGI related values. Move some
|
||||
# of code here to WebSocketRequestHandler class if it's better.
|
||||
options.cgi_directories = []
|
||||
options.is_executable_method = None
|
||||
if options.cgi_paths:
|
||||
options.cgi_directories = options.cgi_paths.split(',')
|
||||
if sys.platform in ('cygwin', 'win32'):
|
||||
cygwin_path = None
|
||||
# For Win32 Python, it is expected that CYGWIN_PATH
|
||||
# is set to a directory of cygwin binaries.
|
||||
# For example, websocket_server.py in Chromium sets CYGWIN_PATH to
|
||||
# full path of third_party/cygwin/bin.
|
||||
if 'CYGWIN_PATH' in os.environ:
|
||||
cygwin_path = os.environ['CYGWIN_PATH']
|
||||
|
||||
def __check_script(scriptpath):
|
||||
return util.get_script_interp(scriptpath, cygwin_path)
|
||||
|
||||
options.is_executable_method = __check_script
|
||||
|
||||
if options.use_tls:
|
||||
logging.debug('Using ssl module')
|
||||
|
||||
if not options.private_key or not options.certificate:
|
||||
logging.critical(
|
||||
'To use TLS, specify private_key and certificate.')
|
||||
sys.exit(1)
|
||||
|
||||
if (options.tls_client_cert_optional and not options.tls_client_auth):
|
||||
logging.critical('Client authentication must be enabled to '
|
||||
'specify tls_client_cert_optional')
|
||||
sys.exit(1)
|
||||
else:
|
||||
if options.tls_client_auth:
|
||||
logging.critical('TLS must be enabled for client authentication.')
|
||||
sys.exit(1)
|
||||
|
||||
if options.tls_client_cert_optional:
|
||||
logging.critical('TLS must be enabled for client authentication.')
|
||||
sys.exit(1)
|
||||
|
||||
if not options.scan_dir:
|
||||
options.scan_dir = options.websock_handlers
|
||||
|
||||
if options.use_basic_auth:
|
||||
options.basic_auth_credential = 'Basic ' + base64.b64encode(
|
||||
options.basic_auth_credential.encode('UTF-8')).decode()
|
||||
|
||||
try:
|
||||
if options.thread_monitor_interval_in_sec > 0:
|
||||
# Run a thread monitor to show the status of server threads for
|
||||
# debugging.
|
||||
server_util.ThreadMonitor(
|
||||
options.thread_monitor_interval_in_sec).start()
|
||||
|
||||
server = WebSocketServer(options)
|
||||
server.serve_forever()
|
||||
except Exception as e:
|
||||
logging.critical('pywebsocket3: %s' % e)
|
||||
logging.critical('pywebsocket3: %s' % traceback.format_exc())
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
_main(sys.argv[1:])
|
||||
|
||||
# vi:sts=4 sw=4 et
|
||||
951
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/pywebsocket3/pywebsocket3/stream.py
vendored
Normal file
951
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/pywebsocket3/pywebsocket3/stream.py
vendored
Normal file
|
|
@ -0,0 +1,951 @@
|
|||
# Copyright 2011, Google Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following disclaimer
|
||||
# in the documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
# * Neither the name of Google Inc. nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
"""This file provides classes and helper functions for parsing/building frames
|
||||
of the WebSocket protocol (RFC 6455).
|
||||
|
||||
Specification:
|
||||
http://tools.ietf.org/html/rfc6455
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import struct
|
||||
import time
|
||||
from collections import deque
|
||||
|
||||
import six
|
||||
|
||||
from pywebsocket3 import common, util
|
||||
from pywebsocket3._stream_exceptions import (
|
||||
BadOperationException,
|
||||
ConnectionTerminatedException,
|
||||
InvalidFrameException,
|
||||
InvalidUTF8Exception,
|
||||
UnsupportedFrameException
|
||||
)
|
||||
|
||||
_NOOP_MASKER = util.NoopMasker()
|
||||
|
||||
|
||||
class Frame(object):
|
||||
def __init__(self,
|
||||
fin=1,
|
||||
rsv1=0,
|
||||
rsv2=0,
|
||||
rsv3=0,
|
||||
opcode=None,
|
||||
payload=b''):
|
||||
self.fin = fin
|
||||
self.rsv1 = rsv1
|
||||
self.rsv2 = rsv2
|
||||
self.rsv3 = rsv3
|
||||
self.opcode = opcode
|
||||
self.payload = payload
|
||||
|
||||
|
||||
# Helper functions made public to be used for writing unittests for WebSocket
|
||||
# clients.
|
||||
|
||||
|
||||
def create_length_header(length, mask):
|
||||
"""Creates a length header.
|
||||
|
||||
Args:
|
||||
length: Frame length. Must be less than 2^63.
|
||||
mask: Mask bit. Must be boolean.
|
||||
|
||||
Raises:
|
||||
ValueError: when bad data is given.
|
||||
"""
|
||||
|
||||
if mask:
|
||||
mask_bit = 1 << 7
|
||||
else:
|
||||
mask_bit = 0
|
||||
|
||||
if length < 0:
|
||||
raise ValueError('length must be non negative integer')
|
||||
elif length <= 125:
|
||||
return util.pack_byte(mask_bit | length)
|
||||
elif length < (1 << 16):
|
||||
return util.pack_byte(mask_bit | 126) + struct.pack('!H', length)
|
||||
elif length < (1 << 63):
|
||||
return util.pack_byte(mask_bit | 127) + struct.pack('!Q', length)
|
||||
else:
|
||||
raise ValueError('Payload is too big for one frame')
|
||||
|
||||
|
||||
def create_header(opcode, payload_length, fin, rsv1, rsv2, rsv3, mask):
|
||||
"""Creates a frame header.
|
||||
|
||||
Raises:
|
||||
Exception: when bad data is given.
|
||||
"""
|
||||
|
||||
if opcode < 0 or 0xf < opcode:
|
||||
raise ValueError('Opcode out of range')
|
||||
|
||||
if payload_length < 0 or (1 << 63) <= payload_length:
|
||||
raise ValueError('payload_length out of range')
|
||||
|
||||
if (fin | rsv1 | rsv2 | rsv3) & ~1:
|
||||
raise ValueError('FIN bit and Reserved bit parameter must be 0 or 1')
|
||||
|
||||
header = b''
|
||||
|
||||
first_byte = ((fin << 7)
|
||||
| (rsv1 << 6) | (rsv2 << 5) | (rsv3 << 4)
|
||||
| opcode)
|
||||
header += util.pack_byte(first_byte)
|
||||
header += create_length_header(payload_length, mask)
|
||||
|
||||
return header
|
||||
|
||||
|
||||
def _build_frame(header, body, mask):
|
||||
if not mask:
|
||||
return header + body
|
||||
|
||||
masking_nonce = os.urandom(4)
|
||||
masker = util.RepeatedXorMasker(masking_nonce)
|
||||
|
||||
return header + masking_nonce + masker.mask(body)
|
||||
|
||||
|
||||
def _filter_and_format_frame_object(frame, mask, frame_filters):
|
||||
for frame_filter in frame_filters:
|
||||
frame_filter.filter(frame)
|
||||
|
||||
header = create_header(frame.opcode, len(frame.payload), frame.fin,
|
||||
frame.rsv1, frame.rsv2, frame.rsv3, mask)
|
||||
return _build_frame(header, frame.payload, mask)
|
||||
|
||||
|
||||
def create_binary_frame(message,
|
||||
opcode=common.OPCODE_BINARY,
|
||||
fin=1,
|
||||
mask=False,
|
||||
frame_filters=[]):
|
||||
"""Creates a simple binary frame with no extension, reserved bit."""
|
||||
|
||||
frame = Frame(fin=fin, opcode=opcode, payload=message)
|
||||
return _filter_and_format_frame_object(frame, mask, frame_filters)
|
||||
|
||||
|
||||
def create_text_frame(message,
|
||||
opcode=common.OPCODE_TEXT,
|
||||
fin=1,
|
||||
mask=False,
|
||||
frame_filters=[]):
|
||||
"""Creates a simple text frame with no extension, reserved bit."""
|
||||
|
||||
encoded_message = message.encode('utf-8')
|
||||
return create_binary_frame(encoded_message, opcode, fin, mask,
|
||||
frame_filters)
|
||||
|
||||
|
||||
def parse_frame(receive_bytes,
|
||||
logger=None,
|
||||
ws_version=common.VERSION_HYBI_LATEST,
|
||||
unmask_receive=True):
|
||||
"""Parses a frame. Returns a tuple containing each header field and
|
||||
payload.
|
||||
|
||||
Args:
|
||||
receive_bytes: a function that reads frame data from a stream or
|
||||
something similar. The function takes length of the bytes to be
|
||||
read. The function must raise ConnectionTerminatedException if
|
||||
there is not enough data to be read.
|
||||
logger: a logging object.
|
||||
ws_version: the version of WebSocket protocol.
|
||||
unmask_receive: unmask received frames. When received unmasked
|
||||
frame, raises InvalidFrameException.
|
||||
|
||||
Raises:
|
||||
ConnectionTerminatedException: when receive_bytes raises it.
|
||||
InvalidFrameException: when the frame contains invalid data.
|
||||
"""
|
||||
|
||||
if not logger:
|
||||
logger = logging.getLogger()
|
||||
|
||||
logger.log(common.LOGLEVEL_FINE, 'Receive the first 2 octets of a frame')
|
||||
|
||||
first_byte = ord(receive_bytes(1))
|
||||
fin = (first_byte >> 7) & 1
|
||||
rsv1 = (first_byte >> 6) & 1
|
||||
rsv2 = (first_byte >> 5) & 1
|
||||
rsv3 = (first_byte >> 4) & 1
|
||||
opcode = first_byte & 0xf
|
||||
|
||||
second_byte = ord(receive_bytes(1))
|
||||
mask = (second_byte >> 7) & 1
|
||||
payload_length = second_byte & 0x7f
|
||||
|
||||
logger.log(
|
||||
common.LOGLEVEL_FINE, 'FIN=%s, RSV1=%s, RSV2=%s, RSV3=%s, opcode=%s, '
|
||||
'Mask=%s, Payload_length=%s', fin, rsv1, rsv2, rsv3, opcode, mask,
|
||||
payload_length)
|
||||
|
||||
if (mask == 1) != unmask_receive:
|
||||
raise InvalidFrameException(
|
||||
'Mask bit on the received frame did\'nt match masking '
|
||||
'configuration for received frames')
|
||||
|
||||
# The HyBi and later specs disallow putting a value in 0x0-0xFFFF
|
||||
# into the 8-octet extended payload length field (or 0x0-0xFD in
|
||||
# 2-octet field).
|
||||
valid_length_encoding = True
|
||||
length_encoding_bytes = 1
|
||||
if payload_length == 127:
|
||||
logger.log(common.LOGLEVEL_FINE,
|
||||
'Receive 8-octet extended payload length')
|
||||
|
||||
extended_payload_length = receive_bytes(8)
|
||||
payload_length = struct.unpack('!Q', extended_payload_length)[0]
|
||||
if payload_length > 0x7FFFFFFFFFFFFFFF:
|
||||
raise InvalidFrameException('Extended payload length >= 2^63')
|
||||
if ws_version >= 13 and payload_length < 0x10000:
|
||||
valid_length_encoding = False
|
||||
length_encoding_bytes = 8
|
||||
|
||||
logger.log(common.LOGLEVEL_FINE, 'Decoded_payload_length=%s',
|
||||
payload_length)
|
||||
elif payload_length == 126:
|
||||
logger.log(common.LOGLEVEL_FINE,
|
||||
'Receive 2-octet extended payload length')
|
||||
|
||||
extended_payload_length = receive_bytes(2)
|
||||
payload_length = struct.unpack('!H', extended_payload_length)[0]
|
||||
if ws_version >= 13 and payload_length < 126:
|
||||
valid_length_encoding = False
|
||||
length_encoding_bytes = 2
|
||||
|
||||
logger.log(common.LOGLEVEL_FINE, 'Decoded_payload_length=%s',
|
||||
payload_length)
|
||||
|
||||
if not valid_length_encoding:
|
||||
logger.warning(
|
||||
'Payload length is not encoded using the minimal number of '
|
||||
'bytes (%d is encoded using %d bytes)', payload_length,
|
||||
length_encoding_bytes)
|
||||
|
||||
if mask == 1:
|
||||
logger.log(common.LOGLEVEL_FINE, 'Receive mask')
|
||||
|
||||
masking_nonce = receive_bytes(4)
|
||||
masker = util.RepeatedXorMasker(masking_nonce)
|
||||
|
||||
logger.log(common.LOGLEVEL_FINE, 'Mask=%r', masking_nonce)
|
||||
else:
|
||||
masker = _NOOP_MASKER
|
||||
|
||||
logger.log(common.LOGLEVEL_FINE, 'Receive payload data')
|
||||
if logger.isEnabledFor(common.LOGLEVEL_FINE):
|
||||
receive_start = time.time()
|
||||
|
||||
raw_payload_bytes = receive_bytes(payload_length)
|
||||
|
||||
if logger.isEnabledFor(common.LOGLEVEL_FINE):
|
||||
logger.log(
|
||||
common.LOGLEVEL_FINE, 'Done receiving payload data at %s MB/s',
|
||||
payload_length / (time.time() - receive_start) / 1000 / 1000)
|
||||
logger.log(common.LOGLEVEL_FINE, 'Unmask payload data')
|
||||
|
||||
if logger.isEnabledFor(common.LOGLEVEL_FINE):
|
||||
unmask_start = time.time()
|
||||
|
||||
unmasked_bytes = masker.mask(raw_payload_bytes)
|
||||
|
||||
if logger.isEnabledFor(common.LOGLEVEL_FINE):
|
||||
logger.log(common.LOGLEVEL_FINE,
|
||||
'Done unmasking payload data at %s MB/s',
|
||||
payload_length / (time.time() - unmask_start) / 1000 / 1000)
|
||||
|
||||
return opcode, unmasked_bytes, fin, rsv1, rsv2, rsv3
|
||||
|
||||
|
||||
class FragmentedFrameBuilder(object):
|
||||
"""A stateful class to send a message as fragments."""
|
||||
def __init__(self, mask, frame_filters=[], encode_utf8=True):
|
||||
"""Constructs an instance."""
|
||||
|
||||
self._mask = mask
|
||||
self._frame_filters = frame_filters
|
||||
# This is for skipping UTF-8 encoding when building text type frames
|
||||
# from compressed data.
|
||||
self._encode_utf8 = encode_utf8
|
||||
|
||||
self._started = False
|
||||
|
||||
# Hold opcode of the first frame in messages to verify types of other
|
||||
# frames in the message are all the same.
|
||||
self._opcode = common.OPCODE_TEXT
|
||||
|
||||
def build(self, payload_data, end, binary):
|
||||
if binary:
|
||||
frame_type = common.OPCODE_BINARY
|
||||
else:
|
||||
frame_type = common.OPCODE_TEXT
|
||||
if self._started:
|
||||
if self._opcode != frame_type:
|
||||
raise ValueError('Message types are different in frames for '
|
||||
'the same message')
|
||||
opcode = common.OPCODE_CONTINUATION
|
||||
else:
|
||||
opcode = frame_type
|
||||
self._opcode = frame_type
|
||||
|
||||
if end:
|
||||
self._started = False
|
||||
fin = 1
|
||||
else:
|
||||
self._started = True
|
||||
fin = 0
|
||||
|
||||
if binary or not self._encode_utf8:
|
||||
return create_binary_frame(payload_data, opcode, fin, self._mask,
|
||||
self._frame_filters)
|
||||
else:
|
||||
return create_text_frame(payload_data, opcode, fin, self._mask,
|
||||
self._frame_filters)
|
||||
|
||||
|
||||
def _create_control_frame(opcode, body, mask, frame_filters):
|
||||
frame = Frame(opcode=opcode, payload=body)
|
||||
|
||||
for frame_filter in frame_filters:
|
||||
frame_filter.filter(frame)
|
||||
|
||||
if len(frame.payload) > 125:
|
||||
raise BadOperationException(
|
||||
'Payload data size of control frames must be 125 bytes or less')
|
||||
|
||||
header = create_header(frame.opcode, len(frame.payload), frame.fin,
|
||||
frame.rsv1, frame.rsv2, frame.rsv3, mask)
|
||||
return _build_frame(header, frame.payload, mask)
|
||||
|
||||
|
||||
def create_ping_frame(body, mask=False, frame_filters=[]):
|
||||
return _create_control_frame(common.OPCODE_PING, body, mask, frame_filters)
|
||||
|
||||
|
||||
def create_pong_frame(body, mask=False, frame_filters=[]):
|
||||
return _create_control_frame(common.OPCODE_PONG, body, mask, frame_filters)
|
||||
|
||||
|
||||
def create_close_frame(body, mask=False, frame_filters=[]):
|
||||
return _create_control_frame(common.OPCODE_CLOSE, body, mask,
|
||||
frame_filters)
|
||||
|
||||
|
||||
def create_closing_handshake_body(code, reason):
|
||||
body = b''
|
||||
if code is not None:
|
||||
if (code > common.STATUS_USER_PRIVATE_MAX
|
||||
or code < common.STATUS_NORMAL_CLOSURE):
|
||||
raise BadOperationException('Status code is out of range')
|
||||
if (code == common.STATUS_NO_STATUS_RECEIVED
|
||||
or code == common.STATUS_ABNORMAL_CLOSURE
|
||||
or code == common.STATUS_TLS_HANDSHAKE):
|
||||
raise BadOperationException('Status code is reserved pseudo '
|
||||
'code')
|
||||
encoded_reason = reason.encode('utf-8')
|
||||
body = struct.pack('!H', code) + encoded_reason
|
||||
return body
|
||||
|
||||
|
||||
class StreamOptions(object):
|
||||
"""Holds option values to configure Stream objects."""
|
||||
def __init__(self):
|
||||
"""Constructs StreamOptions."""
|
||||
|
||||
# Filters applied to frames.
|
||||
self.outgoing_frame_filters = []
|
||||
self.incoming_frame_filters = []
|
||||
|
||||
# Filters applied to messages. Control frames are not affected by them.
|
||||
self.outgoing_message_filters = []
|
||||
self.incoming_message_filters = []
|
||||
|
||||
self.encode_text_message_to_utf8 = True
|
||||
self.mask_send = False
|
||||
self.unmask_receive = True
|
||||
|
||||
|
||||
class Stream(object):
|
||||
"""A class for parsing/building frames of the WebSocket protocol
|
||||
(RFC 6455).
|
||||
"""
|
||||
def __init__(self, request, options):
|
||||
"""Constructs an instance.
|
||||
|
||||
Args:
|
||||
request: mod_python request.
|
||||
"""
|
||||
|
||||
self._logger = util.get_class_logger(self)
|
||||
|
||||
self._options = options
|
||||
self._request = request
|
||||
|
||||
self._request.client_terminated = False
|
||||
self._request.server_terminated = False
|
||||
|
||||
# Holds body of received fragments.
|
||||
self._received_fragments = []
|
||||
# Holds the opcode of the first fragment.
|
||||
self._original_opcode = None
|
||||
|
||||
self._writer = FragmentedFrameBuilder(
|
||||
self._options.mask_send, self._options.outgoing_frame_filters,
|
||||
self._options.encode_text_message_to_utf8)
|
||||
|
||||
self._ping_queue = deque()
|
||||
|
||||
def _read(self, length):
|
||||
"""Reads length bytes from connection. In case we catch any exception,
|
||||
prepends remote address to the exception message and raise again.
|
||||
|
||||
Raises:
|
||||
ConnectionTerminatedException: when read returns empty string.
|
||||
"""
|
||||
|
||||
try:
|
||||
read_bytes = self._request.connection.read(length)
|
||||
if not read_bytes:
|
||||
raise ConnectionTerminatedException(
|
||||
'Receiving %d byte failed. Peer (%r) closed connection' %
|
||||
(length, (self._request.connection.remote_addr, )))
|
||||
return read_bytes
|
||||
except IOError as e:
|
||||
# Also catch an IOError because mod_python throws it.
|
||||
raise ConnectionTerminatedException(
|
||||
'Receiving %d byte failed. IOError (%s) occurred' %
|
||||
(length, e))
|
||||
|
||||
def _write(self, bytes_to_write):
|
||||
"""Writes given bytes to connection. In case we catch any exception,
|
||||
prepends remote address to the exception message and raise again.
|
||||
"""
|
||||
|
||||
try:
|
||||
self._request.connection.write(bytes_to_write)
|
||||
except Exception as e:
|
||||
util.prepend_message_to_exception(
|
||||
'Failed to send message to %r: ' %
|
||||
(self._request.connection.remote_addr, ), e)
|
||||
raise
|
||||
|
||||
def receive_bytes(self, length):
|
||||
"""Receives multiple bytes. Retries read when we couldn't receive the
|
||||
specified amount. This method returns byte strings.
|
||||
|
||||
Raises:
|
||||
ConnectionTerminatedException: when read returns empty string.
|
||||
"""
|
||||
|
||||
read_bytes = []
|
||||
while length > 0:
|
||||
new_read_bytes = self._read(length)
|
||||
read_bytes.append(new_read_bytes)
|
||||
length -= len(new_read_bytes)
|
||||
return b''.join(read_bytes)
|
||||
|
||||
def _read_until(self, delim_char):
|
||||
"""Reads bytes until we encounter delim_char. The result will not
|
||||
contain delim_char.
|
||||
|
||||
Raises:
|
||||
ConnectionTerminatedException: when read returns empty string.
|
||||
"""
|
||||
|
||||
read_bytes = []
|
||||
while True:
|
||||
ch = self._read(1)
|
||||
if ch == delim_char:
|
||||
break
|
||||
read_bytes.append(ch)
|
||||
return b''.join(read_bytes)
|
||||
|
||||
def _receive_frame(self):
|
||||
"""Receives a frame and return data in the frame as a tuple containing
|
||||
each header field and payload separately.
|
||||
|
||||
Raises:
|
||||
ConnectionTerminatedException: when read returns empty
|
||||
string.
|
||||
InvalidFrameException: when the frame contains invalid data.
|
||||
"""
|
||||
def _receive_bytes(length):
|
||||
return self.receive_bytes(length)
|
||||
|
||||
return parse_frame(receive_bytes=_receive_bytes,
|
||||
logger=self._logger,
|
||||
ws_version=self._request.ws_version,
|
||||
unmask_receive=self._options.unmask_receive)
|
||||
|
||||
def _receive_frame_as_frame_object(self):
|
||||
opcode, unmasked_bytes, fin, rsv1, rsv2, rsv3 = self._receive_frame()
|
||||
|
||||
return Frame(fin=fin,
|
||||
rsv1=rsv1,
|
||||
rsv2=rsv2,
|
||||
rsv3=rsv3,
|
||||
opcode=opcode,
|
||||
payload=unmasked_bytes)
|
||||
|
||||
def receive_filtered_frame(self):
|
||||
"""Receives a frame and applies frame filters and message filters.
|
||||
The frame to be received must satisfy following conditions:
|
||||
- The frame is not fragmented.
|
||||
- The opcode of the frame is TEXT or BINARY.
|
||||
|
||||
DO NOT USE this method except for testing purpose.
|
||||
"""
|
||||
|
||||
frame = self._receive_frame_as_frame_object()
|
||||
if not frame.fin:
|
||||
raise InvalidFrameException(
|
||||
'Segmented frames must not be received via '
|
||||
'receive_filtered_frame()')
|
||||
if (frame.opcode != common.OPCODE_TEXT
|
||||
and frame.opcode != common.OPCODE_BINARY):
|
||||
raise InvalidFrameException(
|
||||
'Control frames must not be received via '
|
||||
'receive_filtered_frame()')
|
||||
|
||||
for frame_filter in self._options.incoming_frame_filters:
|
||||
frame_filter.filter(frame)
|
||||
for message_filter in self._options.incoming_message_filters:
|
||||
frame.payload = message_filter.filter(frame.payload)
|
||||
return frame
|
||||
|
||||
def send_message(self, message, end=True, binary=False):
|
||||
"""Send message.
|
||||
|
||||
Args:
|
||||
message: text in unicode or binary in str to send.
|
||||
binary: send message as binary frame.
|
||||
|
||||
Raises:
|
||||
BadOperationException: when called on a server-terminated
|
||||
connection or called with inconsistent message type or
|
||||
binary parameter.
|
||||
"""
|
||||
|
||||
if self._request.server_terminated:
|
||||
raise BadOperationException(
|
||||
'Requested send_message after sending out a closing handshake')
|
||||
|
||||
if binary and isinstance(message, six.text_type):
|
||||
raise BadOperationException(
|
||||
'Message for binary frame must not be instance of Unicode')
|
||||
|
||||
for message_filter in self._options.outgoing_message_filters:
|
||||
message = message_filter.filter(message, end, binary)
|
||||
|
||||
try:
|
||||
# Set this to any positive integer to limit maximum size of data in
|
||||
# payload data of each frame.
|
||||
MAX_PAYLOAD_DATA_SIZE = -1
|
||||
|
||||
if MAX_PAYLOAD_DATA_SIZE <= 0:
|
||||
self._write(self._writer.build(message, end, binary))
|
||||
return
|
||||
|
||||
bytes_written = 0
|
||||
while True:
|
||||
end_for_this_frame = end
|
||||
bytes_to_write = len(message) - bytes_written
|
||||
if (MAX_PAYLOAD_DATA_SIZE > 0
|
||||
and bytes_to_write > MAX_PAYLOAD_DATA_SIZE):
|
||||
end_for_this_frame = False
|
||||
bytes_to_write = MAX_PAYLOAD_DATA_SIZE
|
||||
|
||||
frame = self._writer.build(
|
||||
message[bytes_written:bytes_written + bytes_to_write],
|
||||
end_for_this_frame, binary)
|
||||
self._write(frame)
|
||||
|
||||
bytes_written += bytes_to_write
|
||||
|
||||
# This if must be placed here (the end of while block) so that
|
||||
# at least one frame is sent.
|
||||
if len(message) <= bytes_written:
|
||||
break
|
||||
except ValueError as e:
|
||||
raise BadOperationException(e)
|
||||
|
||||
def _get_message_from_frame(self, frame):
|
||||
"""Gets a message from frame. If the message is composed of fragmented
|
||||
frames and the frame is not the last fragmented frame, this method
|
||||
returns None. The whole message will be returned when the last
|
||||
fragmented frame is passed to this method.
|
||||
|
||||
Raises:
|
||||
InvalidFrameException: when the frame doesn't match defragmentation
|
||||
context, or the frame contains invalid data.
|
||||
"""
|
||||
|
||||
if frame.opcode == common.OPCODE_CONTINUATION:
|
||||
if not self._received_fragments:
|
||||
if frame.fin:
|
||||
raise InvalidFrameException(
|
||||
'Received a termination frame but fragmentation '
|
||||
'not started')
|
||||
else:
|
||||
raise InvalidFrameException(
|
||||
'Received an intermediate frame but '
|
||||
'fragmentation not started')
|
||||
|
||||
if frame.fin:
|
||||
# End of fragmentation frame
|
||||
self._received_fragments.append(frame.payload)
|
||||
message = b''.join(self._received_fragments)
|
||||
self._received_fragments = []
|
||||
return message
|
||||
else:
|
||||
# Intermediate frame
|
||||
self._received_fragments.append(frame.payload)
|
||||
return None
|
||||
else:
|
||||
if self._received_fragments:
|
||||
if frame.fin:
|
||||
raise InvalidFrameException(
|
||||
'Received an unfragmented frame without '
|
||||
'terminating existing fragmentation')
|
||||
else:
|
||||
raise InvalidFrameException(
|
||||
'New fragmentation started without terminating '
|
||||
'existing fragmentation')
|
||||
|
||||
if frame.fin:
|
||||
# Unfragmented frame
|
||||
|
||||
self._original_opcode = frame.opcode
|
||||
return frame.payload
|
||||
else:
|
||||
# Start of fragmentation frame
|
||||
|
||||
if common.is_control_opcode(frame.opcode):
|
||||
raise InvalidFrameException(
|
||||
'Control frames must not be fragmented')
|
||||
|
||||
self._original_opcode = frame.opcode
|
||||
self._received_fragments.append(frame.payload)
|
||||
return None
|
||||
|
||||
def _process_close_message(self, message):
|
||||
"""Processes close message.
|
||||
|
||||
Args:
|
||||
message: close message.
|
||||
|
||||
Raises:
|
||||
InvalidFrameException: when the message is invalid.
|
||||
"""
|
||||
|
||||
self._request.client_terminated = True
|
||||
|
||||
# Status code is optional. We can have status reason only if we
|
||||
# have status code. Status reason can be empty string. So,
|
||||
# allowed cases are
|
||||
# - no application data: no code no reason
|
||||
# - 2 octet of application data: has code but no reason
|
||||
# - 3 or more octet of application data: both code and reason
|
||||
if len(message) == 0:
|
||||
self._logger.debug('Received close frame (empty body)')
|
||||
self._request.ws_close_code = common.STATUS_NO_STATUS_RECEIVED
|
||||
elif len(message) == 1:
|
||||
raise InvalidFrameException(
|
||||
'If a close frame has status code, the length of '
|
||||
'status code must be 2 octet')
|
||||
elif len(message) >= 2:
|
||||
self._request.ws_close_code = struct.unpack('!H', message[0:2])[0]
|
||||
self._request.ws_close_reason = message[2:].decode(
|
||||
'utf-8', 'replace')
|
||||
self._logger.debug('Received close frame (code=%d, reason=%r)',
|
||||
self._request.ws_close_code,
|
||||
self._request.ws_close_reason)
|
||||
|
||||
# As we've received a close frame, no more data is coming over the
|
||||
# socket. We can now safely close the socket without worrying about
|
||||
# RST sending.
|
||||
|
||||
if self._request.server_terminated:
|
||||
self._logger.debug(
|
||||
'Received ack for server-initiated closing handshake')
|
||||
return
|
||||
|
||||
self._logger.debug('Received client-initiated closing handshake')
|
||||
|
||||
code = common.STATUS_NORMAL_CLOSURE
|
||||
reason = ''
|
||||
if hasattr(self._request, '_dispatcher'):
|
||||
dispatcher = self._request._dispatcher
|
||||
code, reason = dispatcher.passive_closing_handshake(self._request)
|
||||
if code is None and reason is not None and len(reason) > 0:
|
||||
self._logger.warning(
|
||||
'Handler specified reason despite code being None')
|
||||
reason = ''
|
||||
if reason is None:
|
||||
reason = ''
|
||||
self._send_closing_handshake(code, reason)
|
||||
self._logger.debug(
|
||||
'Acknowledged closing handshake initiated by the peer '
|
||||
'(code=%r, reason=%r)', code, reason)
|
||||
|
||||
def _process_ping_message(self, message):
|
||||
"""Processes ping message.
|
||||
|
||||
Args:
|
||||
message: ping message.
|
||||
"""
|
||||
|
||||
try:
|
||||
handler = self._request.on_ping_handler
|
||||
if handler:
|
||||
handler(self._request, message)
|
||||
return
|
||||
except AttributeError:
|
||||
pass
|
||||
self._send_pong(message)
|
||||
|
||||
def _process_pong_message(self, message):
|
||||
"""Processes pong message.
|
||||
|
||||
Args:
|
||||
message: pong message.
|
||||
"""
|
||||
|
||||
# TODO(tyoshino): Add ping timeout handling.
|
||||
|
||||
inflight_pings = deque()
|
||||
|
||||
while True:
|
||||
try:
|
||||
expected_body = self._ping_queue.popleft()
|
||||
if expected_body == message:
|
||||
# inflight_pings contains pings ignored by the
|
||||
# other peer. Just forget them.
|
||||
self._logger.debug(
|
||||
'Ping %r is acked (%d pings were ignored)',
|
||||
expected_body, len(inflight_pings))
|
||||
break
|
||||
else:
|
||||
inflight_pings.append(expected_body)
|
||||
except IndexError:
|
||||
# The received pong was unsolicited pong. Keep the
|
||||
# ping queue as is.
|
||||
self._ping_queue = inflight_pings
|
||||
self._logger.debug('Received a unsolicited pong')
|
||||
break
|
||||
|
||||
try:
|
||||
handler = self._request.on_pong_handler
|
||||
if handler:
|
||||
handler(self._request, message)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
def receive_message(self):
|
||||
"""Receive a WebSocket frame and return its payload as a text in
|
||||
unicode or a binary in str.
|
||||
|
||||
Returns:
|
||||
payload data of the frame
|
||||
- as unicode instance if received text frame
|
||||
- as str instance if received binary frame
|
||||
or None iff received closing handshake.
|
||||
Raises:
|
||||
BadOperationException: when called on a client-terminated
|
||||
connection.
|
||||
ConnectionTerminatedException: when read returns empty
|
||||
string.
|
||||
InvalidFrameException: when the frame contains invalid
|
||||
data.
|
||||
UnsupportedFrameException: when the received frame has
|
||||
flags, opcode we cannot handle. You can ignore this
|
||||
exception and continue receiving the next frame.
|
||||
"""
|
||||
|
||||
if self._request.client_terminated:
|
||||
raise BadOperationException(
|
||||
'Requested receive_message after receiving a closing '
|
||||
'handshake')
|
||||
|
||||
while True:
|
||||
# mp_conn.read will block if no bytes are available.
|
||||
|
||||
frame = self._receive_frame_as_frame_object()
|
||||
|
||||
# Check the constraint on the payload size for control frames
|
||||
# before extension processes the frame.
|
||||
# See also http://tools.ietf.org/html/rfc6455#section-5.5
|
||||
if (common.is_control_opcode(frame.opcode)
|
||||
and len(frame.payload) > 125):
|
||||
raise InvalidFrameException(
|
||||
'Payload data size of control frames must be 125 bytes or '
|
||||
'less')
|
||||
|
||||
for frame_filter in self._options.incoming_frame_filters:
|
||||
frame_filter.filter(frame)
|
||||
|
||||
if frame.rsv1 or frame.rsv2 or frame.rsv3:
|
||||
raise UnsupportedFrameException(
|
||||
'Unsupported flag is set (rsv = %d%d%d)' %
|
||||
(frame.rsv1, frame.rsv2, frame.rsv3))
|
||||
|
||||
message = self._get_message_from_frame(frame)
|
||||
if message is None:
|
||||
continue
|
||||
|
||||
for message_filter in self._options.incoming_message_filters:
|
||||
message = message_filter.filter(message)
|
||||
|
||||
if self._original_opcode == common.OPCODE_TEXT:
|
||||
# The WebSocket protocol section 4.4 specifies that invalid
|
||||
# characters must be replaced with U+fffd REPLACEMENT
|
||||
# CHARACTER.
|
||||
try:
|
||||
return message.decode('utf-8')
|
||||
except UnicodeDecodeError as e:
|
||||
raise InvalidUTF8Exception(e)
|
||||
elif self._original_opcode == common.OPCODE_BINARY:
|
||||
return message
|
||||
elif self._original_opcode == common.OPCODE_CLOSE:
|
||||
self._process_close_message(message)
|
||||
return None
|
||||
elif self._original_opcode == common.OPCODE_PING:
|
||||
self._process_ping_message(message)
|
||||
elif self._original_opcode == common.OPCODE_PONG:
|
||||
self._process_pong_message(message)
|
||||
else:
|
||||
raise UnsupportedFrameException('Opcode %d is not supported' %
|
||||
self._original_opcode)
|
||||
|
||||
def _send_closing_handshake(self, code, reason):
|
||||
body = create_closing_handshake_body(code, reason)
|
||||
frame = create_close_frame(
|
||||
body,
|
||||
mask=self._options.mask_send,
|
||||
frame_filters=self._options.outgoing_frame_filters)
|
||||
|
||||
self._request.server_terminated = True
|
||||
|
||||
self._write(frame)
|
||||
|
||||
def close_connection(self,
|
||||
code=common.STATUS_NORMAL_CLOSURE,
|
||||
reason='',
|
||||
wait_response=True):
|
||||
"""Closes a WebSocket connection. Note that this method blocks until
|
||||
it receives acknowledgement to the closing handshake.
|
||||
|
||||
Args:
|
||||
code: Status code for close frame. If code is None, a close
|
||||
frame with empty body will be sent.
|
||||
reason: string representing close reason.
|
||||
wait_response: True when caller want to wait the response.
|
||||
Raises:
|
||||
BadOperationException: when reason is specified with code None
|
||||
or reason is not an instance of both str and unicode.
|
||||
"""
|
||||
|
||||
if self._request.server_terminated:
|
||||
self._logger.debug(
|
||||
'Requested close_connection but server is already terminated')
|
||||
return
|
||||
|
||||
# When we receive a close frame, we call _process_close_message().
|
||||
# _process_close_message() immediately acknowledges to the
|
||||
# server-initiated closing handshake and sets server_terminated to
|
||||
# True. So, here we can assume that we haven't received any close
|
||||
# frame. We're initiating a closing handshake.
|
||||
|
||||
if code is None:
|
||||
if reason is not None and len(reason) > 0:
|
||||
raise BadOperationException(
|
||||
'close reason must not be specified if code is None')
|
||||
reason = ''
|
||||
else:
|
||||
if not isinstance(reason, bytes) and not isinstance(
|
||||
reason, six.text_type):
|
||||
raise BadOperationException(
|
||||
'close reason must be an instance of bytes or unicode')
|
||||
|
||||
self._send_closing_handshake(code, reason)
|
||||
self._logger.debug('Initiated closing handshake (code=%r, reason=%r)',
|
||||
code, reason)
|
||||
|
||||
if (code == common.STATUS_GOING_AWAY
|
||||
or code == common.STATUS_PROTOCOL_ERROR) or not wait_response:
|
||||
# It doesn't make sense to wait for a close frame if the reason is
|
||||
# protocol error or that the server is going away. For some of
|
||||
# other reasons, it might not make sense to wait for a close frame,
|
||||
# but it's not clear, yet.
|
||||
return
|
||||
|
||||
# TODO(ukai): 2. wait until the /client terminated/ flag has been set,
|
||||
# or until a server-defined timeout expires.
|
||||
#
|
||||
# For now, we expect receiving closing handshake right after sending
|
||||
# out closing handshake.
|
||||
message = self.receive_message()
|
||||
if message is not None:
|
||||
raise ConnectionTerminatedException(
|
||||
'Didn\'t receive valid ack for closing handshake')
|
||||
# TODO: 3. close the WebSocket connection.
|
||||
# note: mod_python Connection (mp_conn) doesn't have close method.
|
||||
|
||||
def send_ping(self, body, binary=False):
|
||||
if not binary and isinstance(body, six.text_type):
|
||||
body = body.encode('UTF-8')
|
||||
frame = create_ping_frame(body, self._options.mask_send,
|
||||
self._options.outgoing_frame_filters)
|
||||
self._write(frame)
|
||||
|
||||
self._ping_queue.append(body)
|
||||
|
||||
def _send_pong(self, body):
|
||||
frame = create_pong_frame(body, self._options.mask_send,
|
||||
self._options.outgoing_frame_filters)
|
||||
self._write(frame)
|
||||
|
||||
def get_last_received_opcode(self):
|
||||
"""Returns the opcode of the WebSocket message which the last received
|
||||
frame belongs to. The return value is valid iff immediately after
|
||||
receive_message call.
|
||||
"""
|
||||
|
||||
return self._original_opcode
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
||||
384
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/pywebsocket3/pywebsocket3/util.py
vendored
Normal file
384
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/pywebsocket3/pywebsocket3/util.py
vendored
Normal file
|
|
@ -0,0 +1,384 @@
|
|||
# Copyright 2011, Google Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following disclaimer
|
||||
# in the documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
# * Neither the name of Google Inc. nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
"""WebSocket utilities."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import struct
|
||||
import zlib
|
||||
|
||||
import six
|
||||
from six.moves import map, range
|
||||
|
||||
try:
|
||||
from pywebsocket3 import fast_masking
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
def prepend_message_to_exception(message, exc):
|
||||
"""Prepend message to the exception."""
|
||||
exc.args = (message + str(exc), )
|
||||
return
|
||||
|
||||
|
||||
def __translate_interp(interp, cygwin_path):
|
||||
"""Translate interp program path for Win32 python to run cygwin program
|
||||
(e.g. perl). Note that it doesn't support path that contains space,
|
||||
which is typically true for Unix, where #!-script is written.
|
||||
For Win32 python, cygwin_path is a directory of cygwin binaries.
|
||||
|
||||
Args:
|
||||
interp: interp command line
|
||||
cygwin_path: directory name of cygwin binary, or None
|
||||
Returns:
|
||||
translated interp command line.
|
||||
"""
|
||||
if not cygwin_path:
|
||||
return interp
|
||||
m = re.match('^[^ ]*/([^ ]+)( .*)?', interp)
|
||||
if m:
|
||||
cmd = os.path.join(cygwin_path, m.group(1))
|
||||
return cmd + m.group(2)
|
||||
return interp
|
||||
|
||||
|
||||
def get_script_interp(script_path, cygwin_path=None):
|
||||
r"""Get #!-interpreter command line from the script.
|
||||
|
||||
It also fixes command path. When Cygwin Python is used, e.g. in WebKit,
|
||||
it could run "/usr/bin/perl -wT hello.pl".
|
||||
When Win32 Python is used, e.g. in Chromium, it couldn't. So, fix
|
||||
"/usr/bin/perl" to "<cygwin_path>\perl.exe".
|
||||
|
||||
Args:
|
||||
script_path: pathname of the script
|
||||
cygwin_path: directory name of cygwin binary, or None
|
||||
Returns:
|
||||
#!-interpreter command line, or None if it is not #!-script.
|
||||
"""
|
||||
fp = open(script_path)
|
||||
line = fp.readline()
|
||||
fp.close()
|
||||
m = re.match('^#!(.*)', line)
|
||||
if m:
|
||||
return __translate_interp(m.group(1), cygwin_path)
|
||||
return None
|
||||
|
||||
|
||||
def hexify(s):
|
||||
return ' '.join(['%02x' % x for x in six.iterbytes(s)])
|
||||
|
||||
|
||||
def get_class_logger(o):
|
||||
"""Return the logging class information."""
|
||||
return logging.getLogger('%s.%s' %
|
||||
(o.__class__.__module__, o.__class__.__name__))
|
||||
|
||||
|
||||
def pack_byte(b):
|
||||
"""Pack an integer to network-ordered byte"""
|
||||
return struct.pack('!B', b)
|
||||
|
||||
|
||||
class NoopMasker(object):
|
||||
"""A NoOp masking object.
|
||||
|
||||
This has the same interface as RepeatedXorMasker but just returns
|
||||
the string passed in without making any change.
|
||||
"""
|
||||
def __init__(self):
|
||||
"""NoOp."""
|
||||
pass
|
||||
|
||||
def mask(self, s):
|
||||
"""NoOp."""
|
||||
return s
|
||||
|
||||
|
||||
class RepeatedXorMasker(object):
|
||||
"""A masking object that applies XOR on the string.
|
||||
|
||||
Applies XOR on the byte string given to mask method with the masking bytes
|
||||
given to the constructor repeatedly. This object remembers the position
|
||||
in the masking bytes the last mask method call ended and resumes from
|
||||
that point on the next mask method call.
|
||||
"""
|
||||
def __init__(self, masking_key):
|
||||
self._masking_key = masking_key
|
||||
self._masking_key_index = 0
|
||||
|
||||
def _mask_using_swig(self, s):
|
||||
"""Perform the mask via SWIG."""
|
||||
masked_data = fast_masking.mask(s, self._masking_key,
|
||||
self._masking_key_index)
|
||||
self._masking_key_index = ((self._masking_key_index + len(s)) %
|
||||
len(self._masking_key))
|
||||
return masked_data
|
||||
|
||||
def _mask_using_array(self, s):
|
||||
"""Perform the mask via python."""
|
||||
if isinstance(s, six.text_type):
|
||||
raise Exception(
|
||||
'Masking Operation should not process unicode strings')
|
||||
|
||||
result = bytearray(s)
|
||||
|
||||
# Use temporary local variables to eliminate the cost to access
|
||||
# attributes
|
||||
masking_key = [c for c in six.iterbytes(self._masking_key)]
|
||||
masking_key_size = len(masking_key)
|
||||
masking_key_index = self._masking_key_index
|
||||
|
||||
for i in range(len(result)):
|
||||
result[i] ^= masking_key[masking_key_index]
|
||||
masking_key_index = (masking_key_index + 1) % masking_key_size
|
||||
|
||||
self._masking_key_index = masking_key_index
|
||||
|
||||
return bytes(result)
|
||||
|
||||
if 'fast_masking' in globals():
|
||||
mask = _mask_using_swig
|
||||
else:
|
||||
mask = _mask_using_array
|
||||
|
||||
|
||||
# By making wbits option negative, we can suppress CMF/FLG (2 octet) and
|
||||
# ADLER32 (4 octet) fields of zlib so that we can use zlib module just as
|
||||
# deflate library. DICTID won't be added as far as we don't set dictionary.
|
||||
# LZ77 window of 32K will be used for both compression and decompression.
|
||||
# For decompression, we can just use 32K to cover any windows size. For
|
||||
# compression, we use 32K so receivers must use 32K.
|
||||
#
|
||||
# Compression level is Z_DEFAULT_COMPRESSION. We don't have to match level
|
||||
# to decode.
|
||||
#
|
||||
# See zconf.h, deflate.cc, inflate.cc of zlib library, and zlibmodule.c of
|
||||
# Python. See also RFC1950 (ZLIB 3.3).
|
||||
|
||||
|
||||
class _Deflater(object):
|
||||
def __init__(self, window_bits):
|
||||
self._logger = get_class_logger(self)
|
||||
|
||||
# Using the smallest window bits of 9 for generating input frames.
|
||||
# On WebSocket spec, the smallest window bit is 8. However, zlib does
|
||||
# not accept window_bit = 8.
|
||||
#
|
||||
# Because of a zlib deflate quirk, back-references will not use the
|
||||
# entire range of 1 << window_bits, but will instead use a restricted
|
||||
# range of (1 << window_bits) - 262. With an increased window_bits = 9,
|
||||
# back-references will be within a range of 250. These can still be
|
||||
# decompressed with window_bits = 8 and the 256-byte window used there.
|
||||
#
|
||||
# Similar disscussions can be found in https://crbug.com/691074
|
||||
window_bits = max(window_bits, 9)
|
||||
|
||||
self._compress = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION,
|
||||
zlib.DEFLATED, -window_bits)
|
||||
|
||||
def compress(self, bytes):
|
||||
compressed_bytes = self._compress.compress(bytes)
|
||||
self._logger.debug('Compress input %r', bytes)
|
||||
self._logger.debug('Compress result %r', compressed_bytes)
|
||||
return compressed_bytes
|
||||
|
||||
def compress_and_flush(self, bytes):
|
||||
compressed_bytes = self._compress.compress(bytes)
|
||||
compressed_bytes += self._compress.flush(zlib.Z_SYNC_FLUSH)
|
||||
self._logger.debug('Compress input %r', bytes)
|
||||
self._logger.debug('Compress result %r', compressed_bytes)
|
||||
return compressed_bytes
|
||||
|
||||
def compress_and_finish(self, bytes):
|
||||
compressed_bytes = self._compress.compress(bytes)
|
||||
compressed_bytes += self._compress.flush(zlib.Z_FINISH)
|
||||
self._logger.debug('Compress input %r', bytes)
|
||||
self._logger.debug('Compress result %r', compressed_bytes)
|
||||
return compressed_bytes
|
||||
|
||||
|
||||
class _Inflater(object):
|
||||
def __init__(self, window_bits):
|
||||
self._logger = get_class_logger(self)
|
||||
self._window_bits = window_bits
|
||||
|
||||
self._unconsumed = b''
|
||||
|
||||
self.reset()
|
||||
|
||||
def decompress(self, size):
|
||||
if not (size == -1 or size > 0):
|
||||
raise Exception('size must be -1 or positive')
|
||||
|
||||
data = b''
|
||||
|
||||
while True:
|
||||
data += self._decompress.decompress(self._unconsumed,
|
||||
max(0, size - len(data)))
|
||||
self._unconsumed = self._decompress.unconsumed_tail
|
||||
if self._decompress.unused_data:
|
||||
# Encountered a last block (i.e. a block with BFINAL = 1) and
|
||||
# found a new stream (unused_data). We cannot use the same
|
||||
# zlib.Decompress object for the new stream. Create a new
|
||||
# Decompress object to decompress the new one.
|
||||
#
|
||||
# It's fine to ignore unconsumed_tail if unused_data is not
|
||||
# empty.
|
||||
self._unconsumed = self._decompress.unused_data
|
||||
self.reset()
|
||||
if size >= 0 and len(data) == size:
|
||||
# data is filled. Don't call decompress again.
|
||||
break
|
||||
else:
|
||||
# Re-invoke Decompress.decompress to try to decompress all
|
||||
# available bytes before invoking read which blocks until
|
||||
# any new byte is available.
|
||||
continue
|
||||
else:
|
||||
# Here, since unused_data is empty, even if unconsumed_tail is
|
||||
# not empty, bytes of requested length are already in data. We
|
||||
# don't have to "continue" here.
|
||||
break
|
||||
|
||||
if data:
|
||||
self._logger.debug('Decompressed %r', data)
|
||||
return data
|
||||
|
||||
def append(self, data):
|
||||
self._logger.debug('Appended %r', data)
|
||||
self._unconsumed += data
|
||||
|
||||
def reset(self):
|
||||
self._logger.debug('Reset')
|
||||
self._decompress = zlib.decompressobj(-self._window_bits)
|
||||
|
||||
|
||||
# Compresses/decompresses given octets using the method introduced in RFC1979.
|
||||
|
||||
|
||||
class _RFC1979Deflater(object):
|
||||
"""A compressor class that applies DEFLATE to given byte sequence and
|
||||
flushes using the algorithm described in the RFC1979 section 2.1.
|
||||
"""
|
||||
def __init__(self, window_bits, no_context_takeover):
|
||||
self._deflater = None
|
||||
if window_bits is None:
|
||||
window_bits = zlib.MAX_WBITS
|
||||
self._window_bits = window_bits
|
||||
self._no_context_takeover = no_context_takeover
|
||||
|
||||
def filter(self, bytes, end=True, bfinal=False):
|
||||
if self._deflater is None:
|
||||
self._deflater = _Deflater(self._window_bits)
|
||||
|
||||
if bfinal:
|
||||
result = self._deflater.compress_and_finish(bytes)
|
||||
# Add a padding block with BFINAL = 0 and BTYPE = 0.
|
||||
result = result + pack_byte(0)
|
||||
self._deflater = None
|
||||
return result
|
||||
|
||||
result = self._deflater.compress_and_flush(bytes)
|
||||
if end:
|
||||
# Strip last 4 octets which is LEN and NLEN field of a
|
||||
# non-compressed block added for Z_SYNC_FLUSH.
|
||||
result = result[:-4]
|
||||
|
||||
if self._no_context_takeover and end:
|
||||
self._deflater = None
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class _RFC1979Inflater(object):
|
||||
"""A decompressor class a la RFC1979.
|
||||
|
||||
A decompressor class for byte sequence compressed and flushed following
|
||||
the algorithm described in the RFC1979 section 2.1.
|
||||
"""
|
||||
def __init__(self, window_bits=zlib.MAX_WBITS):
|
||||
self._inflater = _Inflater(window_bits)
|
||||
|
||||
def filter(self, bytes):
|
||||
# Restore stripped LEN and NLEN field of a non-compressed block added
|
||||
# for Z_SYNC_FLUSH.
|
||||
self._inflater.append(bytes + b'\x00\x00\xff\xff')
|
||||
return self._inflater.decompress(-1)
|
||||
|
||||
|
||||
class DeflateSocket(object):
|
||||
"""A wrapper class for socket object to intercept send and recv to perform
|
||||
deflate compression and decompression transparently.
|
||||
"""
|
||||
|
||||
# Size of the buffer passed to recv to receive compressed data.
|
||||
_RECV_SIZE = 4096
|
||||
|
||||
def __init__(self, socket):
|
||||
self._socket = socket
|
||||
|
||||
self._logger = get_class_logger(self)
|
||||
|
||||
self._deflater = _Deflater(zlib.MAX_WBITS)
|
||||
self._inflater = _Inflater(zlib.MAX_WBITS)
|
||||
|
||||
def recv(self, size):
|
||||
"""Receives data from the socket specified on the construction up
|
||||
to the specified size. Once any data is available, returns it even
|
||||
if it's smaller than the specified size.
|
||||
"""
|
||||
|
||||
# TODO(tyoshino): Allow call with size=0. It should block until any
|
||||
# decompressed data is available.
|
||||
if size <= 0:
|
||||
raise Exception('Non-positive size passed')
|
||||
while True:
|
||||
data = self._inflater.decompress(size)
|
||||
if len(data) != 0:
|
||||
return data
|
||||
|
||||
read_data = self._socket.recv(DeflateSocket._RECV_SIZE)
|
||||
if not read_data:
|
||||
return b''
|
||||
self._inflater.append(read_data)
|
||||
|
||||
def sendall(self, bytes):
|
||||
self.send(bytes)
|
||||
|
||||
def send(self, bytes):
|
||||
self._socket.sendall(self._deflater.compress_and_flush(bytes))
|
||||
return len(bytes)
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
||||
|
|
@ -0,0 +1,290 @@
|
|||
# Copyright 2020, Google Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following disclaimer
|
||||
# in the documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
# * Neither the name of Google Inc. nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
"""Standalone WebsocketServer
|
||||
|
||||
This file deals with the main module of standalone server. Although it is fine
|
||||
to import this file directly to use WebSocketServer, it is strongly recommended
|
||||
to use standalone.py, since it is intended to act as a skeleton of this module.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
import re
|
||||
import select
|
||||
import socket
|
||||
import ssl
|
||||
import threading
|
||||
import traceback
|
||||
|
||||
from six.moves import BaseHTTPServer, socketserver
|
||||
|
||||
from pywebsocket3 import dispatch, util
|
||||
from pywebsocket3.request_handler import WebSocketRequestHandler
|
||||
|
||||
|
||||
def _alias_handlers(dispatcher, websock_handlers_map_file):
|
||||
"""Set aliases specified in websock_handler_map_file in dispatcher.
|
||||
|
||||
Args:
|
||||
dispatcher: dispatch.Dispatcher instance
|
||||
websock_handler_map_file: alias map file
|
||||
"""
|
||||
|
||||
with open(websock_handlers_map_file) as f:
|
||||
for line in f:
|
||||
if line[0] == '#' or line.isspace():
|
||||
continue
|
||||
m = re.match(r'(\S+)\s+(\S+)$', line)
|
||||
if not m:
|
||||
logging.warning('Wrong format in map file:' + line)
|
||||
continue
|
||||
try:
|
||||
dispatcher.add_resource_path_alias(m.group(1), m.group(2))
|
||||
except dispatch.DispatchException as e:
|
||||
logging.error(str(e))
|
||||
|
||||
|
||||
class WebSocketServer(socketserver.ThreadingMixIn, BaseHTTPServer.HTTPServer):
|
||||
"""HTTPServer specialized for WebSocket."""
|
||||
|
||||
# Overrides SocketServer.ThreadingMixIn.daemon_threads
|
||||
daemon_threads = True
|
||||
# Overrides BaseHTTPServer.HTTPServer.allow_reuse_address
|
||||
allow_reuse_address = True
|
||||
|
||||
def __init__(self, options):
|
||||
"""Override SocketServer.TCPServer.__init__ to set SSL enabled
|
||||
socket object to self.socket before server_bind and server_activate,
|
||||
if necessary.
|
||||
"""
|
||||
|
||||
# Fall back to None for embedders that don't know about the
|
||||
# handler_encoding option.
|
||||
handler_encoding = getattr(options, "handler_encoding", None)
|
||||
|
||||
# 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,
|
||||
options.allow_handlers_outside_root_dir, handler_encoding)
|
||||
if options.websock_handlers_map_file:
|
||||
_alias_handlers(options.dispatcher,
|
||||
options.websock_handlers_map_file)
|
||||
warnings = options.dispatcher.source_warnings()
|
||||
if warnings:
|
||||
for warning in warnings:
|
||||
logging.warning('Warning in source loading: %s' % warning)
|
||||
|
||||
self._logger = util.get_class_logger(self)
|
||||
|
||||
self.request_queue_size = options.request_queue_size
|
||||
self.__ws_is_shut_down = threading.Event()
|
||||
self.__ws_serving = False
|
||||
|
||||
socketserver.BaseServer.__init__(self,
|
||||
(options.server_host, options.port),
|
||||
WebSocketRequestHandler)
|
||||
|
||||
# Expose the options object to allow handler objects access it. We name
|
||||
# it with websocket_ prefix to avoid conflict.
|
||||
self.websocket_server_options = options
|
||||
|
||||
self._create_sockets()
|
||||
self.server_bind()
|
||||
self.server_activate()
|
||||
|
||||
def _create_sockets(self):
|
||||
self.server_name, self.server_port = self.server_address
|
||||
self._sockets = []
|
||||
if not self.server_name:
|
||||
# On platforms that doesn't support IPv6, the first bind fails.
|
||||
# On platforms that supports IPv6
|
||||
# - If it binds both IPv4 and IPv6 on call with AF_INET6, the
|
||||
# first bind succeeds and the second fails (we'll see 'Address
|
||||
# already in use' error).
|
||||
# - If it binds only IPv6 on call with AF_INET6, both call are
|
||||
# expected to succeed to listen both protocol.
|
||||
addrinfo_array = [(socket.AF_INET6, socket.SOCK_STREAM, '', '',
|
||||
''),
|
||||
(socket.AF_INET, socket.SOCK_STREAM, '', '', '')]
|
||||
else:
|
||||
addrinfo_array = socket.getaddrinfo(self.server_name,
|
||||
self.server_port,
|
||||
socket.AF_UNSPEC,
|
||||
socket.SOCK_STREAM,
|
||||
socket.IPPROTO_TCP)
|
||||
for addrinfo in addrinfo_array:
|
||||
self._logger.info('Create socket on: %r', addrinfo)
|
||||
family, socktype, proto, canonname, sockaddr = addrinfo
|
||||
try:
|
||||
socket_ = socket.socket(family, socktype)
|
||||
except Exception as e:
|
||||
self._logger.info('Skip by failure: %r', e)
|
||||
continue
|
||||
server_options = self.websocket_server_options
|
||||
if server_options.use_tls:
|
||||
if server_options.tls_client_auth:
|
||||
if server_options.tls_client_cert_optional:
|
||||
client_cert_ = ssl.CERT_OPTIONAL
|
||||
else:
|
||||
client_cert_ = ssl.CERT_REQUIRED
|
||||
else:
|
||||
client_cert_ = ssl.CERT_NONE
|
||||
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
ssl_context.verify_mode = client_cert_
|
||||
ssl_context.load_cert_chain(keyfile=server_options.private_key,
|
||||
certfile=server_options.certificate)
|
||||
if client_cert_ != ssl.CERT_NONE:
|
||||
ssl_context.load_verify_locations(cafile=server_options.tls_client_ca)
|
||||
socket_ = ssl_context.wrap_socket(socket_, server_side=True)
|
||||
self._sockets.append((socket_, addrinfo))
|
||||
|
||||
def server_bind(self):
|
||||
"""Override SocketServer.TCPServer.server_bind to enable multiple
|
||||
sockets bind.
|
||||
"""
|
||||
|
||||
failed_sockets = []
|
||||
|
||||
for socketinfo in self._sockets:
|
||||
socket_, addrinfo = socketinfo
|
||||
self._logger.info('Bind on: %r', addrinfo)
|
||||
if self.allow_reuse_address:
|
||||
socket_.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
try:
|
||||
socket_.bind(self.server_address)
|
||||
except Exception as e:
|
||||
self._logger.info('Skip by failure: %r', e)
|
||||
socket_.close()
|
||||
failed_sockets.append(socketinfo)
|
||||
if self.server_address[1] == 0:
|
||||
# The operating system assigns the actual port number for port
|
||||
# number 0. This case, the second and later sockets should use
|
||||
# the same port number. Also self.server_port is rewritten
|
||||
# because it is exported, and will be used by external code.
|
||||
self.server_address = (self.server_name,
|
||||
socket_.getsockname()[1])
|
||||
self.server_port = self.server_address[1]
|
||||
self._logger.info('Port %r is assigned', self.server_port)
|
||||
|
||||
for socketinfo in failed_sockets:
|
||||
self._sockets.remove(socketinfo)
|
||||
|
||||
def server_activate(self):
|
||||
"""Override SocketServer.TCPServer.server_activate to enable multiple
|
||||
sockets listen.
|
||||
"""
|
||||
|
||||
failed_sockets = []
|
||||
|
||||
for socketinfo in self._sockets:
|
||||
socket_, addrinfo = socketinfo
|
||||
self._logger.info('Listen on: %r', addrinfo)
|
||||
try:
|
||||
socket_.listen(self.request_queue_size)
|
||||
except Exception as e:
|
||||
self._logger.info('Skip by failure: %r', e)
|
||||
socket_.close()
|
||||
failed_sockets.append(socketinfo)
|
||||
|
||||
for socketinfo in failed_sockets:
|
||||
self._sockets.remove(socketinfo)
|
||||
|
||||
if len(self._sockets) == 0:
|
||||
self._logger.critical(
|
||||
'No sockets activated. Use info log level to see the reason.')
|
||||
|
||||
def server_close(self):
|
||||
"""Override SocketServer.TCPServer.server_close to enable multiple
|
||||
sockets close.
|
||||
"""
|
||||
|
||||
for socketinfo in self._sockets:
|
||||
socket_, addrinfo = socketinfo
|
||||
self._logger.info('Close on: %r', addrinfo)
|
||||
socket_.close()
|
||||
|
||||
def fileno(self):
|
||||
"""Override SocketServer.TCPServer.fileno."""
|
||||
|
||||
self._logger.critical('Not supported: fileno')
|
||||
return self._sockets[0][0].fileno()
|
||||
|
||||
def handle_error(self, request, client_address):
|
||||
"""Override SocketServer.handle_error."""
|
||||
|
||||
self._logger.error('Exception in processing request from: %r\n%s',
|
||||
client_address, traceback.format_exc())
|
||||
# Note: client_address is a tuple.
|
||||
|
||||
def get_request(self):
|
||||
"""Override TCPServer.get_request."""
|
||||
|
||||
accepted_socket, client_address = self.socket.accept()
|
||||
|
||||
server_options = self.websocket_server_options
|
||||
if server_options.use_tls:
|
||||
# Print cipher in use. Handshake is done on accept.
|
||||
self._logger.debug('Cipher: %s', accepted_socket.cipher())
|
||||
self._logger.debug('Client cert: %r',
|
||||
accepted_socket.getpeercert())
|
||||
|
||||
return accepted_socket, client_address
|
||||
|
||||
def serve_forever(self, poll_interval=0.5):
|
||||
"""Override SocketServer.BaseServer.serve_forever."""
|
||||
|
||||
self.__ws_serving = True
|
||||
self.__ws_is_shut_down.clear()
|
||||
handle_request = self.handle_request
|
||||
if hasattr(self, '_handle_request_noblock'):
|
||||
handle_request = self._handle_request_noblock
|
||||
else:
|
||||
self._logger.warning('Fallback to blocking request handler')
|
||||
try:
|
||||
while self.__ws_serving:
|
||||
r, w, e = select.select(
|
||||
[socket_[0] for socket_ in self._sockets], [], [],
|
||||
poll_interval)
|
||||
for socket_ in r:
|
||||
self.socket = socket_
|
||||
handle_request()
|
||||
self.socket = None
|
||||
finally:
|
||||
self.__ws_is_shut_down.set()
|
||||
|
||||
def shutdown(self):
|
||||
"""Override SocketServer.BaseServer.shutdown."""
|
||||
|
||||
self.__ws_serving = False
|
||||
self.__ws_is_shut_down.wait()
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
||||
998
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/six/six.py
vendored
Normal file
998
Tests/LibWeb/Text/input/wpt-import/_wpttools/third_party/six/six.py
vendored
Normal file
|
|
@ -0,0 +1,998 @@
|
|||
# Copyright (c) 2010-2020 Benjamin Peterson
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
"""Utilities for writing code that runs on Python 2 and 3"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import functools
|
||||
import itertools
|
||||
import operator
|
||||
import sys
|
||||
import types
|
||||
|
||||
__author__ = "Benjamin Peterson <benjamin@python.org>"
|
||||
__version__ = "1.16.0"
|
||||
|
||||
|
||||
# Useful for very coarse version differentiation.
|
||||
PY2 = sys.version_info[0] == 2
|
||||
PY3 = sys.version_info[0] == 3
|
||||
PY34 = sys.version_info[0:2] >= (3, 4)
|
||||
|
||||
if PY3:
|
||||
string_types = str,
|
||||
integer_types = int,
|
||||
class_types = type,
|
||||
text_type = str
|
||||
binary_type = bytes
|
||||
|
||||
MAXSIZE = sys.maxsize
|
||||
else:
|
||||
string_types = basestring,
|
||||
integer_types = (int, long)
|
||||
class_types = (type, types.ClassType)
|
||||
text_type = unicode
|
||||
binary_type = str
|
||||
|
||||
if sys.platform.startswith("java"):
|
||||
# Jython always uses 32 bits.
|
||||
MAXSIZE = int((1 << 31) - 1)
|
||||
else:
|
||||
# It's possible to have sizeof(long) != sizeof(Py_ssize_t).
|
||||
class X(object):
|
||||
|
||||
def __len__(self):
|
||||
return 1 << 31
|
||||
try:
|
||||
len(X())
|
||||
except OverflowError:
|
||||
# 32-bit
|
||||
MAXSIZE = int((1 << 31) - 1)
|
||||
else:
|
||||
# 64-bit
|
||||
MAXSIZE = int((1 << 63) - 1)
|
||||
del X
|
||||
|
||||
if PY34:
|
||||
from importlib.util import spec_from_loader
|
||||
else:
|
||||
spec_from_loader = None
|
||||
|
||||
|
||||
def _add_doc(func, doc):
|
||||
"""Add documentation to a function."""
|
||||
func.__doc__ = doc
|
||||
|
||||
|
||||
def _import_module(name):
|
||||
"""Import module, returning the module after the last dot."""
|
||||
__import__(name)
|
||||
return sys.modules[name]
|
||||
|
||||
|
||||
class _LazyDescr(object):
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def __get__(self, obj, tp):
|
||||
result = self._resolve()
|
||||
setattr(obj, self.name, result) # Invokes __set__.
|
||||
try:
|
||||
# This is a bit ugly, but it avoids running this again by
|
||||
# removing this descriptor.
|
||||
delattr(obj.__class__, self.name)
|
||||
except AttributeError:
|
||||
pass
|
||||
return result
|
||||
|
||||
|
||||
class MovedModule(_LazyDescr):
|
||||
|
||||
def __init__(self, name, old, new=None):
|
||||
super(MovedModule, self).__init__(name)
|
||||
if PY3:
|
||||
if new is None:
|
||||
new = name
|
||||
self.mod = new
|
||||
else:
|
||||
self.mod = old
|
||||
|
||||
def _resolve(self):
|
||||
return _import_module(self.mod)
|
||||
|
||||
def __getattr__(self, attr):
|
||||
_module = self._resolve()
|
||||
value = getattr(_module, attr)
|
||||
setattr(self, attr, value)
|
||||
return value
|
||||
|
||||
|
||||
class _LazyModule(types.ModuleType):
|
||||
|
||||
def __init__(self, name):
|
||||
super(_LazyModule, self).__init__(name)
|
||||
self.__doc__ = self.__class__.__doc__
|
||||
|
||||
def __dir__(self):
|
||||
attrs = ["__doc__", "__name__"]
|
||||
attrs += [attr.name for attr in self._moved_attributes]
|
||||
return attrs
|
||||
|
||||
# Subclasses should override this
|
||||
_moved_attributes = []
|
||||
|
||||
|
||||
class MovedAttribute(_LazyDescr):
|
||||
|
||||
def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None):
|
||||
super(MovedAttribute, self).__init__(name)
|
||||
if PY3:
|
||||
if new_mod is None:
|
||||
new_mod = name
|
||||
self.mod = new_mod
|
||||
if new_attr is None:
|
||||
if old_attr is None:
|
||||
new_attr = name
|
||||
else:
|
||||
new_attr = old_attr
|
||||
self.attr = new_attr
|
||||
else:
|
||||
self.mod = old_mod
|
||||
if old_attr is None:
|
||||
old_attr = name
|
||||
self.attr = old_attr
|
||||
|
||||
def _resolve(self):
|
||||
module = _import_module(self.mod)
|
||||
return getattr(module, self.attr)
|
||||
|
||||
|
||||
class _SixMetaPathImporter(object):
|
||||
|
||||
"""
|
||||
A meta path importer to import six.moves and its submodules.
|
||||
|
||||
This class implements a PEP302 finder and loader. It should be compatible
|
||||
with Python 2.5 and all existing versions of Python3
|
||||
"""
|
||||
|
||||
def __init__(self, six_module_name):
|
||||
self.name = six_module_name
|
||||
self.known_modules = {}
|
||||
|
||||
def _add_module(self, mod, *fullnames):
|
||||
for fullname in fullnames:
|
||||
self.known_modules[self.name + "." + fullname] = mod
|
||||
|
||||
def _get_module(self, fullname):
|
||||
return self.known_modules[self.name + "." + fullname]
|
||||
|
||||
def find_module(self, fullname, path=None):
|
||||
if fullname in self.known_modules:
|
||||
return self
|
||||
return None
|
||||
|
||||
def find_spec(self, fullname, path, target=None):
|
||||
if fullname in self.known_modules:
|
||||
return spec_from_loader(fullname, self)
|
||||
return None
|
||||
|
||||
def __get_module(self, fullname):
|
||||
try:
|
||||
return self.known_modules[fullname]
|
||||
except KeyError:
|
||||
raise ImportError("This loader does not know module " + fullname)
|
||||
|
||||
def load_module(self, fullname):
|
||||
try:
|
||||
# in case of a reload
|
||||
return sys.modules[fullname]
|
||||
except KeyError:
|
||||
pass
|
||||
mod = self.__get_module(fullname)
|
||||
if isinstance(mod, MovedModule):
|
||||
mod = mod._resolve()
|
||||
else:
|
||||
mod.__loader__ = self
|
||||
sys.modules[fullname] = mod
|
||||
return mod
|
||||
|
||||
def is_package(self, fullname):
|
||||
"""
|
||||
Return true, if the named module is a package.
|
||||
|
||||
We need this method to get correct spec objects with
|
||||
Python 3.4 (see PEP451)
|
||||
"""
|
||||
return hasattr(self.__get_module(fullname), "__path__")
|
||||
|
||||
def get_code(self, fullname):
|
||||
"""Return None
|
||||
|
||||
Required, if is_package is implemented"""
|
||||
self.__get_module(fullname) # eventually raises ImportError
|
||||
return None
|
||||
get_source = get_code # same as get_code
|
||||
|
||||
def create_module(self, spec):
|
||||
return self.load_module(spec.name)
|
||||
|
||||
def exec_module(self, module):
|
||||
pass
|
||||
|
||||
_importer = _SixMetaPathImporter(__name__)
|
||||
|
||||
|
||||
class _MovedItems(_LazyModule):
|
||||
|
||||
"""Lazy loading of moved objects"""
|
||||
__path__ = [] # mark as package
|
||||
|
||||
|
||||
_moved_attributes = [
|
||||
MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"),
|
||||
MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"),
|
||||
MovedAttribute("filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse"),
|
||||
MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"),
|
||||
MovedAttribute("intern", "__builtin__", "sys"),
|
||||
MovedAttribute("map", "itertools", "builtins", "imap", "map"),
|
||||
MovedAttribute("getcwd", "os", "os", "getcwdu", "getcwd"),
|
||||
MovedAttribute("getcwdb", "os", "os", "getcwd", "getcwdb"),
|
||||
MovedAttribute("getoutput", "commands", "subprocess"),
|
||||
MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"),
|
||||
MovedAttribute("reload_module", "__builtin__", "importlib" if PY34 else "imp", "reload"),
|
||||
MovedAttribute("reduce", "__builtin__", "functools"),
|
||||
MovedAttribute("shlex_quote", "pipes", "shlex", "quote"),
|
||||
MovedAttribute("StringIO", "StringIO", "io"),
|
||||
MovedAttribute("UserDict", "UserDict", "collections"),
|
||||
MovedAttribute("UserList", "UserList", "collections"),
|
||||
MovedAttribute("UserString", "UserString", "collections"),
|
||||
MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"),
|
||||
MovedAttribute("zip", "itertools", "builtins", "izip", "zip"),
|
||||
MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"),
|
||||
MovedModule("builtins", "__builtin__"),
|
||||
MovedModule("configparser", "ConfigParser"),
|
||||
MovedModule("collections_abc", "collections", "collections.abc" if sys.version_info >= (3, 3) else "collections"),
|
||||
MovedModule("copyreg", "copy_reg"),
|
||||
MovedModule("dbm_gnu", "gdbm", "dbm.gnu"),
|
||||
MovedModule("dbm_ndbm", "dbm", "dbm.ndbm"),
|
||||
MovedModule("_dummy_thread", "dummy_thread", "_dummy_thread" if sys.version_info < (3, 9) else "_thread"),
|
||||
MovedModule("http_cookiejar", "cookielib", "http.cookiejar"),
|
||||
MovedModule("http_cookies", "Cookie", "http.cookies"),
|
||||
MovedModule("html_entities", "htmlentitydefs", "html.entities"),
|
||||
MovedModule("html_parser", "HTMLParser", "html.parser"),
|
||||
MovedModule("http_client", "httplib", "http.client"),
|
||||
MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"),
|
||||
MovedModule("email_mime_image", "email.MIMEImage", "email.mime.image"),
|
||||
MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"),
|
||||
MovedModule("email_mime_nonmultipart", "email.MIMENonMultipart", "email.mime.nonmultipart"),
|
||||
MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"),
|
||||
MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"),
|
||||
MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"),
|
||||
MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"),
|
||||
MovedModule("cPickle", "cPickle", "pickle"),
|
||||
MovedModule("queue", "Queue"),
|
||||
MovedModule("reprlib", "repr"),
|
||||
MovedModule("socketserver", "SocketServer"),
|
||||
MovedModule("_thread", "thread", "_thread"),
|
||||
MovedModule("tkinter", "Tkinter"),
|
||||
MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"),
|
||||
MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"),
|
||||
MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"),
|
||||
MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"),
|
||||
MovedModule("tkinter_tix", "Tix", "tkinter.tix"),
|
||||
MovedModule("tkinter_ttk", "ttk", "tkinter.ttk"),
|
||||
MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"),
|
||||
MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"),
|
||||
MovedModule("tkinter_colorchooser", "tkColorChooser",
|
||||
"tkinter.colorchooser"),
|
||||
MovedModule("tkinter_commondialog", "tkCommonDialog",
|
||||
"tkinter.commondialog"),
|
||||
MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"),
|
||||
MovedModule("tkinter_font", "tkFont", "tkinter.font"),
|
||||
MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"),
|
||||
MovedModule("tkinter_tksimpledialog", "tkSimpleDialog",
|
||||
"tkinter.simpledialog"),
|
||||
MovedModule("urllib_parse", __name__ + ".moves.urllib_parse", "urllib.parse"),
|
||||
MovedModule("urllib_error", __name__ + ".moves.urllib_error", "urllib.error"),
|
||||
MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"),
|
||||
MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"),
|
||||
MovedModule("xmlrpc_client", "xmlrpclib", "xmlrpc.client"),
|
||||
MovedModule("xmlrpc_server", "SimpleXMLRPCServer", "xmlrpc.server"),
|
||||
]
|
||||
# Add windows specific modules.
|
||||
if sys.platform == "win32":
|
||||
_moved_attributes += [
|
||||
MovedModule("winreg", "_winreg"),
|
||||
]
|
||||
|
||||
for attr in _moved_attributes:
|
||||
setattr(_MovedItems, attr.name, attr)
|
||||
if isinstance(attr, MovedModule):
|
||||
_importer._add_module(attr, "moves." + attr.name)
|
||||
del attr
|
||||
|
||||
_MovedItems._moved_attributes = _moved_attributes
|
||||
|
||||
moves = _MovedItems(__name__ + ".moves")
|
||||
_importer._add_module(moves, "moves")
|
||||
|
||||
|
||||
class Module_six_moves_urllib_parse(_LazyModule):
|
||||
|
||||
"""Lazy loading of moved objects in six.moves.urllib_parse"""
|
||||
|
||||
|
||||
_urllib_parse_moved_attributes = [
|
||||
MovedAttribute("ParseResult", "urlparse", "urllib.parse"),
|
||||
MovedAttribute("SplitResult", "urlparse", "urllib.parse"),
|
||||
MovedAttribute("parse_qs", "urlparse", "urllib.parse"),
|
||||
MovedAttribute("parse_qsl", "urlparse", "urllib.parse"),
|
||||
MovedAttribute("urldefrag", "urlparse", "urllib.parse"),
|
||||
MovedAttribute("urljoin", "urlparse", "urllib.parse"),
|
||||
MovedAttribute("urlparse", "urlparse", "urllib.parse"),
|
||||
MovedAttribute("urlsplit", "urlparse", "urllib.parse"),
|
||||
MovedAttribute("urlunparse", "urlparse", "urllib.parse"),
|
||||
MovedAttribute("urlunsplit", "urlparse", "urllib.parse"),
|
||||
MovedAttribute("quote", "urllib", "urllib.parse"),
|
||||
MovedAttribute("quote_plus", "urllib", "urllib.parse"),
|
||||
MovedAttribute("unquote", "urllib", "urllib.parse"),
|
||||
MovedAttribute("unquote_plus", "urllib", "urllib.parse"),
|
||||
MovedAttribute("unquote_to_bytes", "urllib", "urllib.parse", "unquote", "unquote_to_bytes"),
|
||||
MovedAttribute("urlencode", "urllib", "urllib.parse"),
|
||||
MovedAttribute("splitquery", "urllib", "urllib.parse"),
|
||||
MovedAttribute("splittag", "urllib", "urllib.parse"),
|
||||
MovedAttribute("splituser", "urllib", "urllib.parse"),
|
||||
MovedAttribute("splitvalue", "urllib", "urllib.parse"),
|
||||
MovedAttribute("uses_fragment", "urlparse", "urllib.parse"),
|
||||
MovedAttribute("uses_netloc", "urlparse", "urllib.parse"),
|
||||
MovedAttribute("uses_params", "urlparse", "urllib.parse"),
|
||||
MovedAttribute("uses_query", "urlparse", "urllib.parse"),
|
||||
MovedAttribute("uses_relative", "urlparse", "urllib.parse"),
|
||||
]
|
||||
for attr in _urllib_parse_moved_attributes:
|
||||
setattr(Module_six_moves_urllib_parse, attr.name, attr)
|
||||
del attr
|
||||
|
||||
Module_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes
|
||||
|
||||
_importer._add_module(Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse"),
|
||||
"moves.urllib_parse", "moves.urllib.parse")
|
||||
|
||||
|
||||
class Module_six_moves_urllib_error(_LazyModule):
|
||||
|
||||
"""Lazy loading of moved objects in six.moves.urllib_error"""
|
||||
|
||||
|
||||
_urllib_error_moved_attributes = [
|
||||
MovedAttribute("URLError", "urllib2", "urllib.error"),
|
||||
MovedAttribute("HTTPError", "urllib2", "urllib.error"),
|
||||
MovedAttribute("ContentTooShortError", "urllib", "urllib.error"),
|
||||
]
|
||||
for attr in _urllib_error_moved_attributes:
|
||||
setattr(Module_six_moves_urllib_error, attr.name, attr)
|
||||
del attr
|
||||
|
||||
Module_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes
|
||||
|
||||
_importer._add_module(Module_six_moves_urllib_error(__name__ + ".moves.urllib.error"),
|
||||
"moves.urllib_error", "moves.urllib.error")
|
||||
|
||||
|
||||
class Module_six_moves_urllib_request(_LazyModule):
|
||||
|
||||
"""Lazy loading of moved objects in six.moves.urllib_request"""
|
||||
|
||||
|
||||
_urllib_request_moved_attributes = [
|
||||
MovedAttribute("urlopen", "urllib2", "urllib.request"),
|
||||
MovedAttribute("install_opener", "urllib2", "urllib.request"),
|
||||
MovedAttribute("build_opener", "urllib2", "urllib.request"),
|
||||
MovedAttribute("pathname2url", "urllib", "urllib.request"),
|
||||
MovedAttribute("url2pathname", "urllib", "urllib.request"),
|
||||
MovedAttribute("getproxies", "urllib", "urllib.request"),
|
||||
MovedAttribute("Request", "urllib2", "urllib.request"),
|
||||
MovedAttribute("OpenerDirector", "urllib2", "urllib.request"),
|
||||
MovedAttribute("HTTPDefaultErrorHandler", "urllib2", "urllib.request"),
|
||||
MovedAttribute("HTTPRedirectHandler", "urllib2", "urllib.request"),
|
||||
MovedAttribute("HTTPCookieProcessor", "urllib2", "urllib.request"),
|
||||
MovedAttribute("ProxyHandler", "urllib2", "urllib.request"),
|
||||
MovedAttribute("BaseHandler", "urllib2", "urllib.request"),
|
||||
MovedAttribute("HTTPPasswordMgr", "urllib2", "urllib.request"),
|
||||
MovedAttribute("HTTPPasswordMgrWithDefaultRealm", "urllib2", "urllib.request"),
|
||||
MovedAttribute("AbstractBasicAuthHandler", "urllib2", "urllib.request"),
|
||||
MovedAttribute("HTTPBasicAuthHandler", "urllib2", "urllib.request"),
|
||||
MovedAttribute("ProxyBasicAuthHandler", "urllib2", "urllib.request"),
|
||||
MovedAttribute("AbstractDigestAuthHandler", "urllib2", "urllib.request"),
|
||||
MovedAttribute("HTTPDigestAuthHandler", "urllib2", "urllib.request"),
|
||||
MovedAttribute("ProxyDigestAuthHandler", "urllib2", "urllib.request"),
|
||||
MovedAttribute("HTTPHandler", "urllib2", "urllib.request"),
|
||||
MovedAttribute("HTTPSHandler", "urllib2", "urllib.request"),
|
||||
MovedAttribute("FileHandler", "urllib2", "urllib.request"),
|
||||
MovedAttribute("FTPHandler", "urllib2", "urllib.request"),
|
||||
MovedAttribute("CacheFTPHandler", "urllib2", "urllib.request"),
|
||||
MovedAttribute("UnknownHandler", "urllib2", "urllib.request"),
|
||||
MovedAttribute("HTTPErrorProcessor", "urllib2", "urllib.request"),
|
||||
MovedAttribute("urlretrieve", "urllib", "urllib.request"),
|
||||
MovedAttribute("urlcleanup", "urllib", "urllib.request"),
|
||||
MovedAttribute("URLopener", "urllib", "urllib.request"),
|
||||
MovedAttribute("FancyURLopener", "urllib", "urllib.request"),
|
||||
MovedAttribute("proxy_bypass", "urllib", "urllib.request"),
|
||||
MovedAttribute("parse_http_list", "urllib2", "urllib.request"),
|
||||
MovedAttribute("parse_keqv_list", "urllib2", "urllib.request"),
|
||||
]
|
||||
for attr in _urllib_request_moved_attributes:
|
||||
setattr(Module_six_moves_urllib_request, attr.name, attr)
|
||||
del attr
|
||||
|
||||
Module_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes
|
||||
|
||||
_importer._add_module(Module_six_moves_urllib_request(__name__ + ".moves.urllib.request"),
|
||||
"moves.urllib_request", "moves.urllib.request")
|
||||
|
||||
|
||||
class Module_six_moves_urllib_response(_LazyModule):
|
||||
|
||||
"""Lazy loading of moved objects in six.moves.urllib_response"""
|
||||
|
||||
|
||||
_urllib_response_moved_attributes = [
|
||||
MovedAttribute("addbase", "urllib", "urllib.response"),
|
||||
MovedAttribute("addclosehook", "urllib", "urllib.response"),
|
||||
MovedAttribute("addinfo", "urllib", "urllib.response"),
|
||||
MovedAttribute("addinfourl", "urllib", "urllib.response"),
|
||||
]
|
||||
for attr in _urllib_response_moved_attributes:
|
||||
setattr(Module_six_moves_urllib_response, attr.name, attr)
|
||||
del attr
|
||||
|
||||
Module_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes
|
||||
|
||||
_importer._add_module(Module_six_moves_urllib_response(__name__ + ".moves.urllib.response"),
|
||||
"moves.urllib_response", "moves.urllib.response")
|
||||
|
||||
|
||||
class Module_six_moves_urllib_robotparser(_LazyModule):
|
||||
|
||||
"""Lazy loading of moved objects in six.moves.urllib_robotparser"""
|
||||
|
||||
|
||||
_urllib_robotparser_moved_attributes = [
|
||||
MovedAttribute("RobotFileParser", "robotparser", "urllib.robotparser"),
|
||||
]
|
||||
for attr in _urllib_robotparser_moved_attributes:
|
||||
setattr(Module_six_moves_urllib_robotparser, attr.name, attr)
|
||||
del attr
|
||||
|
||||
Module_six_moves_urllib_robotparser._moved_attributes = _urllib_robotparser_moved_attributes
|
||||
|
||||
_importer._add_module(Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser"),
|
||||
"moves.urllib_robotparser", "moves.urllib.robotparser")
|
||||
|
||||
|
||||
class Module_six_moves_urllib(types.ModuleType):
|
||||
|
||||
"""Create a six.moves.urllib namespace that resembles the Python 3 namespace"""
|
||||
__path__ = [] # mark as package
|
||||
parse = _importer._get_module("moves.urllib_parse")
|
||||
error = _importer._get_module("moves.urllib_error")
|
||||
request = _importer._get_module("moves.urllib_request")
|
||||
response = _importer._get_module("moves.urllib_response")
|
||||
robotparser = _importer._get_module("moves.urllib_robotparser")
|
||||
|
||||
def __dir__(self):
|
||||
return ['parse', 'error', 'request', 'response', 'robotparser']
|
||||
|
||||
_importer._add_module(Module_six_moves_urllib(__name__ + ".moves.urllib"),
|
||||
"moves.urllib")
|
||||
|
||||
|
||||
def add_move(move):
|
||||
"""Add an item to six.moves."""
|
||||
setattr(_MovedItems, move.name, move)
|
||||
|
||||
|
||||
def remove_move(name):
|
||||
"""Remove item from six.moves."""
|
||||
try:
|
||||
delattr(_MovedItems, name)
|
||||
except AttributeError:
|
||||
try:
|
||||
del moves.__dict__[name]
|
||||
except KeyError:
|
||||
raise AttributeError("no such move, %r" % (name,))
|
||||
|
||||
|
||||
if PY3:
|
||||
_meth_func = "__func__"
|
||||
_meth_self = "__self__"
|
||||
|
||||
_func_closure = "__closure__"
|
||||
_func_code = "__code__"
|
||||
_func_defaults = "__defaults__"
|
||||
_func_globals = "__globals__"
|
||||
else:
|
||||
_meth_func = "im_func"
|
||||
_meth_self = "im_self"
|
||||
|
||||
_func_closure = "func_closure"
|
||||
_func_code = "func_code"
|
||||
_func_defaults = "func_defaults"
|
||||
_func_globals = "func_globals"
|
||||
|
||||
|
||||
try:
|
||||
advance_iterator = next
|
||||
except NameError:
|
||||
def advance_iterator(it):
|
||||
return it.next()
|
||||
next = advance_iterator
|
||||
|
||||
|
||||
try:
|
||||
callable = callable
|
||||
except NameError:
|
||||
def callable(obj):
|
||||
return any("__call__" in klass.__dict__ for klass in type(obj).__mro__)
|
||||
|
||||
|
||||
if PY3:
|
||||
def get_unbound_function(unbound):
|
||||
return unbound
|
||||
|
||||
create_bound_method = types.MethodType
|
||||
|
||||
def create_unbound_method(func, cls):
|
||||
return func
|
||||
|
||||
Iterator = object
|
||||
else:
|
||||
def get_unbound_function(unbound):
|
||||
return unbound.im_func
|
||||
|
||||
def create_bound_method(func, obj):
|
||||
return types.MethodType(func, obj, obj.__class__)
|
||||
|
||||
def create_unbound_method(func, cls):
|
||||
return types.MethodType(func, None, cls)
|
||||
|
||||
class Iterator(object):
|
||||
|
||||
def next(self):
|
||||
return type(self).__next__(self)
|
||||
|
||||
callable = callable
|
||||
_add_doc(get_unbound_function,
|
||||
"""Get the function out of a possibly unbound function""")
|
||||
|
||||
|
||||
get_method_function = operator.attrgetter(_meth_func)
|
||||
get_method_self = operator.attrgetter(_meth_self)
|
||||
get_function_closure = operator.attrgetter(_func_closure)
|
||||
get_function_code = operator.attrgetter(_func_code)
|
||||
get_function_defaults = operator.attrgetter(_func_defaults)
|
||||
get_function_globals = operator.attrgetter(_func_globals)
|
||||
|
||||
|
||||
if PY3:
|
||||
def iterkeys(d, **kw):
|
||||
return iter(d.keys(**kw))
|
||||
|
||||
def itervalues(d, **kw):
|
||||
return iter(d.values(**kw))
|
||||
|
||||
def iteritems(d, **kw):
|
||||
return iter(d.items(**kw))
|
||||
|
||||
def iterlists(d, **kw):
|
||||
return iter(d.lists(**kw))
|
||||
|
||||
viewkeys = operator.methodcaller("keys")
|
||||
|
||||
viewvalues = operator.methodcaller("values")
|
||||
|
||||
viewitems = operator.methodcaller("items")
|
||||
else:
|
||||
def iterkeys(d, **kw):
|
||||
return d.iterkeys(**kw)
|
||||
|
||||
def itervalues(d, **kw):
|
||||
return d.itervalues(**kw)
|
||||
|
||||
def iteritems(d, **kw):
|
||||
return d.iteritems(**kw)
|
||||
|
||||
def iterlists(d, **kw):
|
||||
return d.iterlists(**kw)
|
||||
|
||||
viewkeys = operator.methodcaller("viewkeys")
|
||||
|
||||
viewvalues = operator.methodcaller("viewvalues")
|
||||
|
||||
viewitems = operator.methodcaller("viewitems")
|
||||
|
||||
_add_doc(iterkeys, "Return an iterator over the keys of a dictionary.")
|
||||
_add_doc(itervalues, "Return an iterator over the values of a dictionary.")
|
||||
_add_doc(iteritems,
|
||||
"Return an iterator over the (key, value) pairs of a dictionary.")
|
||||
_add_doc(iterlists,
|
||||
"Return an iterator over the (key, [values]) pairs of a dictionary.")
|
||||
|
||||
|
||||
if PY3:
|
||||
def b(s):
|
||||
return s.encode("latin-1")
|
||||
|
||||
def u(s):
|
||||
return s
|
||||
unichr = chr
|
||||
import struct
|
||||
int2byte = struct.Struct(">B").pack
|
||||
del struct
|
||||
byte2int = operator.itemgetter(0)
|
||||
indexbytes = operator.getitem
|
||||
iterbytes = iter
|
||||
import io
|
||||
StringIO = io.StringIO
|
||||
BytesIO = io.BytesIO
|
||||
del io
|
||||
_assertCountEqual = "assertCountEqual"
|
||||
if sys.version_info[1] <= 1:
|
||||
_assertRaisesRegex = "assertRaisesRegexp"
|
||||
_assertRegex = "assertRegexpMatches"
|
||||
_assertNotRegex = "assertNotRegexpMatches"
|
||||
else:
|
||||
_assertRaisesRegex = "assertRaisesRegex"
|
||||
_assertRegex = "assertRegex"
|
||||
_assertNotRegex = "assertNotRegex"
|
||||
else:
|
||||
def b(s):
|
||||
return s
|
||||
# Workaround for standalone backslash
|
||||
|
||||
def u(s):
|
||||
return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape")
|
||||
unichr = unichr
|
||||
int2byte = chr
|
||||
|
||||
def byte2int(bs):
|
||||
return ord(bs[0])
|
||||
|
||||
def indexbytes(buf, i):
|
||||
return ord(buf[i])
|
||||
iterbytes = functools.partial(itertools.imap, ord)
|
||||
import StringIO
|
||||
StringIO = BytesIO = StringIO.StringIO
|
||||
_assertCountEqual = "assertItemsEqual"
|
||||
_assertRaisesRegex = "assertRaisesRegexp"
|
||||
_assertRegex = "assertRegexpMatches"
|
||||
_assertNotRegex = "assertNotRegexpMatches"
|
||||
_add_doc(b, """Byte literal""")
|
||||
_add_doc(u, """Text literal""")
|
||||
|
||||
|
||||
def assertCountEqual(self, *args, **kwargs):
|
||||
return getattr(self, _assertCountEqual)(*args, **kwargs)
|
||||
|
||||
|
||||
def assertRaisesRegex(self, *args, **kwargs):
|
||||
return getattr(self, _assertRaisesRegex)(*args, **kwargs)
|
||||
|
||||
|
||||
def assertRegex(self, *args, **kwargs):
|
||||
return getattr(self, _assertRegex)(*args, **kwargs)
|
||||
|
||||
|
||||
def assertNotRegex(self, *args, **kwargs):
|
||||
return getattr(self, _assertNotRegex)(*args, **kwargs)
|
||||
|
||||
|
||||
if PY3:
|
||||
exec_ = getattr(moves.builtins, "exec")
|
||||
|
||||
def reraise(tp, value, tb=None):
|
||||
try:
|
||||
if value is None:
|
||||
value = tp()
|
||||
if value.__traceback__ is not tb:
|
||||
raise value.with_traceback(tb)
|
||||
raise value
|
||||
finally:
|
||||
value = None
|
||||
tb = None
|
||||
|
||||
else:
|
||||
def exec_(_code_, _globs_=None, _locs_=None):
|
||||
"""Execute code in a namespace."""
|
||||
if _globs_ is None:
|
||||
frame = sys._getframe(1)
|
||||
_globs_ = frame.f_globals
|
||||
if _locs_ is None:
|
||||
_locs_ = frame.f_locals
|
||||
del frame
|
||||
elif _locs_ is None:
|
||||
_locs_ = _globs_
|
||||
exec("""exec _code_ in _globs_, _locs_""")
|
||||
|
||||
exec_("""def reraise(tp, value, tb=None):
|
||||
try:
|
||||
raise tp, value, tb
|
||||
finally:
|
||||
tb = None
|
||||
""")
|
||||
|
||||
|
||||
if sys.version_info[:2] > (3,):
|
||||
exec_("""def raise_from(value, from_value):
|
||||
try:
|
||||
raise value from from_value
|
||||
finally:
|
||||
value = None
|
||||
""")
|
||||
else:
|
||||
def raise_from(value, from_value):
|
||||
raise value
|
||||
|
||||
|
||||
print_ = getattr(moves.builtins, "print", None)
|
||||
if print_ is None:
|
||||
def print_(*args, **kwargs):
|
||||
"""The new-style print function for Python 2.4 and 2.5."""
|
||||
fp = kwargs.pop("file", sys.stdout)
|
||||
if fp is None:
|
||||
return
|
||||
|
||||
def write(data):
|
||||
if not isinstance(data, basestring):
|
||||
data = str(data)
|
||||
# If the file has an encoding, encode unicode with it.
|
||||
if (isinstance(fp, file) and
|
||||
isinstance(data, unicode) and
|
||||
fp.encoding is not None):
|
||||
errors = getattr(fp, "errors", None)
|
||||
if errors is None:
|
||||
errors = "strict"
|
||||
data = data.encode(fp.encoding, errors)
|
||||
fp.write(data)
|
||||
want_unicode = False
|
||||
sep = kwargs.pop("sep", None)
|
||||
if sep is not None:
|
||||
if isinstance(sep, unicode):
|
||||
want_unicode = True
|
||||
elif not isinstance(sep, str):
|
||||
raise TypeError("sep must be None or a string")
|
||||
end = kwargs.pop("end", None)
|
||||
if end is not None:
|
||||
if isinstance(end, unicode):
|
||||
want_unicode = True
|
||||
elif not isinstance(end, str):
|
||||
raise TypeError("end must be None or a string")
|
||||
if kwargs:
|
||||
raise TypeError("invalid keyword arguments to print()")
|
||||
if not want_unicode:
|
||||
for arg in args:
|
||||
if isinstance(arg, unicode):
|
||||
want_unicode = True
|
||||
break
|
||||
if want_unicode:
|
||||
newline = unicode("\n")
|
||||
space = unicode(" ")
|
||||
else:
|
||||
newline = "\n"
|
||||
space = " "
|
||||
if sep is None:
|
||||
sep = space
|
||||
if end is None:
|
||||
end = newline
|
||||
for i, arg in enumerate(args):
|
||||
if i:
|
||||
write(sep)
|
||||
write(arg)
|
||||
write(end)
|
||||
if sys.version_info[:2] < (3, 3):
|
||||
_print = print_
|
||||
|
||||
def print_(*args, **kwargs):
|
||||
fp = kwargs.get("file", sys.stdout)
|
||||
flush = kwargs.pop("flush", False)
|
||||
_print(*args, **kwargs)
|
||||
if flush and fp is not None:
|
||||
fp.flush()
|
||||
|
||||
_add_doc(reraise, """Reraise an exception.""")
|
||||
|
||||
if sys.version_info[0:2] < (3, 4):
|
||||
# This does exactly the same what the :func:`py3:functools.update_wrapper`
|
||||
# function does on Python versions after 3.2. It sets the ``__wrapped__``
|
||||
# attribute on ``wrapper`` object and it doesn't raise an error if any of
|
||||
# the attributes mentioned in ``assigned`` and ``updated`` are missing on
|
||||
# ``wrapped`` object.
|
||||
def _update_wrapper(wrapper, wrapped,
|
||||
assigned=functools.WRAPPER_ASSIGNMENTS,
|
||||
updated=functools.WRAPPER_UPDATES):
|
||||
for attr in assigned:
|
||||
try:
|
||||
value = getattr(wrapped, attr)
|
||||
except AttributeError:
|
||||
continue
|
||||
else:
|
||||
setattr(wrapper, attr, value)
|
||||
for attr in updated:
|
||||
getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
|
||||
wrapper.__wrapped__ = wrapped
|
||||
return wrapper
|
||||
_update_wrapper.__doc__ = functools.update_wrapper.__doc__
|
||||
|
||||
def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS,
|
||||
updated=functools.WRAPPER_UPDATES):
|
||||
return functools.partial(_update_wrapper, wrapped=wrapped,
|
||||
assigned=assigned, updated=updated)
|
||||
wraps.__doc__ = functools.wraps.__doc__
|
||||
|
||||
else:
|
||||
wraps = functools.wraps
|
||||
|
||||
|
||||
def with_metaclass(meta, *bases):
|
||||
"""Create a base class with a metaclass."""
|
||||
# This requires a bit of explanation: the basic idea is to make a dummy
|
||||
# metaclass for one level of class instantiation that replaces itself with
|
||||
# the actual metaclass.
|
||||
class metaclass(type):
|
||||
|
||||
def __new__(cls, name, this_bases, d):
|
||||
if sys.version_info[:2] >= (3, 7):
|
||||
# This version introduced PEP 560 that requires a bit
|
||||
# of extra care (we mimic what is done by __build_class__).
|
||||
resolved_bases = types.resolve_bases(bases)
|
||||
if resolved_bases is not bases:
|
||||
d['__orig_bases__'] = bases
|
||||
else:
|
||||
resolved_bases = bases
|
||||
return meta(name, resolved_bases, d)
|
||||
|
||||
@classmethod
|
||||
def __prepare__(cls, name, this_bases):
|
||||
return meta.__prepare__(name, bases)
|
||||
return type.__new__(metaclass, 'temporary_class', (), {})
|
||||
|
||||
|
||||
def add_metaclass(metaclass):
|
||||
"""Class decorator for creating a class with a metaclass."""
|
||||
def wrapper(cls):
|
||||
orig_vars = cls.__dict__.copy()
|
||||
slots = orig_vars.get('__slots__')
|
||||
if slots is not None:
|
||||
if isinstance(slots, str):
|
||||
slots = [slots]
|
||||
for slots_var in slots:
|
||||
orig_vars.pop(slots_var)
|
||||
orig_vars.pop('__dict__', None)
|
||||
orig_vars.pop('__weakref__', None)
|
||||
if hasattr(cls, '__qualname__'):
|
||||
orig_vars['__qualname__'] = cls.__qualname__
|
||||
return metaclass(cls.__name__, cls.__bases__, orig_vars)
|
||||
return wrapper
|
||||
|
||||
|
||||
def ensure_binary(s, encoding='utf-8', errors='strict'):
|
||||
"""Coerce **s** to six.binary_type.
|
||||
|
||||
For Python 2:
|
||||
- `unicode` -> encoded to `str`
|
||||
- `str` -> `str`
|
||||
|
||||
For Python 3:
|
||||
- `str` -> encoded to `bytes`
|
||||
- `bytes` -> `bytes`
|
||||
"""
|
||||
if isinstance(s, binary_type):
|
||||
return s
|
||||
if isinstance(s, text_type):
|
||||
return s.encode(encoding, errors)
|
||||
raise TypeError("not expecting type '%s'" % type(s))
|
||||
|
||||
|
||||
def ensure_str(s, encoding='utf-8', errors='strict'):
|
||||
"""Coerce *s* to `str`.
|
||||
|
||||
For Python 2:
|
||||
- `unicode` -> encoded to `str`
|
||||
- `str` -> `str`
|
||||
|
||||
For Python 3:
|
||||
- `str` -> `str`
|
||||
- `bytes` -> decoded to `str`
|
||||
"""
|
||||
# Optimization: Fast return for the common case.
|
||||
if type(s) is str:
|
||||
return s
|
||||
if PY2 and isinstance(s, text_type):
|
||||
return s.encode(encoding, errors)
|
||||
elif PY3 and isinstance(s, binary_type):
|
||||
return s.decode(encoding, errors)
|
||||
elif not isinstance(s, (text_type, binary_type)):
|
||||
raise TypeError("not expecting type '%s'" % type(s))
|
||||
return s
|
||||
|
||||
|
||||
def ensure_text(s, encoding='utf-8', errors='strict'):
|
||||
"""Coerce *s* to six.text_type.
|
||||
|
||||
For Python 2:
|
||||
- `unicode` -> `unicode`
|
||||
- `str` -> `unicode`
|
||||
|
||||
For Python 3:
|
||||
- `str` -> `str`
|
||||
- `bytes` -> decoded to `str`
|
||||
"""
|
||||
if isinstance(s, binary_type):
|
||||
return s.decode(encoding, errors)
|
||||
elif isinstance(s, text_type):
|
||||
return s
|
||||
else:
|
||||
raise TypeError("not expecting type '%s'" % type(s))
|
||||
|
||||
|
||||
def python_2_unicode_compatible(klass):
|
||||
"""
|
||||
A class decorator that defines __unicode__ and __str__ methods under Python 2.
|
||||
Under Python 3 it does nothing.
|
||||
|
||||
To support Python 2 and 3 with a single code base, define a __str__ method
|
||||
returning text and apply this decorator to the class.
|
||||
"""
|
||||
if PY2:
|
||||
if '__str__' not in klass.__dict__:
|
||||
raise ValueError("@python_2_unicode_compatible cannot be applied "
|
||||
"to %s because it doesn't define __str__()." %
|
||||
klass.__name__)
|
||||
klass.__unicode__ = klass.__str__
|
||||
klass.__str__ = lambda self: self.__unicode__().encode('utf-8')
|
||||
return klass
|
||||
|
||||
|
||||
# Complete the moves implementation.
|
||||
# This code is at the end of this module to speed up module loading.
|
||||
# Turn this module into a package.
|
||||
__path__ = [] # required for PEP 302 and PEP 451
|
||||
__package__ = __name__ # see PEP 366 @ReservedAssignment
|
||||
if globals().get("__spec__") is not None:
|
||||
__spec__.submodule_search_locations = [] # PEP 451 @UndefinedVariable
|
||||
# Remove other six meta path importers, since they cause problems. This can
|
||||
# happen if six is removed from sys.modules and then reloaded. (Setuptools does
|
||||
# this for some reason.)
|
||||
if sys.meta_path:
|
||||
for i, importer in enumerate(sys.meta_path):
|
||||
# Here's some real nastiness: Another "instance" of the six module might
|
||||
# be floating around. Therefore, we can't use isinstance() to check for
|
||||
# the six meta path importer, since the other six instance will have
|
||||
# inserted an importer with different class.
|
||||
if (type(importer).__name__ == "_SixMetaPathImporter" and
|
||||
importer.name == __name__):
|
||||
del sys.meta_path[i]
|
||||
break
|
||||
del i, importer
|
||||
# Finally, add the importer to the meta path import hook.
|
||||
sys.meta_path.append(_importer)
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
from .server import WebTestHttpd, WebTestServer, Router # noqa: F401
|
||||
from .request import Request # noqa: F401
|
||||
from .response import Response # noqa: F401
|
||||
|
|
@ -0,0 +1,279 @@
|
|||
A. HISTORY OF THE SOFTWARE
|
||||
==========================
|
||||
|
||||
Python was created in the early 1990s by Guido van Rossum at Stichting
|
||||
Mathematisch Centrum (CWI, see https://www.cwi.nl) in the Netherlands
|
||||
as a successor of a language called ABC. Guido remains Python's
|
||||
principal author, although it includes many contributions from others.
|
||||
|
||||
In 1995, Guido continued his work on Python at the Corporation for
|
||||
National Research Initiatives (CNRI, see https://www.cnri.reston.va.us)
|
||||
in Reston, Virginia where he released several versions of the
|
||||
software.
|
||||
|
||||
In May 2000, Guido and the Python core development team moved to
|
||||
BeOpen.com to form the BeOpen PythonLabs team. In October of the same
|
||||
year, the PythonLabs team moved to Digital Creations, which became
|
||||
Zope Corporation. In 2001, the Python Software Foundation (PSF, see
|
||||
https://www.python.org/psf/) was formed, a non-profit organization
|
||||
created specifically to own Python-related Intellectual Property.
|
||||
Zope Corporation was a sponsoring member of the PSF.
|
||||
|
||||
All Python releases are Open Source (see https://opensource.org for
|
||||
the Open Source Definition). Historically, most, but not all, Python
|
||||
releases have also been GPL-compatible; the table below summarizes
|
||||
the various releases.
|
||||
|
||||
Release Derived Year Owner GPL-
|
||||
from compatible? (1)
|
||||
|
||||
0.9.0 thru 1.2 1991-1995 CWI yes
|
||||
1.3 thru 1.5.2 1.2 1995-1999 CNRI yes
|
||||
1.6 1.5.2 2000 CNRI no
|
||||
2.0 1.6 2000 BeOpen.com no
|
||||
1.6.1 1.6 2001 CNRI yes (2)
|
||||
2.1 2.0+1.6.1 2001 PSF no
|
||||
2.0.1 2.0+1.6.1 2001 PSF yes
|
||||
2.1.1 2.1+2.0.1 2001 PSF yes
|
||||
2.1.2 2.1.1 2002 PSF yes
|
||||
2.1.3 2.1.2 2002 PSF yes
|
||||
2.2 and above 2.1.1 2001-now PSF yes
|
||||
|
||||
Footnotes:
|
||||
|
||||
(1) GPL-compatible doesn't mean that we're distributing Python under
|
||||
the GPL. All Python licenses, unlike the GPL, let you distribute
|
||||
a modified version without making your changes open source. The
|
||||
GPL-compatible licenses make it possible to combine Python with
|
||||
other software that is released under the GPL; the others don't.
|
||||
|
||||
(2) According to Richard Stallman, 1.6.1 is not GPL-compatible,
|
||||
because its license has a choice of law clause. According to
|
||||
CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1
|
||||
is "not incompatible" with the GPL.
|
||||
|
||||
Thanks to the many outside volunteers who have worked under Guido's
|
||||
direction to make these releases possible.
|
||||
|
||||
|
||||
B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON
|
||||
===============================================================
|
||||
|
||||
Python software and documentation are licensed under the
|
||||
Python Software Foundation License Version 2.
|
||||
|
||||
Starting with Python 3.8.6, examples, recipes, and other code in
|
||||
the documentation are dual licensed under the PSF License Version 2
|
||||
and the Zero-Clause BSD license.
|
||||
|
||||
Some software incorporated into Python is under different licenses.
|
||||
The licenses are listed with code falling under that license.
|
||||
|
||||
|
||||
PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
|
||||
--------------------------------------------
|
||||
|
||||
1. This LICENSE AGREEMENT is between the Python Software Foundation
|
||||
("PSF"), and the Individual or Organization ("Licensee") accessing and
|
||||
otherwise using this software ("Python") in source or binary form and
|
||||
its associated documentation.
|
||||
|
||||
2. Subject to the terms and conditions of this License Agreement, PSF hereby
|
||||
grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
|
||||
analyze, test, perform and/or display publicly, prepare derivative works,
|
||||
distribute, and otherwise use Python alone or in any derivative version,
|
||||
provided, however, that PSF's License Agreement and PSF's notice of copyright,
|
||||
i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
|
||||
2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Python Software Foundation;
|
||||
All Rights Reserved" are retained in Python alone or in any derivative version
|
||||
prepared by Licensee.
|
||||
|
||||
3. In the event Licensee prepares a derivative work that is based on
|
||||
or incorporates Python or any part thereof, and wants to make
|
||||
the derivative work available to others as provided herein, then
|
||||
Licensee hereby agrees to include in any such work a brief summary of
|
||||
the changes made to Python.
|
||||
|
||||
4. PSF is making Python available to Licensee on an "AS IS"
|
||||
basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
|
||||
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
||||
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
|
||||
INFRINGE ANY THIRD PARTY RIGHTS.
|
||||
|
||||
5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
|
||||
FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
|
||||
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
|
||||
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||
|
||||
6. This License Agreement will automatically terminate upon a material
|
||||
breach of its terms and conditions.
|
||||
|
||||
7. Nothing in this License Agreement shall be deemed to create any
|
||||
relationship of agency, partnership, or joint venture between PSF and
|
||||
Licensee. This License Agreement does not grant permission to use PSF
|
||||
trademarks or trade name in a trademark sense to endorse or promote
|
||||
products or services of Licensee, or any third party.
|
||||
|
||||
8. By copying, installing or otherwise using Python, Licensee
|
||||
agrees to be bound by the terms and conditions of this License
|
||||
Agreement.
|
||||
|
||||
|
||||
BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0
|
||||
-------------------------------------------
|
||||
|
||||
BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1
|
||||
|
||||
1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an
|
||||
office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the
|
||||
Individual or Organization ("Licensee") accessing and otherwise using
|
||||
this software in source or binary form and its associated
|
||||
documentation ("the Software").
|
||||
|
||||
2. Subject to the terms and conditions of this BeOpen Python License
|
||||
Agreement, BeOpen hereby grants Licensee a non-exclusive,
|
||||
royalty-free, world-wide license to reproduce, analyze, test, perform
|
||||
and/or display publicly, prepare derivative works, distribute, and
|
||||
otherwise use the Software alone or in any derivative version,
|
||||
provided, however, that the BeOpen Python License is retained in the
|
||||
Software, alone or in any derivative version prepared by Licensee.
|
||||
|
||||
3. BeOpen is making the Software available to Licensee on an "AS IS"
|
||||
basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND
|
||||
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
||||
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT
|
||||
INFRINGE ANY THIRD PARTY RIGHTS.
|
||||
|
||||
4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE
|
||||
SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS
|
||||
AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY
|
||||
DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||
|
||||
5. This License Agreement will automatically terminate upon a material
|
||||
breach of its terms and conditions.
|
||||
|
||||
6. This License Agreement shall be governed by and interpreted in all
|
||||
respects by the law of the State of California, excluding conflict of
|
||||
law provisions. Nothing in this License Agreement shall be deemed to
|
||||
create any relationship of agency, partnership, or joint venture
|
||||
between BeOpen and Licensee. This License Agreement does not grant
|
||||
permission to use BeOpen trademarks or trade names in a trademark
|
||||
sense to endorse or promote products or services of Licensee, or any
|
||||
third party. As an exception, the "BeOpen Python" logos available at
|
||||
http://www.pythonlabs.com/logos.html may be used according to the
|
||||
permissions granted on that web page.
|
||||
|
||||
7. By copying, installing or otherwise using the software, Licensee
|
||||
agrees to be bound by the terms and conditions of this License
|
||||
Agreement.
|
||||
|
||||
|
||||
CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1
|
||||
---------------------------------------
|
||||
|
||||
1. This LICENSE AGREEMENT is between the Corporation for National
|
||||
Research Initiatives, having an office at 1895 Preston White Drive,
|
||||
Reston, VA 20191 ("CNRI"), and the Individual or Organization
|
||||
("Licensee") accessing and otherwise using Python 1.6.1 software in
|
||||
source or binary form and its associated documentation.
|
||||
|
||||
2. Subject to the terms and conditions of this License Agreement, CNRI
|
||||
hereby grants Licensee a nonexclusive, royalty-free, world-wide
|
||||
license to reproduce, analyze, test, perform and/or display publicly,
|
||||
prepare derivative works, distribute, and otherwise use Python 1.6.1
|
||||
alone or in any derivative version, provided, however, that CNRI's
|
||||
License Agreement and CNRI's notice of copyright, i.e., "Copyright (c)
|
||||
1995-2001 Corporation for National Research Initiatives; All Rights
|
||||
Reserved" are retained in Python 1.6.1 alone or in any derivative
|
||||
version prepared by Licensee. Alternately, in lieu of CNRI's License
|
||||
Agreement, Licensee may substitute the following text (omitting the
|
||||
quotes): "Python 1.6.1 is made available subject to the terms and
|
||||
conditions in CNRI's License Agreement. This Agreement together with
|
||||
Python 1.6.1 may be located on the internet using the following
|
||||
unique, persistent identifier (known as a handle): 1895.22/1013. This
|
||||
Agreement may also be obtained from a proxy server on the internet
|
||||
using the following URL: http://hdl.handle.net/1895.22/1013".
|
||||
|
||||
3. In the event Licensee prepares a derivative work that is based on
|
||||
or incorporates Python 1.6.1 or any part thereof, and wants to make
|
||||
the derivative work available to others as provided herein, then
|
||||
Licensee hereby agrees to include in any such work a brief summary of
|
||||
the changes made to Python 1.6.1.
|
||||
|
||||
4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS"
|
||||
basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND
|
||||
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
||||
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT
|
||||
INFRINGE ANY THIRD PARTY RIGHTS.
|
||||
|
||||
5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
|
||||
1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
|
||||
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1,
|
||||
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||
|
||||
6. This License Agreement will automatically terminate upon a material
|
||||
breach of its terms and conditions.
|
||||
|
||||
7. This License Agreement shall be governed by the federal
|
||||
intellectual property law of the United States, including without
|
||||
limitation the federal copyright law, and, to the extent such
|
||||
U.S. federal law does not apply, by the law of the Commonwealth of
|
||||
Virginia, excluding Virginia's conflict of law provisions.
|
||||
Notwithstanding the foregoing, with regard to derivative works based
|
||||
on Python 1.6.1 that incorporate non-separable material that was
|
||||
previously distributed under the GNU General Public License (GPL), the
|
||||
law of the Commonwealth of Virginia shall govern this License
|
||||
Agreement only as to issues arising under or with respect to
|
||||
Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this
|
||||
License Agreement shall be deemed to create any relationship of
|
||||
agency, partnership, or joint venture between CNRI and Licensee. This
|
||||
License Agreement does not grant permission to use CNRI trademarks or
|
||||
trade name in a trademark sense to endorse or promote products or
|
||||
services of Licensee, or any third party.
|
||||
|
||||
8. By clicking on the "ACCEPT" button where indicated, or by copying,
|
||||
installing or otherwise using Python 1.6.1, Licensee agrees to be
|
||||
bound by the terms and conditions of this License Agreement.
|
||||
|
||||
ACCEPT
|
||||
|
||||
|
||||
CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2
|
||||
--------------------------------------------------
|
||||
|
||||
Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam,
|
||||
The Netherlands. All rights reserved.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software and its
|
||||
documentation for any purpose and without fee is hereby granted,
|
||||
provided that the above copyright notice appear in all copies and that
|
||||
both that copyright notice and this permission notice appear in
|
||||
supporting documentation, and that the name of Stichting Mathematisch
|
||||
Centrum or CWI not be used in advertising or publicity pertaining to
|
||||
distribution of the software without specific, written prior
|
||||
permission.
|
||||
|
||||
STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO
|
||||
THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE
|
||||
FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
|
||||
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
ZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON DOCUMENTATION
|
||||
----------------------------------------------------------------------
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
PERFORMANCE OF THIS SOFTWARE.
|
||||
|
|
@ -0,0 +1 @@
|
|||
from .cgi import *
|
||||
1014
Tests/LibWeb/Text/input/wpt-import/_wpttools/wptserve/wptserve/cgi/cgi.py
Executable file
1014
Tests/LibWeb/Text/input/wpt-import/_wpttools/wptserve/wptserve/cgi/cgi.py
Executable file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,706 @@
|
|||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import types
|
||||
import unittest
|
||||
import warnings
|
||||
from collections import namedtuple
|
||||
from io import StringIO, BytesIO
|
||||
|
||||
from . import cgi
|
||||
|
||||
# Inlined from Lib/test/support/__init__.py in Python 3.12
|
||||
def check__all__(test_case, module, name_of_module=None, extra=(),
|
||||
not_exported=()):
|
||||
"""Assert that the __all__ variable of 'module' contains all public names.
|
||||
|
||||
The module's public names (its API) are detected automatically based on
|
||||
whether they match the public name convention and were defined in
|
||||
'module'.
|
||||
|
||||
The 'name_of_module' argument can specify (as a string or tuple thereof)
|
||||
what module(s) an API could be defined in in order to be detected as a
|
||||
public API. One case for this is when 'module' imports part of its public
|
||||
API from other modules, possibly a C backend (like 'csv' and its '_csv').
|
||||
|
||||
The 'extra' argument can be a set of names that wouldn't otherwise be
|
||||
automatically detected as "public", like objects without a proper
|
||||
'__module__' attribute. If provided, it will be added to the
|
||||
automatically detected ones.
|
||||
|
||||
The 'not_exported' argument can be a set of names that must not be treated
|
||||
as part of the public API even though their names indicate otherwise.
|
||||
|
||||
Usage:
|
||||
import bar
|
||||
import foo
|
||||
import unittest
|
||||
from test import support
|
||||
|
||||
class MiscTestCase(unittest.TestCase):
|
||||
def test__all__(self):
|
||||
support.check__all__(self, foo)
|
||||
|
||||
class OtherTestCase(unittest.TestCase):
|
||||
def test__all__(self):
|
||||
extra = {'BAR_CONST', 'FOO_CONST'}
|
||||
not_exported = {'baz'} # Undocumented name.
|
||||
# bar imports part of its API from _bar.
|
||||
support.check__all__(self, bar, ('bar', '_bar'),
|
||||
extra=extra, not_exported=not_exported)
|
||||
|
||||
"""
|
||||
|
||||
if name_of_module is None:
|
||||
name_of_module = (module.__name__, )
|
||||
elif isinstance(name_of_module, str):
|
||||
name_of_module = (name_of_module, )
|
||||
|
||||
expected = set(extra)
|
||||
|
||||
for name in dir(module):
|
||||
if name.startswith('_') or name in not_exported:
|
||||
continue
|
||||
obj = getattr(module, name)
|
||||
if (getattr(obj, '__module__', None) in name_of_module or
|
||||
(not hasattr(obj, '__module__') and
|
||||
not isinstance(obj, types.ModuleType))):
|
||||
expected.add(name)
|
||||
test_case.assertCountEqual(module.__all__, expected)
|
||||
|
||||
|
||||
class HackedSysModule:
|
||||
# The regression test will have real values in sys.argv, which
|
||||
# will completely confuse the test of the cgi module
|
||||
argv = []
|
||||
stdin = sys.stdin
|
||||
version_info = sys.version_info
|
||||
|
||||
cgi.sys = HackedSysModule()
|
||||
|
||||
class ComparableException:
|
||||
def __init__(self, err):
|
||||
self.err = err
|
||||
|
||||
def __str__(self):
|
||||
return str(self.err)
|
||||
|
||||
def __eq__(self, anExc):
|
||||
if not isinstance(anExc, Exception):
|
||||
return NotImplemented
|
||||
return (self.err.__class__ == anExc.__class__ and
|
||||
self.err.args == anExc.args)
|
||||
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.err, attr)
|
||||
|
||||
def do_test(buf, method):
|
||||
env = {}
|
||||
if method == "GET":
|
||||
fp = None
|
||||
env['REQUEST_METHOD'] = 'GET'
|
||||
env['QUERY_STRING'] = buf
|
||||
elif method == "POST":
|
||||
fp = BytesIO(buf.encode('latin-1')) # FieldStorage expects bytes
|
||||
env['REQUEST_METHOD'] = 'POST'
|
||||
env['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'
|
||||
env['CONTENT_LENGTH'] = str(len(buf))
|
||||
else:
|
||||
raise ValueError("unknown method: %s" % method)
|
||||
try:
|
||||
return cgi.parse(fp, env, strict_parsing=1)
|
||||
except Exception as err:
|
||||
return ComparableException(err)
|
||||
|
||||
parse_strict_test_cases = [
|
||||
("", {}),
|
||||
("&", ValueError("bad query field: ''")),
|
||||
("&&", ValueError("bad query field: ''")),
|
||||
# Should the next few really be valid?
|
||||
("=", {}),
|
||||
("=&=", {}),
|
||||
# This rest seem to make sense
|
||||
("=a", {'': ['a']}),
|
||||
("&=a", ValueError("bad query field: ''")),
|
||||
("=a&", ValueError("bad query field: ''")),
|
||||
("=&a", ValueError("bad query field: 'a'")),
|
||||
("b=a", {'b': ['a']}),
|
||||
("b+=a", {'b ': ['a']}),
|
||||
("a=b=a", {'a': ['b=a']}),
|
||||
("a=+b=a", {'a': [' b=a']}),
|
||||
("&b=a", ValueError("bad query field: ''")),
|
||||
("b&=a", ValueError("bad query field: 'b'")),
|
||||
("a=a+b&b=b+c", {'a': ['a b'], 'b': ['b c']}),
|
||||
("a=a+b&a=b+a", {'a': ['a b', 'b a']}),
|
||||
("x=1&y=2.0&z=2-3.%2b0", {'x': ['1'], 'y': ['2.0'], 'z': ['2-3.+0']}),
|
||||
("Hbc5161168c542333633315dee1182227:key_store_seqid=400006&cuyer=r&view=bustomer&order_id=0bb2e248638833d48cb7fed300000f1b&expire=964546263&lobale=en-US&kid=130003.300038&ss=env",
|
||||
{'Hbc5161168c542333633315dee1182227:key_store_seqid': ['400006'],
|
||||
'cuyer': ['r'],
|
||||
'expire': ['964546263'],
|
||||
'kid': ['130003.300038'],
|
||||
'lobale': ['en-US'],
|
||||
'order_id': ['0bb2e248638833d48cb7fed300000f1b'],
|
||||
'ss': ['env'],
|
||||
'view': ['bustomer'],
|
||||
}),
|
||||
|
||||
("group_id=5470&set=custom&_assigned_to=31392&_status=1&_category=100&SUBMIT=Browse",
|
||||
{'SUBMIT': ['Browse'],
|
||||
'_assigned_to': ['31392'],
|
||||
'_category': ['100'],
|
||||
'_status': ['1'],
|
||||
'group_id': ['5470'],
|
||||
'set': ['custom'],
|
||||
})
|
||||
]
|
||||
|
||||
# The behaviour of urllib.parse.parse_qs with empty input changed in Python 3.11
|
||||
if sys.version_info < (3, 11):
|
||||
parse_strict_test_cases[0] = ("", ValueError("bad query field: ''"))
|
||||
|
||||
def norm(seq):
|
||||
return sorted(seq, key=repr)
|
||||
|
||||
def first_elts(list):
|
||||
return [p[0] for p in list]
|
||||
|
||||
def first_second_elts(list):
|
||||
return [(p[0], p[1][0]) for p in list]
|
||||
|
||||
def gen_result(data, environ):
|
||||
encoding = 'latin-1'
|
||||
fake_stdin = BytesIO(data.encode(encoding))
|
||||
fake_stdin.seek(0)
|
||||
form = cgi.FieldStorage(fp=fake_stdin, environ=environ, encoding=encoding)
|
||||
|
||||
result = {}
|
||||
for k, v in dict(form).items():
|
||||
result[k] = isinstance(v, list) and form.getlist(k) or v.value
|
||||
|
||||
return result
|
||||
|
||||
class CgiTests(unittest.TestCase):
|
||||
|
||||
def test_parse_multipart(self):
|
||||
fp = BytesIO(POSTDATA.encode('latin1'))
|
||||
env = {'boundary': BOUNDARY.encode('latin1'),
|
||||
'CONTENT-LENGTH': '558'}
|
||||
result = cgi.parse_multipart(fp, env)
|
||||
expected = {'submit': [' Add '], 'id': ['1234'],
|
||||
'file': [b'Testing 123.\n'], 'title': ['']}
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_parse_multipart_without_content_length(self):
|
||||
POSTDATA = '''--JfISa01
|
||||
Content-Disposition: form-data; name="submit-name"
|
||||
|
||||
just a string
|
||||
|
||||
--JfISa01--
|
||||
'''
|
||||
fp = BytesIO(POSTDATA.encode('latin1'))
|
||||
env = {'boundary': 'JfISa01'.encode('latin1')}
|
||||
result = cgi.parse_multipart(fp, env)
|
||||
expected = {'submit-name': ['just a string\n']}
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_parse_multipart_invalid_encoding(self):
|
||||
BOUNDARY = "JfISa01"
|
||||
POSTDATA = """--JfISa01
|
||||
Content-Disposition: form-data; name="submit-name"
|
||||
Content-Length: 3
|
||||
|
||||
\u2603
|
||||
--JfISa01"""
|
||||
fp = BytesIO(POSTDATA.encode('utf8'))
|
||||
env = {'boundary': BOUNDARY.encode('latin1'),
|
||||
'CONTENT-LENGTH': str(len(POSTDATA.encode('utf8')))}
|
||||
result = cgi.parse_multipart(fp, env, encoding="ascii",
|
||||
errors="surrogateescape")
|
||||
expected = {'submit-name': ["\udce2\udc98\udc83"]}
|
||||
self.assertEqual(result, expected)
|
||||
self.assertEqual("\u2603".encode('utf8'),
|
||||
result["submit-name"][0].encode('utf8', 'surrogateescape'))
|
||||
|
||||
def test_fieldstorage_properties(self):
|
||||
fs = cgi.FieldStorage()
|
||||
self.assertFalse(fs)
|
||||
self.assertIn("FieldStorage", repr(fs))
|
||||
self.assertEqual(list(fs), list(fs.keys()))
|
||||
fs.list.append(namedtuple('MockFieldStorage', 'name')('fieldvalue'))
|
||||
self.assertTrue(fs)
|
||||
|
||||
def test_fieldstorage_invalid(self):
|
||||
self.assertRaises(TypeError, cgi.FieldStorage, "not-a-file-obj",
|
||||
environ={"REQUEST_METHOD":"PUT"})
|
||||
self.assertRaises(TypeError, cgi.FieldStorage, "foo", "bar")
|
||||
fs = cgi.FieldStorage(headers={'content-type':'text/plain'})
|
||||
self.assertRaises(TypeError, bool, fs)
|
||||
|
||||
def test_strict(self):
|
||||
for orig, expect in parse_strict_test_cases:
|
||||
# Test basic parsing
|
||||
d = do_test(orig, "GET")
|
||||
self.assertEqual(d, expect, "Error parsing %s method GET" % repr(orig))
|
||||
d = do_test(orig, "POST")
|
||||
self.assertEqual(d, expect, "Error parsing %s method POST" % repr(orig))
|
||||
|
||||
env = {'QUERY_STRING': orig}
|
||||
fs = cgi.FieldStorage(environ=env)
|
||||
if isinstance(expect, dict):
|
||||
# test dict interface
|
||||
self.assertEqual(len(expect), len(fs))
|
||||
self.assertCountEqual(expect.keys(), fs.keys())
|
||||
##self.assertEqual(norm(expect.values()), norm(fs.values()))
|
||||
##self.assertEqual(norm(expect.items()), norm(fs.items()))
|
||||
self.assertEqual(fs.getvalue("nonexistent field", "default"), "default")
|
||||
# test individual fields
|
||||
for key in expect.keys():
|
||||
expect_val = expect[key]
|
||||
self.assertIn(key, fs)
|
||||
if len(expect_val) > 1:
|
||||
self.assertEqual(fs.getvalue(key), expect_val)
|
||||
else:
|
||||
self.assertEqual(fs.getvalue(key), expect_val[0])
|
||||
|
||||
def test_separator(self):
|
||||
parse_semicolon = [
|
||||
("x=1;y=2.0", {'x': ['1'], 'y': ['2.0']}),
|
||||
("x=1;y=2.0;z=2-3.%2b0", {'x': ['1'], 'y': ['2.0'], 'z': ['2-3.+0']}),
|
||||
(";", ValueError("bad query field: ''")),
|
||||
(";;", ValueError("bad query field: ''")),
|
||||
("=;a", ValueError("bad query field: 'a'")),
|
||||
(";b=a", ValueError("bad query field: ''")),
|
||||
("b;=a", ValueError("bad query field: 'b'")),
|
||||
("a=a+b;b=b+c", {'a': ['a b'], 'b': ['b c']}),
|
||||
("a=a+b;a=b+a", {'a': ['a b', 'b a']}),
|
||||
]
|
||||
for orig, expect in parse_semicolon:
|
||||
env = {'QUERY_STRING': orig}
|
||||
fs = cgi.FieldStorage(separator=';', environ=env)
|
||||
if isinstance(expect, dict):
|
||||
for key in expect.keys():
|
||||
expect_val = expect[key]
|
||||
self.assertIn(key, fs)
|
||||
if len(expect_val) > 1:
|
||||
self.assertEqual(fs.getvalue(key), expect_val)
|
||||
else:
|
||||
self.assertEqual(fs.getvalue(key), expect_val[0])
|
||||
|
||||
def test_log(self):
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter('ignore', category=DeprecationWarning)
|
||||
cgi.log("Testing")
|
||||
|
||||
cgi.logfp = StringIO()
|
||||
cgi.initlog("%s", "Testing initlog 1")
|
||||
cgi.log("%s", "Testing log 2")
|
||||
self.assertEqual(cgi.logfp.getvalue(), "Testing initlog 1\nTesting log 2\n")
|
||||
if os.path.exists(os.devnull):
|
||||
cgi.logfp = None
|
||||
cgi.logfile = os.devnull
|
||||
cgi.initlog("%s", "Testing log 3")
|
||||
self.addCleanup(cgi.closelog)
|
||||
cgi.log("Testing log 4")
|
||||
|
||||
def test_fieldstorage_readline(self):
|
||||
# FieldStorage uses readline, which has the capacity to read all
|
||||
# contents of the input file into memory; we use readline's size argument
|
||||
# to prevent that for files that do not contain any newlines in
|
||||
# non-GET/HEAD requests
|
||||
class TestReadlineFile:
|
||||
def __init__(self, file):
|
||||
self.file = file
|
||||
self.numcalls = 0
|
||||
|
||||
def readline(self, size=None):
|
||||
self.numcalls += 1
|
||||
if size:
|
||||
return self.file.readline(size)
|
||||
else:
|
||||
return self.file.readline()
|
||||
|
||||
def __getattr__(self, name):
|
||||
file = self.__dict__['file']
|
||||
a = getattr(file, name)
|
||||
if not isinstance(a, int):
|
||||
setattr(self, name, a)
|
||||
return a
|
||||
|
||||
f = TestReadlineFile(tempfile.TemporaryFile("wb+"))
|
||||
self.addCleanup(f.close)
|
||||
f.write(b'x' * 256 * 1024)
|
||||
f.seek(0)
|
||||
env = {'REQUEST_METHOD':'PUT'}
|
||||
fs = cgi.FieldStorage(fp=f, environ=env)
|
||||
self.addCleanup(fs.file.close)
|
||||
# if we're not chunking properly, readline is only called twice
|
||||
# (by read_binary); if we are chunking properly, it will be called 5 times
|
||||
# as long as the chunksize is 1 << 16.
|
||||
self.assertGreater(f.numcalls, 2)
|
||||
f.close()
|
||||
|
||||
def test_fieldstorage_multipart(self):
|
||||
#Test basic FieldStorage multipart parsing
|
||||
env = {
|
||||
'REQUEST_METHOD': 'POST',
|
||||
'CONTENT_TYPE': 'multipart/form-data; boundary={}'.format(BOUNDARY),
|
||||
'CONTENT_LENGTH': '558'}
|
||||
fp = BytesIO(POSTDATA.encode('latin-1'))
|
||||
fs = cgi.FieldStorage(fp, environ=env, encoding="latin-1")
|
||||
self.assertEqual(len(fs.list), 4)
|
||||
expect = [{'name':'id', 'filename':None, 'value':'1234'},
|
||||
{'name':'title', 'filename':None, 'value':''},
|
||||
{'name':'file', 'filename':'test.txt', 'value':b'Testing 123.\n'},
|
||||
{'name':'submit', 'filename':None, 'value':' Add '}]
|
||||
for x in range(len(fs.list)):
|
||||
for k, exp in expect[x].items():
|
||||
got = getattr(fs.list[x], k)
|
||||
self.assertEqual(got, exp)
|
||||
|
||||
def test_fieldstorage_multipart_leading_whitespace(self):
|
||||
env = {
|
||||
'REQUEST_METHOD': 'POST',
|
||||
'CONTENT_TYPE': 'multipart/form-data; boundary={}'.format(BOUNDARY),
|
||||
'CONTENT_LENGTH': '560'}
|
||||
# Add some leading whitespace to our post data that will cause the
|
||||
# first line to not be the innerboundary.
|
||||
fp = BytesIO(b"\r\n" + POSTDATA.encode('latin-1'))
|
||||
fs = cgi.FieldStorage(fp, environ=env, encoding="latin-1")
|
||||
self.assertEqual(len(fs.list), 4)
|
||||
expect = [{'name':'id', 'filename':None, 'value':'1234'},
|
||||
{'name':'title', 'filename':None, 'value':''},
|
||||
{'name':'file', 'filename':'test.txt', 'value':b'Testing 123.\n'},
|
||||
{'name':'submit', 'filename':None, 'value':' Add '}]
|
||||
for x in range(len(fs.list)):
|
||||
for k, exp in expect[x].items():
|
||||
got = getattr(fs.list[x], k)
|
||||
self.assertEqual(got, exp)
|
||||
|
||||
def test_fieldstorage_multipart_non_ascii(self):
|
||||
#Test basic FieldStorage multipart parsing
|
||||
env = {'REQUEST_METHOD':'POST',
|
||||
'CONTENT_TYPE': 'multipart/form-data; boundary={}'.format(BOUNDARY),
|
||||
'CONTENT_LENGTH':'558'}
|
||||
for encoding in ['iso-8859-1','utf-8']:
|
||||
fp = BytesIO(POSTDATA_NON_ASCII.encode(encoding))
|
||||
fs = cgi.FieldStorage(fp, environ=env,encoding=encoding)
|
||||
self.assertEqual(len(fs.list), 1)
|
||||
expect = [{'name':'id', 'filename':None, 'value':'\xe7\xf1\x80'}]
|
||||
for x in range(len(fs.list)):
|
||||
for k, exp in expect[x].items():
|
||||
got = getattr(fs.list[x], k)
|
||||
self.assertEqual(got, exp)
|
||||
|
||||
def test_fieldstorage_multipart_maxline(self):
|
||||
# Issue #18167
|
||||
maxline = 1 << 16
|
||||
self.maxDiff = None
|
||||
def check(content):
|
||||
data = """---123
|
||||
Content-Disposition: form-data; name="upload"; filename="fake.txt"
|
||||
Content-Type: text/plain
|
||||
|
||||
%s
|
||||
---123--
|
||||
""".replace('\n', '\r\n') % content
|
||||
environ = {
|
||||
'CONTENT_LENGTH': str(len(data)),
|
||||
'CONTENT_TYPE': 'multipart/form-data; boundary=-123',
|
||||
'REQUEST_METHOD': 'POST',
|
||||
}
|
||||
self.assertEqual(gen_result(data, environ),
|
||||
{'upload': content.encode('latin1')})
|
||||
check('x' * (maxline - 1))
|
||||
check('x' * (maxline - 1) + '\r')
|
||||
check('x' * (maxline - 1) + '\r' + 'y' * (maxline - 1))
|
||||
|
||||
def test_fieldstorage_multipart_w3c(self):
|
||||
# Test basic FieldStorage multipart parsing (W3C sample)
|
||||
env = {
|
||||
'REQUEST_METHOD': 'POST',
|
||||
'CONTENT_TYPE': 'multipart/form-data; boundary={}'.format(BOUNDARY_W3),
|
||||
'CONTENT_LENGTH': str(len(POSTDATA_W3))}
|
||||
fp = BytesIO(POSTDATA_W3.encode('latin-1'))
|
||||
fs = cgi.FieldStorage(fp, environ=env, encoding="latin-1")
|
||||
self.assertEqual(len(fs.list), 2)
|
||||
self.assertEqual(fs.list[0].name, 'submit-name')
|
||||
self.assertEqual(fs.list[0].value, 'Larry')
|
||||
self.assertEqual(fs.list[1].name, 'files')
|
||||
files = fs.list[1].value
|
||||
self.assertEqual(len(files), 2)
|
||||
expect = [{'name': None, 'filename': 'file1.txt', 'value': b'... contents of file1.txt ...'},
|
||||
{'name': None, 'filename': 'file2.gif', 'value': b'...contents of file2.gif...'}]
|
||||
for x in range(len(files)):
|
||||
for k, exp in expect[x].items():
|
||||
got = getattr(files[x], k)
|
||||
self.assertEqual(got, exp)
|
||||
|
||||
def test_fieldstorage_part_content_length(self):
|
||||
BOUNDARY = "JfISa01"
|
||||
POSTDATA = """--JfISa01
|
||||
Content-Disposition: form-data; name="submit-name"
|
||||
Content-Length: 5
|
||||
|
||||
Larry
|
||||
--JfISa01"""
|
||||
env = {
|
||||
'REQUEST_METHOD': 'POST',
|
||||
'CONTENT_TYPE': 'multipart/form-data; boundary={}'.format(BOUNDARY),
|
||||
'CONTENT_LENGTH': str(len(POSTDATA))}
|
||||
fp = BytesIO(POSTDATA.encode('latin-1'))
|
||||
fs = cgi.FieldStorage(fp, environ=env, encoding="latin-1")
|
||||
self.assertEqual(len(fs.list), 1)
|
||||
self.assertEqual(fs.list[0].name, 'submit-name')
|
||||
self.assertEqual(fs.list[0].value, 'Larry')
|
||||
|
||||
def test_field_storage_multipart_no_content_length(self):
|
||||
fp = BytesIO(b"""--MyBoundary
|
||||
Content-Disposition: form-data; name="my-arg"; filename="foo"
|
||||
|
||||
Test
|
||||
|
||||
--MyBoundary--
|
||||
""")
|
||||
env = {
|
||||
"REQUEST_METHOD": "POST",
|
||||
"CONTENT_TYPE": "multipart/form-data; boundary=MyBoundary",
|
||||
"wsgi.input": fp,
|
||||
}
|
||||
fields = cgi.FieldStorage(fp, environ=env)
|
||||
|
||||
self.assertEqual(len(fields["my-arg"].file.read()), 5)
|
||||
|
||||
def test_fieldstorage_as_context_manager(self):
|
||||
fp = BytesIO(b'x' * 10)
|
||||
env = {'REQUEST_METHOD': 'PUT'}
|
||||
with cgi.FieldStorage(fp=fp, environ=env) as fs:
|
||||
content = fs.file.read()
|
||||
self.assertFalse(fs.file.closed)
|
||||
self.assertTrue(fs.file.closed)
|
||||
self.assertEqual(content, 'x' * 10)
|
||||
with self.assertRaisesRegex(ValueError, 'I/O operation on closed file'):
|
||||
fs.file.read()
|
||||
|
||||
_qs_result = {
|
||||
'key1': 'value1',
|
||||
'key2': ['value2x', 'value2y'],
|
||||
'key3': 'value3',
|
||||
'key4': 'value4'
|
||||
}
|
||||
def testQSAndUrlEncode(self):
|
||||
data = "key2=value2x&key3=value3&key4=value4"
|
||||
environ = {
|
||||
'CONTENT_LENGTH': str(len(data)),
|
||||
'CONTENT_TYPE': 'application/x-www-form-urlencoded',
|
||||
'QUERY_STRING': 'key1=value1&key2=value2y',
|
||||
'REQUEST_METHOD': 'POST',
|
||||
}
|
||||
v = gen_result(data, environ)
|
||||
self.assertEqual(self._qs_result, v)
|
||||
|
||||
def test_max_num_fields(self):
|
||||
# For application/x-www-form-urlencoded
|
||||
data = '&'.join(['a=a']*11)
|
||||
environ = {
|
||||
'CONTENT_LENGTH': str(len(data)),
|
||||
'CONTENT_TYPE': 'application/x-www-form-urlencoded',
|
||||
'REQUEST_METHOD': 'POST',
|
||||
}
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
cgi.FieldStorage(
|
||||
fp=BytesIO(data.encode()),
|
||||
environ=environ,
|
||||
max_num_fields=10,
|
||||
)
|
||||
|
||||
# For multipart/form-data
|
||||
data = """---123
|
||||
Content-Disposition: form-data; name="a"
|
||||
|
||||
3
|
||||
---123
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
a=4
|
||||
---123
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
a=5
|
||||
---123--
|
||||
"""
|
||||
environ = {
|
||||
'CONTENT_LENGTH': str(len(data)),
|
||||
'CONTENT_TYPE': 'multipart/form-data; boundary=-123',
|
||||
'QUERY_STRING': 'a=1&a=2',
|
||||
'REQUEST_METHOD': 'POST',
|
||||
}
|
||||
|
||||
# 2 GET entities
|
||||
# 1 top level POST entities
|
||||
# 1 entity within the second POST entity
|
||||
# 1 entity within the third POST entity
|
||||
with self.assertRaises(ValueError):
|
||||
cgi.FieldStorage(
|
||||
fp=BytesIO(data.encode()),
|
||||
environ=environ,
|
||||
max_num_fields=4,
|
||||
)
|
||||
cgi.FieldStorage(
|
||||
fp=BytesIO(data.encode()),
|
||||
environ=environ,
|
||||
max_num_fields=5,
|
||||
)
|
||||
|
||||
def testQSAndFormData(self):
|
||||
data = """---123
|
||||
Content-Disposition: form-data; name="key2"
|
||||
|
||||
value2y
|
||||
---123
|
||||
Content-Disposition: form-data; name="key3"
|
||||
|
||||
value3
|
||||
---123
|
||||
Content-Disposition: form-data; name="key4"
|
||||
|
||||
value4
|
||||
---123--
|
||||
"""
|
||||
environ = {
|
||||
'CONTENT_LENGTH': str(len(data)),
|
||||
'CONTENT_TYPE': 'multipart/form-data; boundary=-123',
|
||||
'QUERY_STRING': 'key1=value1&key2=value2x',
|
||||
'REQUEST_METHOD': 'POST',
|
||||
}
|
||||
v = gen_result(data, environ)
|
||||
self.assertEqual(self._qs_result, v)
|
||||
|
||||
def testQSAndFormDataFile(self):
|
||||
data = """---123
|
||||
Content-Disposition: form-data; name="key2"
|
||||
|
||||
value2y
|
||||
---123
|
||||
Content-Disposition: form-data; name="key3"
|
||||
|
||||
value3
|
||||
---123
|
||||
Content-Disposition: form-data; name="key4"
|
||||
|
||||
value4
|
||||
---123
|
||||
Content-Disposition: form-data; name="upload"; filename="fake.txt"
|
||||
Content-Type: text/plain
|
||||
|
||||
this is the content of the fake file
|
||||
|
||||
---123--
|
||||
"""
|
||||
environ = {
|
||||
'CONTENT_LENGTH': str(len(data)),
|
||||
'CONTENT_TYPE': 'multipart/form-data; boundary=-123',
|
||||
'QUERY_STRING': 'key1=value1&key2=value2x',
|
||||
'REQUEST_METHOD': 'POST',
|
||||
}
|
||||
result = self._qs_result.copy()
|
||||
result.update({
|
||||
'upload': b'this is the content of the fake file\n'
|
||||
})
|
||||
v = gen_result(data, environ)
|
||||
self.assertEqual(result, v)
|
||||
|
||||
def test_parse_header(self):
|
||||
self.assertEqual(
|
||||
cgi.parse_header("text/plain"),
|
||||
("text/plain", {}))
|
||||
self.assertEqual(
|
||||
cgi.parse_header("text/vnd.just.made.this.up ; "),
|
||||
("text/vnd.just.made.this.up", {}))
|
||||
self.assertEqual(
|
||||
cgi.parse_header("text/plain;charset=us-ascii"),
|
||||
("text/plain", {"charset": "us-ascii"}))
|
||||
self.assertEqual(
|
||||
cgi.parse_header('text/plain ; charset="us-ascii"'),
|
||||
("text/plain", {"charset": "us-ascii"}))
|
||||
self.assertEqual(
|
||||
cgi.parse_header('text/plain ; charset="us-ascii"; another=opt'),
|
||||
("text/plain", {"charset": "us-ascii", "another": "opt"}))
|
||||
self.assertEqual(
|
||||
cgi.parse_header('attachment; filename="silly.txt"'),
|
||||
("attachment", {"filename": "silly.txt"}))
|
||||
self.assertEqual(
|
||||
cgi.parse_header('attachment; filename="strange;name"'),
|
||||
("attachment", {"filename": "strange;name"}))
|
||||
self.assertEqual(
|
||||
cgi.parse_header('attachment; filename="strange;name";size=123;'),
|
||||
("attachment", {"filename": "strange;name", "size": "123"}))
|
||||
self.assertEqual(
|
||||
cgi.parse_header('form-data; name="files"; filename="fo\\"o;bar"'),
|
||||
("form-data", {"name": "files", "filename": 'fo"o;bar'}))
|
||||
|
||||
def test_all(self):
|
||||
not_exported = {
|
||||
"logfile", "logfp", "initlog", "dolog", "nolog", "closelog", "log",
|
||||
"maxlen", "valid_boundary"}
|
||||
check__all__(self, cgi, not_exported=not_exported)
|
||||
|
||||
|
||||
BOUNDARY = "---------------------------721837373350705526688164684"
|
||||
|
||||
POSTDATA = """-----------------------------721837373350705526688164684
|
||||
Content-Disposition: form-data; name="id"
|
||||
|
||||
1234
|
||||
-----------------------------721837373350705526688164684
|
||||
Content-Disposition: form-data; name="title"
|
||||
|
||||
|
||||
-----------------------------721837373350705526688164684
|
||||
Content-Disposition: form-data; name="file"; filename="test.txt"
|
||||
Content-Type: text/plain
|
||||
|
||||
Testing 123.
|
||||
|
||||
-----------------------------721837373350705526688164684
|
||||
Content-Disposition: form-data; name="submit"
|
||||
|
||||
Add\x20
|
||||
-----------------------------721837373350705526688164684--
|
||||
"""
|
||||
|
||||
POSTDATA_NON_ASCII = """-----------------------------721837373350705526688164684
|
||||
Content-Disposition: form-data; name="id"
|
||||
|
||||
\xe7\xf1\x80
|
||||
-----------------------------721837373350705526688164684
|
||||
"""
|
||||
|
||||
# http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4
|
||||
BOUNDARY_W3 = "AaB03x"
|
||||
POSTDATA_W3 = """--AaB03x
|
||||
Content-Disposition: form-data; name="submit-name"
|
||||
|
||||
Larry
|
||||
--AaB03x
|
||||
Content-Disposition: form-data; name="files"
|
||||
Content-Type: multipart/mixed; boundary=BbC04y
|
||||
|
||||
--BbC04y
|
||||
Content-Disposition: file; filename="file1.txt"
|
||||
Content-Type: text/plain
|
||||
|
||||
... contents of file1.txt ...
|
||||
--BbC04y
|
||||
Content-Disposition: file; filename="file2.gif"
|
||||
Content-Type: image/gif
|
||||
Content-Transfer-Encoding: binary
|
||||
|
||||
...contents of file2.gif...
|
||||
--BbC04y--
|
||||
--AaB03x--
|
||||
"""
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
@ -0,0 +1,339 @@
|
|||
# mypy: allow-untyped-defs
|
||||
|
||||
import copy
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from typing import Any, Mapping
|
||||
|
||||
from . import sslutils
|
||||
from .utils import get_port
|
||||
|
||||
|
||||
_renamed_props = {
|
||||
"host": "browser_host",
|
||||
"bind_hostname": "bind_address",
|
||||
"external_host": "server_host",
|
||||
"host_ip": "server_host",
|
||||
}
|
||||
|
||||
|
||||
def _merge_dict(base_dict, override_dict):
|
||||
rv = base_dict.copy()
|
||||
for key, value in base_dict.items():
|
||||
if key in override_dict:
|
||||
if isinstance(value, dict):
|
||||
rv[key] = _merge_dict(value, override_dict[key])
|
||||
else:
|
||||
rv[key] = override_dict[key]
|
||||
return rv
|
||||
|
||||
|
||||
class Config(Mapping[str, Any]):
|
||||
"""wptserve configuration data
|
||||
|
||||
Immutable configuration that's safe to be passed between processes.
|
||||
|
||||
Inherits from Mapping for backwards compatibility with the old dict-based config
|
||||
|
||||
:param data: - Extra configuration data
|
||||
"""
|
||||
def __init__(self, data):
|
||||
for name in data.keys():
|
||||
if name.startswith("_"):
|
||||
raise ValueError("Invalid configuration key %s" % name)
|
||||
self.__dict__.update(data)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.__dict__)
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
raise ValueError("Config is immutable")
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
raise ValueError("Config is immutable")
|
||||
|
||||
def __getitem__(self, key):
|
||||
try:
|
||||
return getattr(self, key)
|
||||
except AttributeError:
|
||||
raise ValueError
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self.__dict__
|
||||
|
||||
def __iter__(self):
|
||||
return (x for x in self.__dict__ if not x.startswith("_"))
|
||||
|
||||
def __len__(self):
|
||||
return len([item for item in self])
|
||||
|
||||
def as_dict(self):
|
||||
return json_types(self.__dict__, skip={"_logger"})
|
||||
|
||||
|
||||
def json_types(obj, skip=None):
|
||||
if skip is None:
|
||||
skip = set()
|
||||
if isinstance(obj, dict):
|
||||
return {key: json_types(value) for key, value in obj.items() if key not in skip}
|
||||
if (isinstance(obj, str) or
|
||||
isinstance(obj, int) or
|
||||
isinstance(obj, float) or
|
||||
isinstance(obj, bool) or
|
||||
obj is None):
|
||||
return obj
|
||||
if isinstance(obj, list) or hasattr(obj, "__iter__"):
|
||||
return [json_types(value) for value in obj]
|
||||
raise ValueError
|
||||
|
||||
|
||||
class ConfigBuilder:
|
||||
"""Builder object for setting the wptserve config.
|
||||
|
||||
Configuration can be passed in as a dictionary to the constructor, or
|
||||
set via attributes after construction. Configuration options must match
|
||||
the keys on the _default class property.
|
||||
|
||||
The generated configuration is obtained by using the builder
|
||||
object as a context manager; this returns a Config object
|
||||
containing immutable configuration that may be shared between
|
||||
threads and processes. In general the configuration is only valid
|
||||
for the context used to obtain it.
|
||||
|
||||
with ConfigBuilder() as config:
|
||||
# Use the configuration
|
||||
print config.browser_host
|
||||
|
||||
The properties on the final configuration include those explicitly
|
||||
supplied and computed properties. The computed properties are
|
||||
defined by the computed_properties attribute on the class. This
|
||||
is a list of property names, each corresponding to a _get_<name>
|
||||
method on the class. These methods are called in the order defined
|
||||
in computed_properties and are passed a single argument, a
|
||||
dictionary containing the current set of properties. Thus computed
|
||||
properties later in the list may depend on the value of earlier
|
||||
ones.
|
||||
|
||||
|
||||
:param logger: - A logger object. This is used for logging during
|
||||
the creation of the configuration, but isn't
|
||||
part of the configuration
|
||||
:param subdomains: - A set of valid subdomains to include in the
|
||||
configuration.
|
||||
:param not_subdomains: - A set of invalid subdomains to include in
|
||||
the configuration.
|
||||
:param config_cls: - A class to use for the configuration. Defaults
|
||||
to default_config_cls
|
||||
"""
|
||||
|
||||
_default = {
|
||||
"browser_host": "localhost",
|
||||
"alternate_hosts": {},
|
||||
"doc_root": os.path.dirname("__file__"),
|
||||
"server_host": None,
|
||||
"ports": {"http": [8000]},
|
||||
"check_subdomains": True,
|
||||
"bind_address": True,
|
||||
"ssl": {
|
||||
"type": "none",
|
||||
"encrypt_after_connect": False,
|
||||
"none": {},
|
||||
"openssl": {
|
||||
"openssl_binary": "openssl",
|
||||
"base_path": "_certs",
|
||||
"password": "web-platform-tests",
|
||||
"force_regenerate": False,
|
||||
"duration": 30,
|
||||
"base_conf_path": None
|
||||
},
|
||||
"pregenerated": {
|
||||
"host_key_path": None,
|
||||
"host_cert_path": None,
|
||||
},
|
||||
},
|
||||
"aliases": [],
|
||||
"logging": {
|
||||
"level": "debug",
|
||||
"suppress_handler_traceback": False,
|
||||
}
|
||||
}
|
||||
default_config_cls = Config
|
||||
|
||||
# Configuration properties that are computed. Each corresponds to a method
|
||||
# _get_foo, which is called with the current data dictionary. The properties
|
||||
# are computed in the order specified in the list.
|
||||
computed_properties = ["logging",
|
||||
"paths",
|
||||
"server_host",
|
||||
"ports",
|
||||
"domains",
|
||||
"not_domains",
|
||||
"all_domains",
|
||||
"domains_set",
|
||||
"not_domains_set",
|
||||
"all_domains_set",
|
||||
"ssl_config"]
|
||||
|
||||
def __init__(self,
|
||||
logger,
|
||||
subdomains=set(),
|
||||
not_subdomains=set(),
|
||||
config_cls=None,
|
||||
**kwargs):
|
||||
|
||||
self._logger = logger
|
||||
self._data = self._default.copy()
|
||||
self._ssl_env = None
|
||||
|
||||
self._config_cls = config_cls or self.default_config_cls
|
||||
|
||||
for k, v in self._default.items():
|
||||
self._data[k] = kwargs.pop(k, v)
|
||||
|
||||
self._data["subdomains"] = subdomains
|
||||
self._data["not_subdomains"] = not_subdomains
|
||||
|
||||
for k, new_k in _renamed_props.items():
|
||||
if k in kwargs:
|
||||
logger.warning(
|
||||
"%s in config is deprecated; use %s instead" % (
|
||||
k,
|
||||
new_k
|
||||
)
|
||||
)
|
||||
self._data[new_k] = kwargs.pop(k)
|
||||
|
||||
if kwargs:
|
||||
raise TypeError("__init__() got unexpected keyword arguments %r" % (tuple(kwargs),))
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
if not key[0] == "_":
|
||||
self._data[key] = value
|
||||
else:
|
||||
self.__dict__[key] = value
|
||||
|
||||
def __getattr__(self, key):
|
||||
try:
|
||||
return self._data[key]
|
||||
except KeyError as e:
|
||||
raise AttributeError from e
|
||||
|
||||
def update(self, override):
|
||||
"""Load an overrides dict to override config values"""
|
||||
override = override.copy()
|
||||
|
||||
for k in self._default:
|
||||
if k in override:
|
||||
self._set_override(k, override.pop(k))
|
||||
|
||||
for k, new_k in _renamed_props.items():
|
||||
if k in override:
|
||||
self._logger.warning(
|
||||
"%s in config is deprecated; use %s instead" % (
|
||||
k,
|
||||
new_k
|
||||
)
|
||||
)
|
||||
self._set_override(new_k, override.pop(k))
|
||||
|
||||
if override:
|
||||
k = next(iter(override))
|
||||
raise KeyError("unknown config override '%s'" % k)
|
||||
|
||||
def _set_override(self, k, v):
|
||||
old_v = self._data[k]
|
||||
if isinstance(old_v, dict):
|
||||
self._data[k] = _merge_dict(old_v, v)
|
||||
else:
|
||||
self._data[k] = v
|
||||
|
||||
def __enter__(self):
|
||||
if self._ssl_env is not None:
|
||||
raise ValueError("Tried to re-enter configuration")
|
||||
data = self._data.copy()
|
||||
prefix = "_get_"
|
||||
for key in self.computed_properties:
|
||||
data[key] = getattr(self, prefix + key)(data)
|
||||
return self._config_cls(data)
|
||||
|
||||
def __exit__(self, *args):
|
||||
self._ssl_env.__exit__(*args)
|
||||
self._ssl_env = None
|
||||
|
||||
def _get_logging(self, data):
|
||||
logging = data["logging"]
|
||||
logging["level"] = logging["level"].upper()
|
||||
return logging
|
||||
|
||||
def _get_paths(self, data):
|
||||
return {"doc_root": data["doc_root"]}
|
||||
|
||||
def _get_server_host(self, data):
|
||||
return data["server_host"] if data.get("server_host") is not None else data["browser_host"]
|
||||
|
||||
def _get_ports(self, data):
|
||||
new_ports = defaultdict(list)
|
||||
for scheme, ports in data["ports"].items():
|
||||
if scheme in ["wss", "https"] and not sslutils.get_cls(data["ssl"]["type"]).ssl_enabled:
|
||||
continue
|
||||
for i, port in enumerate(ports):
|
||||
real_port = get_port("") if port == "auto" else port
|
||||
new_ports[scheme].append(real_port)
|
||||
return new_ports
|
||||
|
||||
def _get_domains(self, data):
|
||||
hosts = data["alternate_hosts"].copy()
|
||||
assert "" not in hosts
|
||||
hosts[""] = data["browser_host"]
|
||||
|
||||
rv = {}
|
||||
for name, host in hosts.items():
|
||||
rv[name] = {subdomain: (subdomain.encode("idna").decode("ascii") + "." + host)
|
||||
for subdomain in data["subdomains"]}
|
||||
rv[name][""] = host
|
||||
return rv
|
||||
|
||||
def _get_not_domains(self, data):
|
||||
hosts = data["alternate_hosts"].copy()
|
||||
assert "" not in hosts
|
||||
hosts[""] = data["browser_host"]
|
||||
|
||||
rv = {}
|
||||
for name, host in hosts.items():
|
||||
rv[name] = {subdomain: (subdomain.encode("idna").decode("ascii") + "." + host)
|
||||
for subdomain in data["not_subdomains"]}
|
||||
return rv
|
||||
|
||||
def _get_all_domains(self, data):
|
||||
rv = copy.deepcopy(data["domains"])
|
||||
nd = data["not_domains"]
|
||||
for host in rv:
|
||||
rv[host].update(nd[host])
|
||||
return rv
|
||||
|
||||
def _get_domains_set(self, data):
|
||||
return {domain
|
||||
for per_host_domains in data["domains"].values()
|
||||
for domain in per_host_domains.values()}
|
||||
|
||||
def _get_not_domains_set(self, data):
|
||||
return {domain
|
||||
for per_host_domains in data["not_domains"].values()
|
||||
for domain in per_host_domains.values()}
|
||||
|
||||
def _get_all_domains_set(self, data):
|
||||
return data["domains_set"] | data["not_domains_set"]
|
||||
|
||||
def _get_ssl_config(self, data):
|
||||
ssl_type = data["ssl"]["type"]
|
||||
ssl_cls = sslutils.get_cls(ssl_type)
|
||||
kwargs = data["ssl"].get(ssl_type, {})
|
||||
self._ssl_env = ssl_cls(self._logger, **kwargs)
|
||||
self._ssl_env.__enter__()
|
||||
if self._ssl_env.ssl_enabled:
|
||||
key_path, cert_path = self._ssl_env.host_cert_path(data["domains_set"])
|
||||
ca_cert_path = self._ssl_env.ca_cert_path(data["domains_set"])
|
||||
return {"key_path": key_path,
|
||||
"ca_cert_path": ca_cert_path,
|
||||
"cert_path": cert_path,
|
||||
"encrypt_after_connect": data["ssl"].get("encrypt_after_connect", False)}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
from . import utils
|
||||
|
||||
content_types = utils.invert_dict({
|
||||
"application/json": ["json"],
|
||||
"application/wasm": ["wasm"],
|
||||
"application/xhtml+xml": ["xht", "xhtm", "xhtml"],
|
||||
"application/xml": ["xml"],
|
||||
"application/x-xpinstall": ["xpi"],
|
||||
"audio/mp4": ["m4a"],
|
||||
"audio/mpeg": ["mp3"],
|
||||
"audio/ogg": ["oga"],
|
||||
"audio/webm": ["weba"],
|
||||
"audio/x-wav": ["wav"],
|
||||
"image/avif": ["avif"],
|
||||
"image/bmp": ["bmp"],
|
||||
"image/gif": ["gif"],
|
||||
"image/jpeg": ["jpg", "jpeg"],
|
||||
"image/jxl": ["jxl"],
|
||||
"image/png": ["png"],
|
||||
"image/svg+xml": ["svg"],
|
||||
"text/cache-manifest": ["manifest"],
|
||||
"text/css": ["css"],
|
||||
"text/event-stream": ["event_stream"],
|
||||
"text/html": ["htm", "html"],
|
||||
"text/javascript": ["js", "mjs"],
|
||||
"text/plain": ["txt", "md"],
|
||||
"text/vtt": ["vtt"],
|
||||
"video/mp4": ["mp4", "m4v"],
|
||||
"video/webm": ["webm"],
|
||||
})
|
||||
|
||||
response_codes = {
|
||||
100: ('Continue', 'Request received, please continue'),
|
||||
101: ('Switching Protocols',
|
||||
'Switching to new protocol; obey Upgrade header'),
|
||||
|
||||
200: ('OK', 'Request fulfilled, document follows'),
|
||||
201: ('Created', 'Document created, URL follows'),
|
||||
202: ('Accepted',
|
||||
'Request accepted, processing continues off-line'),
|
||||
203: ('Non-Authoritative Information', 'Request fulfilled from cache'),
|
||||
204: ('No Content', 'Request fulfilled, nothing follows'),
|
||||
205: ('Reset Content', 'Clear input form for further input.'),
|
||||
206: ('Partial Content', 'Partial content follows.'),
|
||||
|
||||
300: ('Multiple Choices',
|
||||
'Object has several resources -- see URI list'),
|
||||
301: ('Moved Permanently', 'Object moved permanently -- see URI list'),
|
||||
302: ('Found', 'Object moved temporarily -- see URI list'),
|
||||
303: ('See Other', 'Object moved -- see Method and URL list'),
|
||||
304: ('Not Modified',
|
||||
'Document has not changed since given time'),
|
||||
305: ('Use Proxy',
|
||||
'You must use proxy specified in Location to access this '
|
||||
'resource.'),
|
||||
307: ('Temporary Redirect',
|
||||
'Object moved temporarily -- see URI list'),
|
||||
|
||||
400: ('Bad Request',
|
||||
'Bad request syntax or unsupported method'),
|
||||
401: ('Unauthorized',
|
||||
'No permission -- see authorization schemes'),
|
||||
402: ('Payment Required',
|
||||
'No payment -- see charging schemes'),
|
||||
403: ('Forbidden',
|
||||
'Request forbidden -- authorization will not help'),
|
||||
404: ('Not Found', 'Nothing matches the given URI'),
|
||||
405: ('Method Not Allowed',
|
||||
'Specified method is invalid for this resource.'),
|
||||
406: ('Not Acceptable', 'URI not available in preferred format.'),
|
||||
407: ('Proxy Authentication Required', 'You must authenticate with '
|
||||
'this proxy before proceeding.'),
|
||||
408: ('Request Timeout', 'Request timed out; try again later.'),
|
||||
409: ('Conflict', 'Request conflict.'),
|
||||
410: ('Gone',
|
||||
'URI no longer exists and has been permanently removed.'),
|
||||
411: ('Length Required', 'Client must specify Content-Length.'),
|
||||
412: ('Precondition Failed', 'Precondition in headers is false.'),
|
||||
413: ('Request Entity Too Large', 'Entity is too large.'),
|
||||
414: ('Request-URI Too Long', 'URI is too long.'),
|
||||
415: ('Unsupported Media Type', 'Entity body in unsupported format.'),
|
||||
416: ('Requested Range Not Satisfiable',
|
||||
'Cannot satisfy request range.'),
|
||||
417: ('Expectation Failed',
|
||||
'Expect condition could not be satisfied.'),
|
||||
|
||||
500: ('Internal Server Error', 'Server got itself in trouble'),
|
||||
501: ('Not Implemented',
|
||||
'Server does not support this operation'),
|
||||
502: ('Bad Gateway', 'Invalid responses from another server/proxy.'),
|
||||
503: ('Service Unavailable',
|
||||
'The server cannot process the request due to a high load'),
|
||||
504: ('Gateway Timeout',
|
||||
'The gateway server did not receive a timely response'),
|
||||
505: ('HTTP Version Not Supported', 'Cannot fulfill request.'),
|
||||
}
|
||||
|
||||
h2_headers = ['method', 'scheme', 'host', 'path', 'authority', 'status']
|
||||
|
|
@ -0,0 +1,546 @@
|
|||
# mypy: allow-untyped-defs
|
||||
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
from collections import defaultdict
|
||||
|
||||
from urllib.parse import quote, unquote, urljoin
|
||||
|
||||
from .constants import content_types
|
||||
from .pipes import Pipeline, template
|
||||
from .ranges import RangeParser
|
||||
from .request import Authentication
|
||||
from .response import MultipartContent
|
||||
from .utils import HTTPException
|
||||
|
||||
from html import escape
|
||||
|
||||
__all__ = ["file_handler", "python_script_handler",
|
||||
"FunctionHandler", "handler", "json_handler",
|
||||
"as_is_handler", "ErrorHandler", "BasicAuthHandler"]
|
||||
|
||||
|
||||
def guess_content_type(path):
|
||||
ext = os.path.splitext(path)[1].lstrip(".")
|
||||
if ext in content_types:
|
||||
return content_types[ext]
|
||||
|
||||
return "application/octet-stream"
|
||||
|
||||
|
||||
def filesystem_path(base_path, request, url_base="/"):
|
||||
if base_path is None:
|
||||
base_path = request.doc_root
|
||||
|
||||
path = unquote(request.url_parts.path)
|
||||
|
||||
if path.startswith(url_base):
|
||||
path = path[len(url_base):]
|
||||
|
||||
if ".." in path:
|
||||
raise HTTPException(404)
|
||||
|
||||
new_path = os.path.join(base_path, path)
|
||||
|
||||
# Otherwise setting path to / allows access outside the root directory
|
||||
if not new_path.startswith(base_path):
|
||||
raise HTTPException(404)
|
||||
|
||||
return new_path
|
||||
|
||||
|
||||
class DirectoryHandler:
|
||||
def __init__(self, base_path=None, url_base="/"):
|
||||
self.base_path = base_path
|
||||
self.url_base = url_base
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s base_path:%s url_base:%s>" % (self.__class__.__name__, self.base_path, self.url_base)
|
||||
|
||||
def __call__(self, request, response):
|
||||
url_path = request.url_parts.path
|
||||
|
||||
if not url_path.endswith("/"):
|
||||
response.status = 301
|
||||
response.headers = [("Location", "%s/" % request.url)]
|
||||
return
|
||||
|
||||
path = filesystem_path(self.base_path, request, self.url_base)
|
||||
|
||||
assert os.path.isdir(path)
|
||||
|
||||
response.headers = [("Content-Type", "text/html")]
|
||||
response.content = """<!doctype html>
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>Directory listing for %(path)s</title>
|
||||
<h1>Directory listing for %(path)s</h1>
|
||||
<ul>
|
||||
%(items)s
|
||||
</ul>
|
||||
""" % {"path": escape(url_path),
|
||||
"items": "\n".join(self.list_items(url_path, path))} # noqa: E122
|
||||
|
||||
def list_items(self, base_path, path):
|
||||
assert base_path.endswith("/")
|
||||
|
||||
# TODO: this won't actually list all routes, only the
|
||||
# ones that correspond to a real filesystem path. It's
|
||||
# not possible to list every route that will match
|
||||
# something, but it should be possible to at least list the
|
||||
# statically defined ones
|
||||
|
||||
if base_path != "/":
|
||||
link = urljoin(base_path, "..")
|
||||
yield ("""<li class="dir"><a href="%(link)s">%(name)s</a></li>""" %
|
||||
{"link": link, "name": ".."})
|
||||
items = []
|
||||
prev_item = None
|
||||
# This ensures that .headers always sorts after the file it provides the headers for. E.g.,
|
||||
# if we have x, x-y, and x.headers, the order will be x, x.headers, and then x-y.
|
||||
for item in sorted(os.listdir(path), key=lambda x: (x[:-len(".headers")], x) if x.endswith(".headers") else (x, x)):
|
||||
if prev_item and prev_item + ".headers" == item:
|
||||
items[-1][1] = item
|
||||
prev_item = None
|
||||
continue
|
||||
items.append([item, None])
|
||||
prev_item = item
|
||||
for item, dot_headers in items:
|
||||
link = escape(quote(item))
|
||||
dot_headers_markup = ""
|
||||
if dot_headers is not None:
|
||||
dot_headers_markup = (""" (<a href="%(link)s">.headers</a>)""" %
|
||||
{"link": escape(quote(dot_headers))})
|
||||
if os.path.isdir(os.path.join(path, item)):
|
||||
link += "/"
|
||||
class_ = "dir"
|
||||
else:
|
||||
class_ = "file"
|
||||
yield ("""<li class="%(class)s"><a href="%(link)s">%(name)s</a>%(headers)s</li>""" %
|
||||
{"link": link, "name": escape(item), "class": class_,
|
||||
"headers": dot_headers_markup})
|
||||
|
||||
|
||||
def parse_qs(qs):
|
||||
"""Parse a query string given as a string argument (data of type
|
||||
application/x-www-form-urlencoded). Data are returned as a dictionary. The
|
||||
dictionary keys are the unique query variable names and the values are
|
||||
lists of values for each name.
|
||||
|
||||
This implementation is used instead of Python's built-in `parse_qs` method
|
||||
in order to support the semicolon character (which the built-in method
|
||||
interprets as a parameter delimiter)."""
|
||||
pairs = [item.split("=", 1) for item in qs.split('&') if item]
|
||||
rv = defaultdict(list)
|
||||
for pair in pairs:
|
||||
if len(pair) == 1 or len(pair[1]) == 0:
|
||||
continue
|
||||
name = unquote(pair[0].replace('+', ' '))
|
||||
value = unquote(pair[1].replace('+', ' '))
|
||||
rv[name].append(value)
|
||||
return dict(rv)
|
||||
|
||||
|
||||
def wrap_pipeline(path, request, response):
|
||||
"""Applies pipelines to a response.
|
||||
|
||||
Pipelines are specified in the filename (.sub.) or the query param (?pipe).
|
||||
"""
|
||||
query = parse_qs(request.url_parts.query)
|
||||
pipe_string = ""
|
||||
|
||||
if ".sub." in path:
|
||||
ml_extensions = {".html", ".htm", ".xht", ".xhtml", ".xml", ".svg"}
|
||||
escape_type = "html" if os.path.splitext(path)[1] in ml_extensions else "none"
|
||||
pipe_string = "sub(%s)" % escape_type
|
||||
|
||||
if "pipe" in query:
|
||||
if pipe_string:
|
||||
pipe_string += "|"
|
||||
|
||||
pipe_string += query["pipe"][-1]
|
||||
|
||||
if pipe_string:
|
||||
response = Pipeline(pipe_string)(request, response)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def load_headers(request, path):
|
||||
"""Loads headers from files for a given path.
|
||||
|
||||
Attempts to load both the neighbouring __dir__{.sub}.headers and
|
||||
PATH{.sub}.headers (applying template substitution if needed); results are
|
||||
concatenated in that order.
|
||||
"""
|
||||
def _load(request, path):
|
||||
headers_path = path + ".sub.headers"
|
||||
if os.path.exists(headers_path):
|
||||
use_sub = True
|
||||
else:
|
||||
headers_path = path + ".headers"
|
||||
use_sub = False
|
||||
|
||||
try:
|
||||
with open(headers_path, "rb") as headers_file:
|
||||
data = headers_file.read()
|
||||
except OSError:
|
||||
return []
|
||||
else:
|
||||
if use_sub:
|
||||
data = template(request, data, escape_type="none")
|
||||
return [tuple(item.strip() for item in line.split(b":", 1))
|
||||
for line in data.splitlines() if line]
|
||||
|
||||
return (_load(request, os.path.join(os.path.dirname(path), "__dir__")) +
|
||||
_load(request, path))
|
||||
|
||||
|
||||
class FileHandler:
|
||||
def __init__(self, base_path=None, url_base="/"):
|
||||
self.base_path = base_path
|
||||
self.url_base = url_base
|
||||
self.directory_handler = DirectoryHandler(self.base_path, self.url_base)
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s base_path:%s url_base:%s>" % (self.__class__.__name__, self.base_path, self.url_base)
|
||||
|
||||
def __call__(self, request, response):
|
||||
path = filesystem_path(self.base_path, request, self.url_base)
|
||||
|
||||
if os.path.isdir(path):
|
||||
return self.directory_handler(request, response)
|
||||
try:
|
||||
#This is probably racy with some other process trying to change the file
|
||||
file_size = os.stat(path).st_size
|
||||
response.headers.update(self.get_headers(request, path))
|
||||
if "Range" in request.headers:
|
||||
try:
|
||||
byte_ranges = RangeParser()(request.headers['Range'], file_size)
|
||||
except HTTPException as e:
|
||||
if e.code == 416:
|
||||
response.headers.set("Content-Range", "bytes */%i" % file_size)
|
||||
raise
|
||||
else:
|
||||
byte_ranges = None
|
||||
data = self.get_data(response, path, byte_ranges)
|
||||
response.content = data
|
||||
response = wrap_pipeline(path, request, response)
|
||||
return response
|
||||
|
||||
except OSError:
|
||||
raise HTTPException(404)
|
||||
|
||||
def get_headers(self, request, path):
|
||||
rv = load_headers(request, path)
|
||||
|
||||
if not any(key.lower() == b"content-type" for (key, _) in rv):
|
||||
rv.insert(0, (b"Content-Type", guess_content_type(path).encode("ascii")))
|
||||
|
||||
return rv
|
||||
|
||||
def get_data(self, response, path, byte_ranges):
|
||||
"""Return either the handle to a file, or a string containing
|
||||
the content of a chunk of the file, if we have a range request."""
|
||||
if byte_ranges is None:
|
||||
return open(path, 'rb')
|
||||
else:
|
||||
with open(path, 'rb') as f:
|
||||
response.status = 206
|
||||
if len(byte_ranges) > 1:
|
||||
parts_content_type, content = self.set_response_multipart(response,
|
||||
byte_ranges,
|
||||
f)
|
||||
for byte_range in byte_ranges:
|
||||
content.append_part(self.get_range_data(f, byte_range),
|
||||
parts_content_type,
|
||||
[("Content-Range", byte_range.header_value())])
|
||||
return content
|
||||
else:
|
||||
response.headers.set("Content-Range", byte_ranges[0].header_value())
|
||||
return self.get_range_data(f, byte_ranges[0])
|
||||
|
||||
def set_response_multipart(self, response, ranges, f):
|
||||
parts_content_type = response.headers.get("Content-Type")
|
||||
if parts_content_type:
|
||||
parts_content_type = parts_content_type[-1]
|
||||
else:
|
||||
parts_content_type = None
|
||||
content = MultipartContent()
|
||||
response.headers.set("Content-Type", "multipart/byteranges; boundary=%s" % content.boundary)
|
||||
return parts_content_type, content
|
||||
|
||||
def get_range_data(self, f, byte_range):
|
||||
f.seek(byte_range.lower)
|
||||
return f.read(byte_range.upper - byte_range.lower)
|
||||
|
||||
|
||||
file_handler = FileHandler() # type: ignore
|
||||
|
||||
|
||||
class PythonScriptHandler:
|
||||
def __init__(self, base_path=None, url_base="/"):
|
||||
self.base_path = base_path
|
||||
self.url_base = url_base
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s base_path:%s url_base:%s>" % (self.__class__.__name__, self.base_path, self.url_base)
|
||||
|
||||
def _load_file(self, request, response, func):
|
||||
"""
|
||||
This loads the requested python file as an environ variable.
|
||||
|
||||
If the requested file is a directory, this instead loads the first
|
||||
lexicographically sorted file found in that directory that matches
|
||||
"default*.py".
|
||||
|
||||
Once the environ is loaded, the passed `func` is run with this loaded environ.
|
||||
|
||||
:param request: The request object
|
||||
:param response: The response object
|
||||
:param func: The function to be run with the loaded environ with the modified filepath. Signature: (request, response, environ, path)
|
||||
:return: The return of func
|
||||
"""
|
||||
path = filesystem_path(self.base_path, request, self.url_base)
|
||||
|
||||
# Find a default Python file if the specified path is a directory
|
||||
if os.path.isdir(path):
|
||||
default_py_files = sorted(list(filter(
|
||||
pathlib.Path.is_file,
|
||||
pathlib.Path(path).glob("default*.py"))))
|
||||
if default_py_files:
|
||||
path = str(default_py_files[0])
|
||||
|
||||
try:
|
||||
environ = {"__file__": path}
|
||||
with open(path, 'rb') as f:
|
||||
exec(compile(f.read(), path, 'exec'), environ, environ)
|
||||
|
||||
if func is not None:
|
||||
return func(request, response, environ, path)
|
||||
|
||||
except OSError:
|
||||
raise HTTPException(404)
|
||||
|
||||
def __call__(self, request, response):
|
||||
def func(request, response, environ, path):
|
||||
if "main" in environ:
|
||||
handler = FunctionHandler(environ["main"])
|
||||
handler(request, response)
|
||||
wrap_pipeline(path, request, response)
|
||||
else:
|
||||
raise HTTPException(500, "No main function in script %s" % path)
|
||||
|
||||
self._load_file(request, response, func)
|
||||
|
||||
def frame_handler(self, request):
|
||||
"""
|
||||
This creates a FunctionHandler with one or more of the handling functions.
|
||||
|
||||
Used by the H2 server.
|
||||
|
||||
:param request: The request object used to generate the handler.
|
||||
:return: A FunctionHandler object with one or more of these functions: `handle_headers`, `handle_data` or `main`
|
||||
"""
|
||||
def func(request, response, environ, path):
|
||||
def _main(req, resp):
|
||||
pass
|
||||
|
||||
handler = FunctionHandler(_main)
|
||||
if "main" in environ:
|
||||
handler.func = environ["main"]
|
||||
if "handle_headers" in environ:
|
||||
handler.handle_headers = environ["handle_headers"]
|
||||
if "handle_data" in environ:
|
||||
handler.handle_data = environ["handle_data"]
|
||||
|
||||
if handler.func is _main and not hasattr(handler, "handle_headers") and not hasattr(handler, "handle_data"):
|
||||
raise HTTPException(500, "No main function or handlers in script %s" % path)
|
||||
|
||||
return handler
|
||||
return self._load_file(request, None, func)
|
||||
|
||||
|
||||
python_script_handler = PythonScriptHandler() # type: ignore
|
||||
|
||||
|
||||
class FunctionHandler:
|
||||
def __init__(self, func):
|
||||
self.func = func
|
||||
|
||||
def __call__(self, request, response):
|
||||
try:
|
||||
rv = self.func(request, response)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(500) from e
|
||||
if rv is not None:
|
||||
if isinstance(rv, tuple):
|
||||
if len(rv) == 3:
|
||||
status, headers, content = rv
|
||||
response.status = status
|
||||
elif len(rv) == 2:
|
||||
headers, content = rv
|
||||
else:
|
||||
raise HTTPException(500)
|
||||
response.headers.update(headers)
|
||||
else:
|
||||
content = rv
|
||||
response.content = content
|
||||
wrap_pipeline('', request, response)
|
||||
|
||||
|
||||
# The generic name here is so that this can be used as a decorator
|
||||
def handler(func):
|
||||
return FunctionHandler(func)
|
||||
|
||||
|
||||
class JsonHandler:
|
||||
def __init__(self, func):
|
||||
self.func = func
|
||||
|
||||
def __call__(self, request, response):
|
||||
return FunctionHandler(self.handle_request)(request, response)
|
||||
|
||||
def handle_request(self, request, response):
|
||||
rv = self.func(request, response)
|
||||
response.headers.set("Content-Type", "application/json")
|
||||
enc = json.dumps
|
||||
if isinstance(rv, tuple):
|
||||
rv = list(rv)
|
||||
value = tuple(rv[:-1] + [enc(rv[-1])])
|
||||
length = len(value[-1])
|
||||
else:
|
||||
value = enc(rv)
|
||||
length = len(value)
|
||||
response.headers.set("Content-Length", length)
|
||||
return value
|
||||
|
||||
|
||||
def json_handler(func):
|
||||
return JsonHandler(func)
|
||||
|
||||
|
||||
class AsIsHandler:
|
||||
def __init__(self, base_path=None, url_base="/"):
|
||||
self.base_path = base_path
|
||||
self.url_base = url_base
|
||||
|
||||
def __call__(self, request, response):
|
||||
path = filesystem_path(self.base_path, request, self.url_base)
|
||||
if os.path.isdir(path):
|
||||
raise HTTPException(
|
||||
500, "AsIsHandler cannot process directory, %s" % path)
|
||||
|
||||
try:
|
||||
with open(path, 'rb') as f:
|
||||
response.writer.write_raw_content(f.read())
|
||||
wrap_pipeline(path, request, response)
|
||||
response.close_connection = True
|
||||
except OSError:
|
||||
raise HTTPException(404)
|
||||
|
||||
|
||||
as_is_handler = AsIsHandler() # type: ignore
|
||||
|
||||
|
||||
class BasicAuthHandler:
|
||||
def __init__(self, handler, user, password):
|
||||
"""
|
||||
A Basic Auth handler
|
||||
|
||||
:Args:
|
||||
- handler: a secondary handler for the request after authentication is successful (example file_handler)
|
||||
- user: string of the valid user name or None if any / all credentials are allowed
|
||||
- password: string of the password required
|
||||
"""
|
||||
self.user = user
|
||||
self.password = password
|
||||
self.handler = handler
|
||||
|
||||
def __call__(self, request, response):
|
||||
if "authorization" not in request.headers:
|
||||
response.status = 401
|
||||
response.headers.set("WWW-Authenticate", "Basic")
|
||||
return response
|
||||
else:
|
||||
auth = Authentication(request.headers)
|
||||
if self.user is not None and (self.user != auth.username or self.password != auth.password):
|
||||
response.set_error(403, "Invalid username or password")
|
||||
return response
|
||||
return self.handler(request, response)
|
||||
|
||||
|
||||
basic_auth_handler = BasicAuthHandler(file_handler, None, None) # type: ignore
|
||||
|
||||
|
||||
class ErrorHandler:
|
||||
def __init__(self, status):
|
||||
self.status = status
|
||||
|
||||
def __call__(self, request, response):
|
||||
response.set_error(self.status)
|
||||
|
||||
|
||||
class StringHandler:
|
||||
def __init__(self, data, content_type, **headers):
|
||||
"""Handler that returns a fixed data string and headers
|
||||
|
||||
:param data: String to use
|
||||
:param content_type: Content type header to server the response with
|
||||
:param headers: List of headers to send with responses"""
|
||||
|
||||
self.data = data
|
||||
|
||||
self.resp_headers = [("Content-Type", content_type)]
|
||||
for k, v in headers.items():
|
||||
self.resp_headers.append((k.replace("_", "-"), v))
|
||||
|
||||
self.handler = handler(self.handle_request)
|
||||
|
||||
def handle_request(self, request, response):
|
||||
return self.resp_headers, self.data
|
||||
|
||||
def __call__(self, request, response):
|
||||
rv = self.handler(request, response)
|
||||
return rv
|
||||
|
||||
|
||||
class StaticHandler:
|
||||
def __init__(self, path, format_args, content_type, **headers):
|
||||
"""Handler that reads a file from a path and substitutes some fixed data
|
||||
|
||||
Note that *.headers files have no effect in this handler.
|
||||
|
||||
:param path: Path(s) to template files to use. If a sequence of paths is provided instead
|
||||
of a single path, the contents of each file will be concatenated together before the
|
||||
`format_args` are interpolated.
|
||||
:param format_args: Dictionary of values to substitute into the template file
|
||||
:param content_type: Content type header to server the response with
|
||||
:param headers: List of headers to send with responses"""
|
||||
self._paths = [path] if isinstance(path, str) else path
|
||||
self._format_args = format_args
|
||||
self._content_type = content_type
|
||||
self._headers = headers
|
||||
self._handler = None
|
||||
|
||||
def __getnewargs_ex__(self):
|
||||
# Do not pickle `self._handler`, which can be arbitrarily large.
|
||||
args = self._paths, self._format_args, self._content_type
|
||||
return args, self._headers
|
||||
|
||||
def __call__(self, request, response):
|
||||
# Load the static file contents lazily so that this handler can be
|
||||
# pickled and sent to child processes efficiently. Transporting file
|
||||
# contents across processes can slow `wptserve` startup by several
|
||||
# seconds (crbug.com/1479850).
|
||||
if not self._handler:
|
||||
data = ""
|
||||
for path in self._paths:
|
||||
with open(path) as f:
|
||||
data += f.read()
|
||||
if self._format_args:
|
||||
data = data % self._format_args
|
||||
self._handler = StringHandler(data, self._content_type, **self._headers)
|
||||
return self._handler(request, response)
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import logging
|
||||
|
||||
def get_logger() -> logging.Logger:
|
||||
# Use the root logger
|
||||
return logging.getLogger()
|
||||
|
|
@ -0,0 +1,561 @@
|
|||
# mypy: allow-untyped-defs
|
||||
|
||||
from collections import deque
|
||||
import base64
|
||||
import gzip as gzip_module
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from html import escape
|
||||
from io import BytesIO
|
||||
from typing import Any, Callable, ClassVar, Dict, Optional, TypeVar
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
def resolve_content(response):
|
||||
return b"".join(item for item in response.iter_content(read_file=True))
|
||||
|
||||
|
||||
class Pipeline:
|
||||
pipes: ClassVar[Dict[str, Callable[..., Any]]] = {}
|
||||
|
||||
def __init__(self, pipe_string):
|
||||
self.pipe_functions = self.parse(pipe_string)
|
||||
|
||||
def parse(self, pipe_string):
|
||||
functions = []
|
||||
for item in PipeTokenizer().tokenize(pipe_string):
|
||||
if not item:
|
||||
break
|
||||
if item[0] == "function":
|
||||
functions.append((self.pipes[item[1]], []))
|
||||
elif item[0] == "argument":
|
||||
functions[-1][1].append(item[1])
|
||||
return functions
|
||||
|
||||
def __call__(self, request, response):
|
||||
for func, args in self.pipe_functions:
|
||||
response = func(request, response, *args)
|
||||
return response
|
||||
|
||||
|
||||
class PipeTokenizer:
|
||||
def __init__(self):
|
||||
#This whole class can likely be replaced by some regexps
|
||||
self.state = None
|
||||
|
||||
def tokenize(self, string):
|
||||
self.string = string
|
||||
self.state = self.func_name_state
|
||||
self._index = 0
|
||||
while self.state:
|
||||
yield self.state()
|
||||
yield None
|
||||
|
||||
def get_char(self):
|
||||
if self._index >= len(self.string):
|
||||
return None
|
||||
rv = self.string[self._index]
|
||||
self._index += 1
|
||||
return rv
|
||||
|
||||
def func_name_state(self):
|
||||
rv = ""
|
||||
while True:
|
||||
char = self.get_char()
|
||||
if char is None:
|
||||
self.state = None
|
||||
if rv:
|
||||
return ("function", rv)
|
||||
else:
|
||||
return None
|
||||
elif char == "(":
|
||||
self.state = self.argument_state
|
||||
return ("function", rv)
|
||||
elif char == "|":
|
||||
if rv:
|
||||
return ("function", rv)
|
||||
else:
|
||||
rv += char
|
||||
|
||||
def argument_state(self):
|
||||
rv = ""
|
||||
while True:
|
||||
char = self.get_char()
|
||||
if char is None:
|
||||
self.state = None
|
||||
return ("argument", rv)
|
||||
elif char == "\\":
|
||||
rv += self.get_escape()
|
||||
if rv is None:
|
||||
#This should perhaps be an error instead
|
||||
return ("argument", rv)
|
||||
elif char == ",":
|
||||
return ("argument", rv)
|
||||
elif char == ")":
|
||||
self.state = self.func_name_state
|
||||
return ("argument", rv)
|
||||
else:
|
||||
rv += char
|
||||
|
||||
def get_escape(self):
|
||||
char = self.get_char()
|
||||
escapes = {"n": "\n",
|
||||
"r": "\r",
|
||||
"t": "\t"}
|
||||
return escapes.get(char, char)
|
||||
|
||||
|
||||
class pipe:
|
||||
def __init__(self, *arg_converters: Callable[[str], Any]):
|
||||
self.arg_converters = arg_converters
|
||||
self.max_args = len(self.arg_converters)
|
||||
self.min_args = 0
|
||||
opt_seen = False
|
||||
for item in self.arg_converters:
|
||||
if not opt_seen:
|
||||
if isinstance(item, opt):
|
||||
opt_seen = True
|
||||
else:
|
||||
self.min_args += 1
|
||||
else:
|
||||
if not isinstance(item, opt):
|
||||
raise ValueError("Non-optional argument cannot follow optional argument")
|
||||
|
||||
def __call__(self, f):
|
||||
def inner(request, response, *args):
|
||||
if not (self.min_args <= len(args) <= self.max_args):
|
||||
raise ValueError("Expected between %d and %d args, got %d" %
|
||||
(self.min_args, self.max_args, len(args)))
|
||||
arg_values = tuple(f(x) for f, x in zip(self.arg_converters, args))
|
||||
return f(request, response, *arg_values)
|
||||
Pipeline.pipes[f.__name__] = inner
|
||||
#We actually want the undecorated function in the main namespace
|
||||
return f
|
||||
|
||||
|
||||
class opt:
|
||||
def __init__(self, f: Callable[[str], Any]):
|
||||
self.f = f
|
||||
|
||||
def __call__(self, arg: str) -> Any:
|
||||
return self.f(arg)
|
||||
|
||||
|
||||
def nullable(func: Callable[[str], T]) -> Callable[[str], Optional[T]]:
|
||||
def inner(arg: str) -> Optional[T]:
|
||||
if arg.lower() == "null":
|
||||
return None
|
||||
else:
|
||||
return func(arg)
|
||||
return inner
|
||||
|
||||
|
||||
def boolean(arg: str) -> bool:
|
||||
if arg.lower() in ("true", "1"):
|
||||
return True
|
||||
elif arg.lower() in ("false", "0"):
|
||||
return False
|
||||
raise ValueError
|
||||
|
||||
|
||||
@pipe(int)
|
||||
def status(request, response, code):
|
||||
"""Alter the status code.
|
||||
|
||||
:param code: Status code to use for the response."""
|
||||
response.status = code
|
||||
return response
|
||||
|
||||
|
||||
@pipe(str, str, opt(boolean))
|
||||
def header(request, response, name, value, append=False):
|
||||
"""Set a HTTP header.
|
||||
|
||||
Replaces any existing HTTP header of the same name unless
|
||||
append is set, in which case the header is appended without
|
||||
replacement.
|
||||
|
||||
:param name: Name of the header to set.
|
||||
:param value: Value to use for the header.
|
||||
:param append: True if existing headers should not be replaced
|
||||
"""
|
||||
if not append:
|
||||
response.headers.set(name, value)
|
||||
else:
|
||||
response.headers.append(name, value)
|
||||
return response
|
||||
|
||||
|
||||
@pipe(str)
|
||||
def trickle(request, response, delays):
|
||||
"""Send the response in parts, with time delays.
|
||||
|
||||
:param delays: A string of delays and amounts, in bytes, of the
|
||||
response to send. Each component is separated by
|
||||
a colon. Amounts in bytes are plain integers, whilst
|
||||
delays are floats prefixed with a single d e.g.
|
||||
d1:100:d2
|
||||
Would cause a 1 second delay, would then send 100 bytes
|
||||
of the file, and then cause a 2 second delay, before sending
|
||||
the remainder of the file.
|
||||
|
||||
If the last token is of the form rN, instead of sending the
|
||||
remainder of the file, the previous N instructions will be
|
||||
repeated until the whole file has been sent e.g.
|
||||
d1:100:d2:r2
|
||||
Causes a delay of 1s, then 100 bytes to be sent, then a 2s delay
|
||||
and then a further 100 bytes followed by a two second delay
|
||||
until the response has been fully sent.
|
||||
"""
|
||||
def parse_delays():
|
||||
parts = delays.split(":")
|
||||
rv = []
|
||||
for item in parts:
|
||||
if item.startswith("d"):
|
||||
item_type = "delay"
|
||||
item = item[1:]
|
||||
value = float(item)
|
||||
elif item.startswith("r"):
|
||||
item_type = "repeat"
|
||||
value = int(item[1:])
|
||||
if not value % 2 == 0:
|
||||
raise ValueError
|
||||
else:
|
||||
item_type = "bytes"
|
||||
value = int(item)
|
||||
if len(rv) and rv[-1][0] == item_type:
|
||||
rv[-1][1] += value
|
||||
else:
|
||||
rv.append((item_type, value))
|
||||
return rv
|
||||
|
||||
delays = parse_delays()
|
||||
if not delays:
|
||||
return response
|
||||
content = resolve_content(response)
|
||||
offset = [0]
|
||||
|
||||
if not ("Cache-Control" in response.headers or
|
||||
"Pragma" in response.headers or
|
||||
"Expires" in response.headers):
|
||||
response.headers.set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
response.headers.set("Pragma", "no-cache")
|
||||
response.headers.set("Expires", "0")
|
||||
|
||||
def add_content(delays, repeat=False):
|
||||
for i, (item_type, value) in enumerate(delays):
|
||||
if item_type == "bytes":
|
||||
yield content[offset[0]:offset[0] + value]
|
||||
offset[0] += value
|
||||
elif item_type == "delay":
|
||||
time.sleep(value)
|
||||
elif item_type == "repeat":
|
||||
if i != len(delays) - 1:
|
||||
continue
|
||||
while offset[0] < len(content):
|
||||
yield from add_content(delays[-(value + 1):-1], True)
|
||||
|
||||
if not repeat and offset[0] < len(content):
|
||||
yield content[offset[0]:]
|
||||
|
||||
response.content = add_content(delays)
|
||||
return response
|
||||
|
||||
|
||||
@pipe(nullable(int), opt(nullable(int)))
|
||||
def slice(request, response, start, end=None):
|
||||
"""Send a byte range of the response body
|
||||
|
||||
:param start: The starting offset. Follows python semantics including
|
||||
negative numbers.
|
||||
|
||||
:param end: The ending offset, again with python semantics and None
|
||||
(spelled "null" in a query string) to indicate the end of
|
||||
the file.
|
||||
"""
|
||||
content = resolve_content(response)[start:end]
|
||||
response.content = content
|
||||
response.headers.set("Content-Length", len(content))
|
||||
return response
|
||||
|
||||
|
||||
class ReplacementTokenizer:
|
||||
def arguments(self, token):
|
||||
unwrapped = token[1:-1].decode('utf8')
|
||||
return ("arguments", re.split(r",\s*", unwrapped) if unwrapped else [])
|
||||
|
||||
def ident(self, token):
|
||||
return ("ident", token.decode('utf8'))
|
||||
|
||||
def index(self, token):
|
||||
token = token[1:-1].decode('utf8')
|
||||
try:
|
||||
index = int(token)
|
||||
except ValueError:
|
||||
index = token
|
||||
return ("index", index)
|
||||
|
||||
def var(self, token):
|
||||
token = token[:-1].decode('utf8')
|
||||
return ("var", token)
|
||||
|
||||
def tokenize(self, string):
|
||||
assert isinstance(string, bytes)
|
||||
return self.scanner.scan(string)[0]
|
||||
|
||||
# re.Scanner is missing from typeshed:
|
||||
# https://github.com/python/typeshed/pull/3071
|
||||
scanner = re.Scanner([(br"\$\w+:", var), # type: ignore
|
||||
(br"\$?\w+", ident),
|
||||
(br"\[[^\]]*\]", index),
|
||||
(br"\([^)]*\)", arguments)])
|
||||
|
||||
|
||||
class FirstWrapper:
|
||||
def __init__(self, params):
|
||||
self.params = params
|
||||
|
||||
def __getitem__(self, key):
|
||||
try:
|
||||
if isinstance(key, str):
|
||||
key = key.encode('iso-8859-1')
|
||||
return self.params.first(key)
|
||||
except KeyError:
|
||||
return ""
|
||||
|
||||
|
||||
@pipe(opt(nullable(str)))
|
||||
def sub(request, response, escape_type="html"):
|
||||
"""Substitute environment information about the server and request into the script.
|
||||
|
||||
:param escape_type: String detailing the type of escaping to use. Known values are
|
||||
"html" and "none", with "html" the default for historic reasons.
|
||||
|
||||
The format is a very limited template language. Substitutions are
|
||||
enclosed by {{ and }}. There are several available substitutions:
|
||||
|
||||
host
|
||||
A simple string value and represents the primary host from which the
|
||||
tests are being run.
|
||||
domains
|
||||
A dictionary of available domains indexed by subdomain name.
|
||||
ports
|
||||
A dictionary of lists of ports indexed by protocol.
|
||||
location
|
||||
A dictionary of parts of the request URL. Valid keys are
|
||||
'server, 'scheme', 'host', 'hostname', 'port', 'path' and 'query'.
|
||||
'server' is scheme://host:port, 'host' is hostname:port, and query
|
||||
includes the leading '?', but other delimiters are omitted.
|
||||
headers
|
||||
A dictionary of HTTP headers in the request.
|
||||
header_or_default(header, default)
|
||||
The value of an HTTP header, or a default value if it is absent.
|
||||
For example::
|
||||
|
||||
{{header_or_default(X-Test, test-header-absent)}}
|
||||
|
||||
GET
|
||||
A dictionary of query parameters supplied with the request.
|
||||
uuid()
|
||||
A pesudo-random UUID suitable for usage with stash
|
||||
file_hash(algorithm, filepath)
|
||||
The cryptographic hash of a file. Supported algorithms: md5, sha1,
|
||||
sha224, sha256, sha384, and sha512. For example::
|
||||
|
||||
{{file_hash(md5, dom/interfaces.html)}}
|
||||
|
||||
fs_path(filepath)
|
||||
The absolute path to a file inside the wpt document root
|
||||
|
||||
So for example in a setup running on localhost with a www
|
||||
subdomain and a http server on ports 80 and 81::
|
||||
|
||||
{{host}} => localhost
|
||||
{{domains[www]}} => www.localhost
|
||||
{{ports[http][1]}} => 81
|
||||
|
||||
It is also possible to assign a value to a variable name, which must start
|
||||
with the $ character, using the ":" syntax e.g.::
|
||||
|
||||
{{$id:uuid()}}
|
||||
|
||||
Later substitutions in the same file may then refer to the variable
|
||||
by name e.g.::
|
||||
|
||||
{{$id}}
|
||||
"""
|
||||
content = resolve_content(response)
|
||||
|
||||
new_content = template(request, content, escape_type=escape_type)
|
||||
|
||||
response.content = new_content
|
||||
return response
|
||||
|
||||
class SubFunctions:
|
||||
@staticmethod
|
||||
def uuid(request):
|
||||
return str(uuid.uuid4())
|
||||
|
||||
# Maintain a list of supported algorithms, restricted to those that are
|
||||
# available on all platforms [1]. This ensures that test authors do not
|
||||
# unknowingly introduce platform-specific tests.
|
||||
#
|
||||
# [1] https://docs.python.org/2/library/hashlib.html
|
||||
supported_algorithms = ("md5", "sha1", "sha224", "sha256", "sha384", "sha512")
|
||||
|
||||
@staticmethod
|
||||
def file_hash(request, algorithm, path):
|
||||
assert isinstance(algorithm, str)
|
||||
if algorithm not in SubFunctions.supported_algorithms:
|
||||
raise ValueError("Unsupported encryption algorithm: '%s'" % algorithm)
|
||||
|
||||
hash_obj = getattr(hashlib, algorithm)()
|
||||
absolute_path = os.path.join(request.doc_root, path)
|
||||
|
||||
try:
|
||||
with open(absolute_path, "rb") as f:
|
||||
hash_obj.update(f.read())
|
||||
except OSError:
|
||||
# In this context, an unhandled IOError will be interpreted by the
|
||||
# server as an indication that the template file is non-existent.
|
||||
# Although the generic "Exception" is less precise, it avoids
|
||||
# triggering a potentially-confusing HTTP 404 error in cases where
|
||||
# the path to the file to be hashed is invalid.
|
||||
raise Exception('Cannot open file for hash computation: "%s"' % absolute_path)
|
||||
|
||||
return base64.b64encode(hash_obj.digest()).strip()
|
||||
|
||||
@staticmethod
|
||||
def fs_path(request, path):
|
||||
if not path.startswith("/"):
|
||||
subdir = request.request_path[len(request.url_base):]
|
||||
if "/" in subdir:
|
||||
subdir = subdir.rsplit("/", 1)[0]
|
||||
root_rel_path = subdir + "/" + path
|
||||
else:
|
||||
root_rel_path = path[1:]
|
||||
root_rel_path = root_rel_path.replace("/", os.path.sep)
|
||||
absolute_path = os.path.abspath(os.path.join(request.doc_root, root_rel_path))
|
||||
if ".." in os.path.relpath(absolute_path, request.doc_root):
|
||||
raise ValueError("Path outside wpt root")
|
||||
return absolute_path
|
||||
|
||||
@staticmethod
|
||||
def header_or_default(request, name, default):
|
||||
return request.headers.get(name, default)
|
||||
|
||||
def template(request, content, escape_type="html"):
|
||||
#TODO: There basically isn't any error handling here
|
||||
tokenizer = ReplacementTokenizer()
|
||||
|
||||
variables = {}
|
||||
|
||||
def config_replacement(match):
|
||||
content, = match.groups()
|
||||
|
||||
tokens = tokenizer.tokenize(content)
|
||||
tokens = deque(tokens)
|
||||
|
||||
token_type, field = tokens.popleft()
|
||||
assert isinstance(field, str)
|
||||
|
||||
if token_type == "var":
|
||||
variable = field
|
||||
token_type, field = tokens.popleft()
|
||||
assert isinstance(field, str)
|
||||
else:
|
||||
variable = None
|
||||
|
||||
if token_type != "ident":
|
||||
raise Exception("unexpected token type %s (token '%r'), expected ident" % (token_type, field))
|
||||
|
||||
if field in variables:
|
||||
value = variables[field]
|
||||
elif hasattr(SubFunctions, field):
|
||||
value = getattr(SubFunctions, field)
|
||||
elif field == "headers":
|
||||
value = request.headers
|
||||
elif field == "GET":
|
||||
value = FirstWrapper(request.GET)
|
||||
elif field == "hosts":
|
||||
value = request.server.config.all_domains
|
||||
elif field == "domains":
|
||||
value = request.server.config.all_domains[""]
|
||||
elif field == "host":
|
||||
value = request.server.config["browser_host"]
|
||||
elif field in request.server.config:
|
||||
value = request.server.config[field]
|
||||
elif field == "location":
|
||||
value = {"server": "%s://%s:%s" % (request.url_parts.scheme,
|
||||
request.url_parts.hostname,
|
||||
request.url_parts.port),
|
||||
"scheme": request.url_parts.scheme,
|
||||
"host": "%s:%s" % (request.url_parts.hostname,
|
||||
request.url_parts.port),
|
||||
"hostname": request.url_parts.hostname,
|
||||
"port": request.url_parts.port,
|
||||
"path": request.url_parts.path,
|
||||
"pathname": request.url_parts.path,
|
||||
"query": "?%s" % request.url_parts.query}
|
||||
elif field == "url_base":
|
||||
value = request.url_base
|
||||
else:
|
||||
raise Exception("Undefined template variable %s" % field)
|
||||
|
||||
while tokens:
|
||||
ttype, field = tokens.popleft()
|
||||
if ttype == "index":
|
||||
value = value[field]
|
||||
elif ttype == "arguments":
|
||||
value = value(request, *field)
|
||||
else:
|
||||
raise Exception(
|
||||
"unexpected token type %s (token '%r'), expected ident or arguments" % (ttype, field)
|
||||
)
|
||||
|
||||
assert isinstance(value, (int, (bytes, str))), tokens
|
||||
|
||||
if variable is not None:
|
||||
variables[variable] = value
|
||||
|
||||
escape_func = {"html": lambda x:escape(x, quote=True),
|
||||
"none": lambda x:x}[escape_type]
|
||||
|
||||
# Should possibly support escaping for other contexts e.g. script
|
||||
# TODO: read the encoding of the response
|
||||
# cgi.escape() only takes text strings in Python 3.
|
||||
if isinstance(value, bytes):
|
||||
value = value.decode("utf-8")
|
||||
elif isinstance(value, int):
|
||||
value = str(value)
|
||||
return escape_func(value).encode("utf-8")
|
||||
|
||||
template_regexp = re.compile(br"{{([^}]*)}}")
|
||||
new_content = template_regexp.sub(config_replacement, content)
|
||||
|
||||
return new_content
|
||||
|
||||
@pipe()
|
||||
def gzip(request, response):
|
||||
"""This pipe gzip-encodes response data.
|
||||
|
||||
It sets (or overwrites) these HTTP headers:
|
||||
Content-Encoding is set to gzip
|
||||
Content-Length is set to the length of the compressed content
|
||||
"""
|
||||
content = resolve_content(response)
|
||||
response.headers.set("Content-Encoding", "gzip")
|
||||
|
||||
out = BytesIO()
|
||||
with gzip_module.GzipFile(fileobj=out, mode="w") as f:
|
||||
f.write(content)
|
||||
response.content = out.getvalue()
|
||||
|
||||
response.headers.set("Content-Length", len(response.content))
|
||||
|
||||
return response
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
# mypy: allow-untyped-defs
|
||||
|
||||
from .utils import HTTPException
|
||||
|
||||
|
||||
class RangeParser:
|
||||
def __call__(self, header, file_size):
|
||||
try:
|
||||
header = header.decode("ascii")
|
||||
except UnicodeDecodeError:
|
||||
raise HTTPException(400, "Non-ASCII range header value")
|
||||
prefix = "bytes="
|
||||
if not header.startswith(prefix):
|
||||
raise HTTPException(416, message=f"Unrecognised range type {header}")
|
||||
|
||||
parts = header[len(prefix):].split(",")
|
||||
ranges = []
|
||||
for item in parts:
|
||||
components = item.split("-")
|
||||
if len(components) != 2:
|
||||
raise HTTPException(416, "Bad range specifier %s" % (item))
|
||||
data = []
|
||||
for component in components:
|
||||
if component == "":
|
||||
data.append(None)
|
||||
else:
|
||||
try:
|
||||
data.append(int(component))
|
||||
except ValueError:
|
||||
raise HTTPException(416, "Bad range specifier %s" % (item))
|
||||
try:
|
||||
ranges.append(Range(data[0], data[1], file_size))
|
||||
except ValueError:
|
||||
raise HTTPException(416, "Bad range specifier %s" % (item))
|
||||
|
||||
return self.coalesce_ranges(ranges, file_size)
|
||||
|
||||
def coalesce_ranges(self, ranges, file_size):
|
||||
rv = []
|
||||
target = None
|
||||
for current in reversed(sorted(ranges)):
|
||||
if target is None:
|
||||
target = current
|
||||
else:
|
||||
new = target.coalesce(current)
|
||||
target = new[0]
|
||||
if len(new) > 1:
|
||||
rv.append(new[1])
|
||||
rv.append(target)
|
||||
|
||||
return rv[::-1]
|
||||
|
||||
|
||||
class Range:
|
||||
def __init__(self, lower, upper, file_size):
|
||||
self.file_size = file_size
|
||||
self.lower, self.upper = self._abs(lower, upper)
|
||||
if self.lower >= self.upper or self.lower >= self.file_size:
|
||||
raise ValueError
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Range {self.lower}-{self.upper}>"
|
||||
|
||||
def __lt__(self, other):
|
||||
return self.lower < other.lower
|
||||
|
||||
def __gt__(self, other):
|
||||
return self.lower > other.lower
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.lower == other.lower and self.upper == other.upper
|
||||
|
||||
def _abs(self, lower, upper):
|
||||
if lower is None and upper is None:
|
||||
lower, upper = 0, self.file_size
|
||||
elif lower is None:
|
||||
lower, upper = max(0, self.file_size - upper), self.file_size
|
||||
elif upper is None:
|
||||
lower, upper = lower, self.file_size
|
||||
else:
|
||||
lower, upper = lower, min(self.file_size, upper + 1)
|
||||
|
||||
return lower, upper
|
||||
|
||||
def coalesce(self, other):
|
||||
assert self.file_size == other.file_size
|
||||
|
||||
if (self.upper < other.lower or self.lower > other.upper):
|
||||
return sorted([self, other])
|
||||
else:
|
||||
return [Range(min(self.lower, other.lower),
|
||||
max(self.upper, other.upper) - 1,
|
||||
self.file_size)]
|
||||
|
||||
def header_value(self):
|
||||
return "bytes %i-%i/%i" % (self.lower, self.upper - 1, self.file_size)
|
||||
|
|
@ -0,0 +1,710 @@
|
|||
# mypy: allow-untyped-defs
|
||||
|
||||
import base64
|
||||
import tempfile
|
||||
|
||||
from http.cookies import BaseCookie
|
||||
from io import BytesIO
|
||||
from typing import Dict, List, TypeVar
|
||||
from urllib.parse import parse_qsl, urlsplit
|
||||
|
||||
from . import stash
|
||||
from .cgi import FieldStorage
|
||||
from .utils import HTTPException, isomorphic_encode, isomorphic_decode
|
||||
|
||||
KT = TypeVar('KT')
|
||||
VT = TypeVar('VT')
|
||||
|
||||
missing = object()
|
||||
|
||||
|
||||
class Server:
|
||||
"""Data about the server environment
|
||||
|
||||
.. attribute:: config
|
||||
|
||||
Environment configuration information with information about the
|
||||
various servers running, their hostnames and ports.
|
||||
|
||||
.. attribute:: stash
|
||||
|
||||
Stash object holding state stored on the server between requests.
|
||||
|
||||
"""
|
||||
config = None
|
||||
|
||||
def __init__(self, request):
|
||||
self._stash = None
|
||||
self._request = request
|
||||
|
||||
@property
|
||||
def stash(self):
|
||||
if self._stash is None:
|
||||
address, authkey = stash.load_env_config()
|
||||
self._stash = stash.Stash(self._request.url_parts.path, address, authkey)
|
||||
return self._stash
|
||||
|
||||
|
||||
class InputFile:
|
||||
max_buffer_size = 1024*1024
|
||||
|
||||
def __init__(self, rfile, length):
|
||||
"""File-like object used to provide a seekable view of request body data"""
|
||||
self._file = rfile
|
||||
self.length = length
|
||||
|
||||
self._file_position = 0
|
||||
|
||||
if length > self.max_buffer_size:
|
||||
self._buf = tempfile.TemporaryFile()
|
||||
else:
|
||||
self._buf = BytesIO()
|
||||
|
||||
def close(self):
|
||||
self._buf.close()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc):
|
||||
self.close()
|
||||
return False
|
||||
|
||||
@property
|
||||
def _buf_position(self):
|
||||
rv = self._buf.tell()
|
||||
assert rv <= self._file_position
|
||||
return rv
|
||||
|
||||
def read(self, bytes=-1):
|
||||
assert self._buf_position <= self._file_position
|
||||
|
||||
if bytes < 0:
|
||||
bytes = self.length - self._buf_position
|
||||
bytes_remaining = min(bytes, self.length - self._buf_position)
|
||||
|
||||
if bytes_remaining == 0:
|
||||
return b""
|
||||
|
||||
if self._buf_position != self._file_position:
|
||||
buf_bytes = min(bytes_remaining, self._file_position - self._buf_position)
|
||||
old_data = self._buf.read(buf_bytes)
|
||||
bytes_remaining -= buf_bytes
|
||||
else:
|
||||
old_data = b""
|
||||
|
||||
assert bytes_remaining == 0 or self._buf_position == self._file_position, (
|
||||
"Before reading buffer position (%i) didn't match file position (%i)" %
|
||||
(self._buf_position, self._file_position))
|
||||
new_data = self._file.read(bytes_remaining)
|
||||
self._buf.write(new_data)
|
||||
self._file_position += bytes_remaining
|
||||
assert bytes_remaining == 0 or self._buf_position == self._file_position, (
|
||||
"After reading buffer position (%i) didn't match file position (%i)" %
|
||||
(self._buf_position, self._file_position))
|
||||
|
||||
return old_data + new_data
|
||||
|
||||
def tell(self):
|
||||
return self._buf_position
|
||||
|
||||
def seek(self, offset):
|
||||
if offset > self.length or offset < 0:
|
||||
raise ValueError
|
||||
if offset <= self._file_position:
|
||||
self._buf.seek(offset)
|
||||
else:
|
||||
self.read(offset - self._file_position)
|
||||
|
||||
def readline(self, max_bytes=None):
|
||||
if max_bytes is None:
|
||||
max_bytes = self.length - self._buf_position
|
||||
|
||||
if self._buf_position < self._file_position:
|
||||
data = self._buf.readline(max_bytes)
|
||||
if data.endswith(b"\n") or len(data) == max_bytes:
|
||||
return data
|
||||
else:
|
||||
data = b""
|
||||
|
||||
assert self._buf_position == self._file_position
|
||||
|
||||
initial_position = self._file_position
|
||||
found = False
|
||||
buf = []
|
||||
max_bytes -= len(data)
|
||||
while not found:
|
||||
readahead = self.read(min(2, max_bytes))
|
||||
max_bytes -= len(readahead)
|
||||
for i, c in enumerate(readahead):
|
||||
if c == b"\n"[0]:
|
||||
buf.append(readahead[:i+1])
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
buf.append(readahead)
|
||||
if not readahead or not max_bytes:
|
||||
break
|
||||
new_data = b"".join(buf)
|
||||
data += new_data
|
||||
self.seek(initial_position + len(new_data))
|
||||
return data
|
||||
|
||||
def readlines(self):
|
||||
rv = []
|
||||
while True:
|
||||
data = self.readline()
|
||||
if data:
|
||||
rv.append(data)
|
||||
else:
|
||||
break
|
||||
return rv
|
||||
|
||||
def __next__(self):
|
||||
data = self.readline()
|
||||
if data:
|
||||
return data
|
||||
else:
|
||||
raise StopIteration
|
||||
|
||||
next = __next__
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
|
||||
class Request:
|
||||
"""Object representing a HTTP request.
|
||||
|
||||
.. attribute:: doc_root
|
||||
|
||||
The local directory to use as a base when resolving paths
|
||||
|
||||
.. attribute:: route_match
|
||||
|
||||
Regexp match object from matching the request path to the route
|
||||
selected for the request.
|
||||
|
||||
.. attribute:: client_address
|
||||
|
||||
Contains a tuple of the form (host, port) representing the client's address.
|
||||
|
||||
.. attribute:: protocol_version
|
||||
|
||||
HTTP version specified in the request.
|
||||
|
||||
.. attribute:: method
|
||||
|
||||
HTTP method in the request.
|
||||
|
||||
.. attribute:: request_path
|
||||
|
||||
Request path as it appears in the HTTP request.
|
||||
|
||||
.. attribute:: url_base
|
||||
|
||||
The prefix part of the path; typically / unless the handler has a url_base set
|
||||
|
||||
.. attribute:: url
|
||||
|
||||
Absolute URL for the request.
|
||||
|
||||
.. attribute:: url_parts
|
||||
|
||||
Parts of the requested URL as obtained by urlparse.urlsplit(path)
|
||||
|
||||
.. attribute:: request_line
|
||||
|
||||
Raw request line
|
||||
|
||||
.. attribute:: headers
|
||||
|
||||
RequestHeaders object providing a dictionary-like representation of
|
||||
the request headers.
|
||||
|
||||
.. attribute:: raw_headers.
|
||||
|
||||
Dictionary of non-normalized request headers.
|
||||
|
||||
.. attribute:: body
|
||||
|
||||
Request body as a string
|
||||
|
||||
.. attribute:: raw_input
|
||||
|
||||
File-like object representing the body of the request.
|
||||
|
||||
.. attribute:: GET
|
||||
|
||||
MultiDict representing the parameters supplied with the request.
|
||||
Note that these may be present on non-GET requests; the name is
|
||||
chosen to be familiar to users of other systems such as PHP.
|
||||
Both keys and values are binary strings.
|
||||
|
||||
.. attribute:: POST
|
||||
|
||||
MultiDict representing the request body parameters. Most parameters
|
||||
are present as string values, but file uploads have file-like
|
||||
values. All string values (including keys) have binary type.
|
||||
|
||||
.. attribute:: cookies
|
||||
|
||||
A Cookies object representing cookies sent with the request with a
|
||||
dictionary-like interface.
|
||||
|
||||
.. attribute:: auth
|
||||
|
||||
An instance of Authentication with username and password properties
|
||||
representing any credentials supplied using HTTP authentication.
|
||||
|
||||
.. attribute:: server
|
||||
|
||||
Server object containing information about the server environment.
|
||||
"""
|
||||
|
||||
def __init__(self, request_handler):
|
||||
self.doc_root = request_handler.server.router.doc_root
|
||||
self.route_match = None # Set by the router
|
||||
self.client_address = request_handler.client_address
|
||||
|
||||
self.protocol_version = request_handler.protocol_version
|
||||
self.method = request_handler.command
|
||||
|
||||
# Keys and values in raw headers are native strings.
|
||||
self._headers = None
|
||||
self.raw_headers = request_handler.headers
|
||||
|
||||
scheme = request_handler.server.scheme
|
||||
host = self.raw_headers.get("Host")
|
||||
port = request_handler.server.server_address[1]
|
||||
|
||||
if host is None:
|
||||
host = request_handler.server.server_address[0]
|
||||
else:
|
||||
if ":" in host:
|
||||
host, port = host.split(":", 1)
|
||||
|
||||
self.request_path = request_handler.path
|
||||
self.url_base = "/"
|
||||
|
||||
if self.request_path.startswith(scheme + "://"):
|
||||
self.url = self.request_path
|
||||
else:
|
||||
# TODO(#23362): Stop using native strings for URLs.
|
||||
self.url = "%s://%s:%s%s" % (
|
||||
scheme, host, port, self.request_path)
|
||||
self.url_parts = urlsplit(self.url)
|
||||
|
||||
self.request_line = request_handler.raw_requestline
|
||||
|
||||
self.raw_input = InputFile(request_handler.rfile,
|
||||
int(self.raw_headers.get("Content-Length", 0)))
|
||||
|
||||
self._body = None
|
||||
|
||||
self._GET = None
|
||||
self._POST = None
|
||||
self._cookies = None
|
||||
self._auth = None
|
||||
|
||||
self.server = Server(self)
|
||||
|
||||
def close(self):
|
||||
return self.raw_input.close()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc):
|
||||
self.close()
|
||||
return False
|
||||
|
||||
def __repr__(self):
|
||||
return "<Request %s %s>" % (self.method, self.url)
|
||||
|
||||
@property
|
||||
def GET(self):
|
||||
if self._GET is None:
|
||||
kwargs = {
|
||||
"keep_blank_values": True,
|
||||
"encoding": "iso-8859-1",
|
||||
}
|
||||
params = parse_qsl(self.url_parts.query, **kwargs)
|
||||
self._GET = MultiDict()
|
||||
for key, value in params:
|
||||
self._GET.add(isomorphic_encode(key), isomorphic_encode(value))
|
||||
return self._GET
|
||||
|
||||
@property
|
||||
def POST(self):
|
||||
if self._POST is None:
|
||||
# Work out the post parameters
|
||||
pos = self.raw_input.tell()
|
||||
self.raw_input.seek(0)
|
||||
kwargs = {
|
||||
"fp": self.raw_input,
|
||||
"environ": {"REQUEST_METHOD": self.method},
|
||||
"headers": self.raw_headers,
|
||||
"keep_blank_values": True,
|
||||
"encoding": "iso-8859-1",
|
||||
}
|
||||
fs = FieldStorage(**kwargs)
|
||||
self._POST = MultiDict.from_field_storage(fs)
|
||||
self.raw_input.seek(pos)
|
||||
return self._POST
|
||||
|
||||
@property
|
||||
def cookies(self):
|
||||
if self._cookies is None:
|
||||
parser = BinaryCookieParser()
|
||||
cookie_headers = self.headers.get("cookie", b"")
|
||||
parser.load(cookie_headers)
|
||||
cookies = Cookies()
|
||||
for key, value in parser.items():
|
||||
cookies[isomorphic_encode(key)] = CookieValue(value)
|
||||
self._cookies = cookies
|
||||
return self._cookies
|
||||
|
||||
@property
|
||||
def headers(self):
|
||||
if self._headers is None:
|
||||
self._headers = RequestHeaders(self.raw_headers)
|
||||
return self._headers
|
||||
|
||||
@property
|
||||
def body(self):
|
||||
if self._body is None:
|
||||
pos = self.raw_input.tell()
|
||||
self.raw_input.seek(0)
|
||||
self._body = self.raw_input.read()
|
||||
self.raw_input.seek(pos)
|
||||
return self._body
|
||||
|
||||
@property
|
||||
def auth(self):
|
||||
if self._auth is None:
|
||||
self._auth = Authentication(self.headers)
|
||||
return self._auth
|
||||
|
||||
|
||||
class H2Request(Request):
|
||||
def __init__(self, request_handler):
|
||||
self.h2_stream_id = request_handler.h2_stream_id
|
||||
self.frames = []
|
||||
super().__init__(request_handler)
|
||||
|
||||
|
||||
class RequestHeaders(Dict[bytes, List[bytes]]):
|
||||
"""Read-only dictionary-like API for accessing request headers.
|
||||
|
||||
Unlike BaseHTTPRequestHandler.headers, this class always returns all
|
||||
headers with the same name (separated by commas). And it ensures all keys
|
||||
(i.e. names of headers) and values have binary type.
|
||||
"""
|
||||
def __init__(self, items):
|
||||
for header in items.keys():
|
||||
key = isomorphic_encode(header).lower()
|
||||
# get all headers with the same name
|
||||
values = items.getallmatchingheaders(header)
|
||||
if len(values) > 1:
|
||||
# collect the multiple variations of the current header
|
||||
multiples = []
|
||||
# loop through the values from getallmatchingheaders
|
||||
for value in values:
|
||||
# getallmatchingheaders returns raw header lines, so
|
||||
# split to get name, value
|
||||
multiples.append(isomorphic_encode(value).split(b':', 1)[1].strip())
|
||||
headers = multiples
|
||||
else:
|
||||
headers = [isomorphic_encode(items[header])]
|
||||
dict.__setitem__(self, key, headers)
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""Get all headers of a certain (case-insensitive) name. If there is
|
||||
more than one, the values are returned comma separated"""
|
||||
key = isomorphic_encode(key)
|
||||
values = dict.__getitem__(self, key.lower())
|
||||
if len(values) == 1:
|
||||
return values[0]
|
||||
else:
|
||||
return b", ".join(values)
|
||||
|
||||
def __setitem__(self, name, value):
|
||||
raise Exception
|
||||
|
||||
def get(self, key, default=None):
|
||||
"""Get a string representing all headers with a particular value,
|
||||
with multiple headers separated by a comma. If no header is found
|
||||
return a default value
|
||||
|
||||
:param key: The header name to look up (case-insensitive)
|
||||
:param default: The value to return in the case of no match
|
||||
"""
|
||||
try:
|
||||
return self[key]
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
def get_list(self, key, default=missing):
|
||||
"""Get all the header values for a particular field name as
|
||||
a list"""
|
||||
key = isomorphic_encode(key)
|
||||
try:
|
||||
return dict.__getitem__(self, key.lower())
|
||||
except KeyError:
|
||||
if default is not missing:
|
||||
return default
|
||||
else:
|
||||
raise
|
||||
|
||||
def __contains__(self, key):
|
||||
key = isomorphic_encode(key)
|
||||
return dict.__contains__(self, key.lower())
|
||||
|
||||
def iteritems(self):
|
||||
for item in self:
|
||||
yield item, self[item]
|
||||
|
||||
def itervalues(self):
|
||||
for item in self:
|
||||
yield self[item]
|
||||
|
||||
|
||||
class CookieValue:
|
||||
"""Representation of cookies.
|
||||
|
||||
Note that cookies are considered read-only and the string value
|
||||
of the cookie will not change if you update the field values.
|
||||
However this is not enforced.
|
||||
|
||||
.. attribute:: key
|
||||
|
||||
The name of the cookie.
|
||||
|
||||
.. attribute:: value
|
||||
|
||||
The value of the cookie
|
||||
|
||||
.. attribute:: expires
|
||||
|
||||
The expiry date of the cookie
|
||||
|
||||
.. attribute:: path
|
||||
|
||||
The path of the cookie
|
||||
|
||||
.. attribute:: comment
|
||||
|
||||
The comment of the cookie.
|
||||
|
||||
.. attribute:: domain
|
||||
|
||||
The domain with which the cookie is associated
|
||||
|
||||
.. attribute:: max_age
|
||||
|
||||
The max-age value of the cookie.
|
||||
|
||||
.. attribute:: secure
|
||||
|
||||
Whether the cookie is marked as secure
|
||||
|
||||
.. attribute:: httponly
|
||||
|
||||
Whether the cookie is marked as httponly
|
||||
|
||||
"""
|
||||
def __init__(self, morsel):
|
||||
self.key = morsel.key
|
||||
self.value = morsel.value
|
||||
|
||||
for attr in ["expires", "path",
|
||||
"comment", "domain", "max-age",
|
||||
"secure", "version", "httponly"]:
|
||||
setattr(self, attr.replace("-", "_"), morsel[attr])
|
||||
|
||||
self._str = morsel.OutputString()
|
||||
|
||||
def __str__(self):
|
||||
return self._str
|
||||
|
||||
def __repr__(self):
|
||||
return self._str
|
||||
|
||||
def __eq__(self, other):
|
||||
"""Equality comparison for cookies. Compares to other cookies
|
||||
based on value alone and on non-cookies based on the equality
|
||||
of self.value with the other object so that a cookie with value
|
||||
"ham" compares equal to the string "ham"
|
||||
"""
|
||||
if hasattr(other, "value"):
|
||||
return self.value == other.value
|
||||
return self.value == other
|
||||
|
||||
|
||||
class MultiDict(Dict[KT, VT]):
|
||||
"""Dictionary type that holds multiple values for each key"""
|
||||
# TODO: this should perhaps also order the keys
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def __setitem__(self, name, value):
|
||||
dict.__setitem__(self, name, [value])
|
||||
|
||||
def add(self, name, value):
|
||||
if name in self:
|
||||
dict.__getitem__(self, name).append(value)
|
||||
else:
|
||||
dict.__setitem__(self, name, [value])
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""Get the first value with a given key"""
|
||||
return self.first(key)
|
||||
|
||||
def first(self, key, default=missing):
|
||||
"""Get the first value with a given key
|
||||
|
||||
:param key: The key to lookup
|
||||
:param default: The default to return if key is
|
||||
not found (throws if nothing is
|
||||
specified)
|
||||
"""
|
||||
if key in self and dict.__getitem__(self, key):
|
||||
return dict.__getitem__(self, key)[0]
|
||||
elif default is not missing:
|
||||
return default
|
||||
raise KeyError(key)
|
||||
|
||||
def last(self, key, default=missing):
|
||||
"""Get the last value with a given key
|
||||
|
||||
:param key: The key to lookup
|
||||
:param default: The default to return if key is
|
||||
not found (throws if nothing is
|
||||
specified)
|
||||
"""
|
||||
if key in self and dict.__getitem__(self, key):
|
||||
return dict.__getitem__(self, key)[-1]
|
||||
elif default is not missing:
|
||||
return default
|
||||
raise KeyError(key)
|
||||
|
||||
# We need to explicitly override dict.get; otherwise, it won't call
|
||||
# __getitem__ and would return a list instead.
|
||||
def get(self, key, default=None):
|
||||
"""Get the first value with a given key
|
||||
|
||||
:param key: The key to lookup
|
||||
:param default: The default to return if key is
|
||||
not found (None by default)
|
||||
"""
|
||||
return self.first(key, default)
|
||||
|
||||
def get_list(self, key):
|
||||
"""Get all values with a given key as a list
|
||||
|
||||
:param key: The key to lookup
|
||||
"""
|
||||
if key in self:
|
||||
return dict.__getitem__(self, key)
|
||||
else:
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def from_field_storage(cls, fs):
|
||||
"""Construct a MultiDict from a FieldStorage
|
||||
|
||||
Note that all keys and values are binary strings.
|
||||
"""
|
||||
self = cls()
|
||||
if fs.list is None:
|
||||
return self
|
||||
for key in fs:
|
||||
values = fs[key]
|
||||
if not isinstance(values, list):
|
||||
values = [values]
|
||||
|
||||
for value in values:
|
||||
if not value.filename:
|
||||
value = isomorphic_encode(value.value)
|
||||
else:
|
||||
assert isinstance(value, FieldStorage)
|
||||
self.add(isomorphic_encode(key), value)
|
||||
return self
|
||||
|
||||
|
||||
class BinaryCookieParser(BaseCookie): # type: ignore
|
||||
"""A subclass of BaseCookie that returns values in binary strings
|
||||
|
||||
This is not intended to store the cookies; use Cookies instead.
|
||||
"""
|
||||
def value_decode(self, val):
|
||||
"""Decode value from network to (real_value, coded_value).
|
||||
|
||||
Override BaseCookie.value_decode.
|
||||
"""
|
||||
return isomorphic_encode(val), val
|
||||
|
||||
def value_encode(self, val):
|
||||
raise NotImplementedError('BinaryCookieParser is not for setting cookies')
|
||||
|
||||
def load(self, rawdata):
|
||||
"""Load cookies from a binary string.
|
||||
|
||||
This overrides and calls BaseCookie.load. Unlike BaseCookie.load, it
|
||||
does not accept dictionaries.
|
||||
"""
|
||||
assert isinstance(rawdata, bytes)
|
||||
# BaseCookie.load expects a native string
|
||||
super().load(isomorphic_decode(rawdata))
|
||||
|
||||
|
||||
class Cookies(MultiDict[bytes, CookieValue]):
|
||||
"""MultiDict specialised for Cookie values
|
||||
|
||||
Keys are binary strings and values are CookieValue objects.
|
||||
"""
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.last(key)
|
||||
|
||||
|
||||
class Authentication:
|
||||
"""Object for dealing with HTTP Authentication
|
||||
|
||||
.. attribute:: username
|
||||
|
||||
The username supplied in the HTTP Authorization
|
||||
header, or None
|
||||
|
||||
.. attribute:: password
|
||||
|
||||
The password supplied in the HTTP Authorization
|
||||
header, or None
|
||||
|
||||
Both attributes are binary strings (`str` in Py2, `bytes` in Py3), since
|
||||
RFC7617 Section 2.1 does not specify the encoding for username & password
|
||||
(as long it's compatible with ASCII). UTF-8 should be a relatively safe
|
||||
choice if callers need to decode them as most browsers use it.
|
||||
"""
|
||||
def __init__(self, headers):
|
||||
self.username = None
|
||||
self.password = None
|
||||
|
||||
auth_schemes = {b"Basic": self.decode_basic}
|
||||
|
||||
if "authorization" in headers:
|
||||
header = headers.get("authorization")
|
||||
assert isinstance(header, bytes)
|
||||
auth_type, data = header.split(b" ", 1)
|
||||
if auth_type in auth_schemes:
|
||||
self.username, self.password = auth_schemes[auth_type](data)
|
||||
else:
|
||||
raise HTTPException(400, "Unsupported authentication scheme %s" % auth_type)
|
||||
|
||||
def decode_basic(self, data):
|
||||
assert isinstance(data, bytes)
|
||||
decoded_data = base64.b64decode(data)
|
||||
return decoded_data.split(b":", 1)
|
||||
|
|
@ -0,0 +1,842 @@
|
|||
# mypy: allow-untyped-defs
|
||||
|
||||
import json
|
||||
import uuid
|
||||
import traceback
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from io import BytesIO
|
||||
|
||||
from hpack.struct import HeaderTuple
|
||||
from http.cookies import BaseCookie, Morsel
|
||||
from hyperframe.frame import HeadersFrame, DataFrame, ContinuationFrame
|
||||
|
||||
from .constants import response_codes, h2_headers
|
||||
from .logger import get_logger
|
||||
from .utils import isomorphic_decode, isomorphic_encode
|
||||
|
||||
missing = object()
|
||||
|
||||
|
||||
class Response:
|
||||
"""Object representing the response to a HTTP request
|
||||
|
||||
:param handler: RequestHandler being used for this response
|
||||
:param request: Request that this is the response for
|
||||
|
||||
.. attribute:: request
|
||||
|
||||
Request associated with this Response.
|
||||
|
||||
.. attribute:: encoding
|
||||
|
||||
The encoding to use when converting unicode to strings for output.
|
||||
|
||||
.. attribute:: add_required_headers
|
||||
|
||||
Boolean indicating whether mandatory headers should be added to the
|
||||
response.
|
||||
|
||||
.. attribute:: send_body_for_head_request
|
||||
|
||||
Boolean, default False, indicating whether the body content should be
|
||||
sent when the request method is HEAD.
|
||||
|
||||
.. attribute:: writer
|
||||
|
||||
The ResponseWriter for this response
|
||||
|
||||
.. attribute:: status
|
||||
|
||||
Status tuple (code, message). Can be set to an integer in which case the
|
||||
message part is filled in automatically, or a tuple (code, message) in
|
||||
which case code is an int and message is a text or binary string.
|
||||
|
||||
.. attribute:: headers
|
||||
|
||||
List of HTTP headers to send with the response. Each item in the list is a
|
||||
tuple of (name, value).
|
||||
|
||||
.. attribute:: content
|
||||
|
||||
The body of the response. This can either be a string or a iterable of response
|
||||
parts. If it is an iterable, any item may be a string or a function of zero
|
||||
parameters which, when called, returns a string."""
|
||||
|
||||
def __init__(self, handler, request, response_writer_cls=None):
|
||||
self.request = request
|
||||
self.encoding = "utf8"
|
||||
|
||||
self.add_required_headers = True
|
||||
self.send_body_for_head_request = False
|
||||
self.close_connection = False
|
||||
|
||||
self.logger = get_logger()
|
||||
self.writer = response_writer_cls(handler, self) if response_writer_cls else ResponseWriter(handler, self)
|
||||
|
||||
self._status = (200, None)
|
||||
self.headers = ResponseHeaders()
|
||||
self.content = []
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
return self._status
|
||||
|
||||
@status.setter
|
||||
def status(self, value):
|
||||
if hasattr(value, "__len__"):
|
||||
if len(value) != 2:
|
||||
raise ValueError
|
||||
else:
|
||||
code = int(value[0])
|
||||
message = value[1]
|
||||
# Only call str() if message is not a string type, so that we
|
||||
# don't get `str(b"foo") == "b'foo'"` in Python 3.
|
||||
if not isinstance(message, (bytes, str)):
|
||||
message = str(message)
|
||||
self._status = (code, message)
|
||||
else:
|
||||
self._status = (int(value), None)
|
||||
|
||||
def set_cookie(self, name, value, path="/", domain=None, max_age=None,
|
||||
expires=None, samesite=None, secure=False, httponly=False,
|
||||
comment=None):
|
||||
"""Set a cookie to be sent with a Set-Cookie header in the
|
||||
response
|
||||
|
||||
:param name: name of the cookie (a binary string)
|
||||
:param value: value of the cookie (a binary string, or None)
|
||||
:param max_age: datetime.timedelta int representing the time (in seconds)
|
||||
until the cookie expires
|
||||
:param path: String path to which the cookie applies
|
||||
:param domain: String domain to which the cookie applies
|
||||
:param samesit: String indicating whether the cookie should be
|
||||
restricted to same site context
|
||||
:param secure: Boolean indicating whether the cookie is marked as secure
|
||||
:param httponly: Boolean indicating whether the cookie is marked as
|
||||
HTTP Only
|
||||
:param comment: String comment
|
||||
:param expires: datetime.datetime or datetime.timedelta indicating a
|
||||
time or interval from now when the cookie expires
|
||||
|
||||
"""
|
||||
# TODO(Python 3): Convert other parameters (e.g. path) to bytes, too.
|
||||
if value is None:
|
||||
value = b''
|
||||
max_age = 0
|
||||
expires = timedelta(days=-1)
|
||||
|
||||
name = isomorphic_decode(name)
|
||||
value = isomorphic_decode(value)
|
||||
|
||||
days = {i+1: name for i, name in enumerate(["jan", "feb", "mar",
|
||||
"apr", "may", "jun",
|
||||
"jul", "aug", "sep",
|
||||
"oct", "nov", "dec"])}
|
||||
|
||||
if isinstance(expires, timedelta):
|
||||
expires = datetime.now(timezone.utc) + expires
|
||||
|
||||
if expires is not None:
|
||||
expires_str = expires.strftime("%d %%s %Y %H:%M:%S GMT")
|
||||
expires_str = expires_str % days[expires.month]
|
||||
expires = expires_str
|
||||
|
||||
if max_age is not None:
|
||||
if hasattr(max_age, "total_seconds"):
|
||||
max_age = int(max_age.total_seconds())
|
||||
max_age = "%.0d" % max_age
|
||||
|
||||
m = Morsel()
|
||||
|
||||
def maybe_set(key, value):
|
||||
if value is not None and value is not False:
|
||||
m[key] = value
|
||||
|
||||
m.set(name, value, value)
|
||||
maybe_set("path", path)
|
||||
maybe_set("domain", domain)
|
||||
maybe_set("comment", comment)
|
||||
maybe_set("expires", expires)
|
||||
maybe_set("max-age", max_age)
|
||||
maybe_set("secure", secure)
|
||||
maybe_set("httponly", httponly)
|
||||
maybe_set("samesite", samesite)
|
||||
|
||||
self.headers.append("Set-Cookie", m.OutputString())
|
||||
|
||||
def unset_cookie(self, name):
|
||||
"""Remove a cookie from those that are being sent with the response"""
|
||||
name = isomorphic_decode(name)
|
||||
cookies = self.headers.get("Set-Cookie")
|
||||
parser = BaseCookie()
|
||||
for cookie in cookies:
|
||||
parser.load(isomorphic_decode(cookie))
|
||||
|
||||
if name in parser.keys():
|
||||
del self.headers["Set-Cookie"]
|
||||
for m in parser.values():
|
||||
if m.key != name:
|
||||
self.headers.append(("Set-Cookie", m.OutputString()))
|
||||
|
||||
def delete_cookie(self, name, path="/", domain=None):
|
||||
"""Delete a cookie on the client by setting it to the empty string
|
||||
and to expire in the past"""
|
||||
self.set_cookie(name, None, path=path, domain=domain, max_age=0,
|
||||
expires=timedelta(days=-1))
|
||||
|
||||
def iter_content(self, read_file=False):
|
||||
"""Iterator returning chunks of response body content.
|
||||
|
||||
If any part of the content is a function, this will be called
|
||||
and the resulting value (if any) returned.
|
||||
|
||||
:param read_file: boolean controlling the behaviour when content is a
|
||||
file handle. When set to False the handle will be
|
||||
returned directly allowing the file to be passed to
|
||||
the output in small chunks. When set to True, the
|
||||
entire content of the file will be returned as a
|
||||
string facilitating non-streaming operations like
|
||||
template substitution.
|
||||
"""
|
||||
if isinstance(self.content, bytes):
|
||||
yield self.content
|
||||
elif isinstance(self.content, str):
|
||||
yield self.content.encode(self.encoding)
|
||||
elif hasattr(self.content, "read"):
|
||||
# Read the file in chunks rather than reading the whole file into
|
||||
# memory at once. (See also ResponseWriter.file_chunk_size)
|
||||
while True:
|
||||
read = self.content.read(32 * 1024)
|
||||
if len(read) == 0:
|
||||
break
|
||||
yield read
|
||||
self.content.close()
|
||||
else:
|
||||
for item in self.content:
|
||||
if hasattr(item, "__call__"):
|
||||
value = item()
|
||||
else:
|
||||
value = item
|
||||
if value:
|
||||
yield value
|
||||
|
||||
def write_status_headers(self):
|
||||
"""Write out the status line and headers for the response"""
|
||||
self.writer.write_status(*self.status)
|
||||
for item in self.headers:
|
||||
self.writer.write_header(*item)
|
||||
self.writer.end_headers()
|
||||
|
||||
def write_content(self):
|
||||
"""Write out the response content"""
|
||||
if self.request.method != "HEAD" or self.send_body_for_head_request:
|
||||
for item in self.iter_content():
|
||||
self.writer.write_content(item)
|
||||
|
||||
def write(self):
|
||||
"""Write the whole response"""
|
||||
self.write_status_headers()
|
||||
self.write_content()
|
||||
|
||||
def set_error(self, code, err=None):
|
||||
"""Set the response status headers and return a JSON error object:
|
||||
|
||||
{"error": {"code": code, "message": message}}
|
||||
code is an int (HTTP status code), and message is a text string.
|
||||
"""
|
||||
if 500 <= code < 600:
|
||||
message = self._format_server_error(err)
|
||||
self.logger.warning(message)
|
||||
else:
|
||||
if err is None:
|
||||
message = ""
|
||||
else:
|
||||
message = str(err)
|
||||
|
||||
data = json.dumps({"error": {
|
||||
"code": code,
|
||||
"message": message}
|
||||
})
|
||||
self.status = code
|
||||
self.headers = [("Content-Type", "application/json"),
|
||||
("Content-Length", len(data))]
|
||||
self.content = data
|
||||
|
||||
def _format_server_error(self, err):
|
||||
if err is None:
|
||||
suffix = "<no traceback>"
|
||||
elif isinstance(err, str):
|
||||
suffix = err
|
||||
elif self.request.server.config.logging["suppress_handler_traceback"]:
|
||||
frame = traceback.extract_tb(err.__traceback__)[-1]
|
||||
suffix = (f"""File "{frame.filename}", line {frame.lineno} """
|
||||
f"""in {frame.name} (traceback suppressed)""")
|
||||
else:
|
||||
tb = "\n".join(f" {line}"
|
||||
for line in traceback.format_tb(err.__traceback__))
|
||||
suffix = f"""Traceback (most recent call last):
|
||||
{tb} {type(err).__name__}: {err}
|
||||
"""
|
||||
return f"Internal server error loading {self.request.url}:\n {suffix}"
|
||||
|
||||
|
||||
class MultipartContent:
|
||||
def __init__(self, boundary=None, default_content_type=None):
|
||||
self.items = []
|
||||
if boundary is None:
|
||||
boundary = str(uuid.uuid4())
|
||||
self.boundary = boundary
|
||||
self.default_content_type = default_content_type
|
||||
|
||||
def __call__(self):
|
||||
boundary = b"--" + self.boundary.encode("ascii")
|
||||
rv = [b"", boundary]
|
||||
for item in self.items:
|
||||
rv.append(item.to_bytes())
|
||||
rv.append(boundary)
|
||||
rv[-1] += b"--"
|
||||
return b"\r\n".join(rv)
|
||||
|
||||
def append_part(self, data, content_type=None, headers=None):
|
||||
if content_type is None:
|
||||
content_type = self.default_content_type
|
||||
self.items.append(MultipartPart(data, content_type, headers))
|
||||
|
||||
def __iter__(self):
|
||||
#This is hackish; when writing the response we need an iterable
|
||||
#or a string. For a multipart/byterange response we want an
|
||||
#iterable that contains a single callable; the MultipartContent
|
||||
#object itself
|
||||
yield self
|
||||
|
||||
|
||||
class MultipartPart:
|
||||
def __init__(self, data, content_type=None, headers=None):
|
||||
assert isinstance(data, bytes), data
|
||||
self.headers = ResponseHeaders()
|
||||
|
||||
if content_type is not None:
|
||||
self.headers.set("Content-Type", content_type)
|
||||
|
||||
if headers is not None:
|
||||
for name, value in headers:
|
||||
if name.lower() == b"content-type":
|
||||
func = self.headers.set
|
||||
else:
|
||||
func = self.headers.append
|
||||
func(name, value)
|
||||
|
||||
self.data = data
|
||||
|
||||
def to_bytes(self):
|
||||
rv = []
|
||||
for key, value in self.headers:
|
||||
assert isinstance(key, bytes)
|
||||
assert isinstance(value, bytes)
|
||||
rv.append(b"%s: %s" % (key, value))
|
||||
rv.append(b"")
|
||||
rv.append(self.data)
|
||||
return b"\r\n".join(rv)
|
||||
|
||||
|
||||
def _maybe_encode(s):
|
||||
"""Encode a string or an int into binary data using isomorphic_encode()."""
|
||||
if isinstance(s, int):
|
||||
return b"%i" % (s,)
|
||||
return isomorphic_encode(s)
|
||||
|
||||
|
||||
class ResponseHeaders:
|
||||
"""Dictionary-like object holding the headers for the response"""
|
||||
def __init__(self):
|
||||
self.data = OrderedDict()
|
||||
|
||||
def set(self, key, value):
|
||||
"""Set a header to a specific value, overwriting any previous header
|
||||
with the same name
|
||||
|
||||
:param key: Name of the header to set
|
||||
:param value: Value to set the header to
|
||||
"""
|
||||
key = _maybe_encode(key)
|
||||
value = _maybe_encode(value)
|
||||
self.data[key.lower()] = (key, [value])
|
||||
|
||||
def append(self, key, value):
|
||||
"""Add a new header with a given name, not overwriting any existing
|
||||
headers with the same name
|
||||
|
||||
:param key: Name of the header to add
|
||||
:param value: Value to set for the header
|
||||
"""
|
||||
key = _maybe_encode(key)
|
||||
value = _maybe_encode(value)
|
||||
if key.lower() in self.data:
|
||||
self.data[key.lower()][1].append(value)
|
||||
else:
|
||||
self.set(key, value)
|
||||
|
||||
def get(self, key, default=missing):
|
||||
"""Get the set values for a particular header."""
|
||||
key = _maybe_encode(key)
|
||||
try:
|
||||
return self[key]
|
||||
except KeyError:
|
||||
if default is missing:
|
||||
return []
|
||||
return default
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""Get a list of values for a particular header
|
||||
|
||||
"""
|
||||
key = _maybe_encode(key)
|
||||
return self.data[key.lower()][1]
|
||||
|
||||
def __delitem__(self, key):
|
||||
key = _maybe_encode(key)
|
||||
del self.data[key.lower()]
|
||||
|
||||
def __contains__(self, key):
|
||||
key = _maybe_encode(key)
|
||||
return key.lower() in self.data
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self.set(key, value)
|
||||
|
||||
def __iter__(self):
|
||||
for key, values in self.data.values():
|
||||
for value in values:
|
||||
yield key, value
|
||||
|
||||
def items(self):
|
||||
return list(self)
|
||||
|
||||
def update(self, items_iter):
|
||||
for name, value in items_iter:
|
||||
self.append(name, value)
|
||||
|
||||
def __repr__(self):
|
||||
return repr(self.data)
|
||||
|
||||
|
||||
class H2Response(Response):
|
||||
|
||||
def __init__(self, handler, request):
|
||||
super().__init__(handler, request, response_writer_cls=H2ResponseWriter)
|
||||
|
||||
def write_status_headers(self):
|
||||
self.writer.write_headers(self.headers, *self.status)
|
||||
|
||||
# Hacky way of detecting last item in generator
|
||||
def write_content(self):
|
||||
"""Write out the response content"""
|
||||
if self.request.method != "HEAD" or self.send_body_for_head_request:
|
||||
item = None
|
||||
item_iter = self.iter_content()
|
||||
try:
|
||||
item = next(item_iter)
|
||||
while True:
|
||||
check_last = next(item_iter)
|
||||
self.writer.write_data(item, last=False)
|
||||
item = check_last
|
||||
except StopIteration:
|
||||
if item:
|
||||
self.writer.write_data(item, last=True)
|
||||
|
||||
|
||||
class H2ResponseWriter:
|
||||
|
||||
def __init__(self, handler, response):
|
||||
self.socket = handler.request
|
||||
self.h2conn = handler.conn
|
||||
self._response = response
|
||||
self._handler = handler
|
||||
self.stream_ended = False
|
||||
self.content_written = False
|
||||
self.request = response.request
|
||||
self.logger = response.logger
|
||||
|
||||
def write_headers(self, headers, status_code, status_message=None, stream_id=None, last=False):
|
||||
"""
|
||||
Send a HEADER frame that is tracked by the local state machine.
|
||||
|
||||
Write a HEADER frame using the H2 Connection object, will only work if the stream is in a state to send
|
||||
HEADER frames.
|
||||
|
||||
:param headers: List of (header, value) tuples
|
||||
:param status_code: The HTTP status code of the response
|
||||
:param stream_id: Id of stream to send frame on. Will use the request stream ID if None
|
||||
:param last: Flag to signal if this is the last frame in stream.
|
||||
"""
|
||||
formatted_headers = []
|
||||
secondary_headers = [] # Non ':' prefixed headers are to be added afterwards
|
||||
|
||||
for header, value in headers:
|
||||
# h2_headers are native strings
|
||||
# header field names are strings of ASCII
|
||||
if isinstance(header, bytes):
|
||||
header = header.decode('ascii')
|
||||
# value in headers can be either string or integer
|
||||
if isinstance(value, bytes):
|
||||
value = self.decode(value)
|
||||
if header in h2_headers:
|
||||
header = ':' + header
|
||||
formatted_headers.append((header, str(value)))
|
||||
else:
|
||||
secondary_headers.append((header, str(value)))
|
||||
|
||||
formatted_headers.append((':status', str(status_code)))
|
||||
formatted_headers.extend(secondary_headers)
|
||||
|
||||
with self.h2conn as connection:
|
||||
connection.send_headers(
|
||||
stream_id=self.request.h2_stream_id if stream_id is None else stream_id,
|
||||
headers=formatted_headers,
|
||||
end_stream=last or self.request.method == "HEAD"
|
||||
)
|
||||
|
||||
self.write(connection)
|
||||
|
||||
def write_data(self, item, last=False, stream_id=None):
|
||||
"""
|
||||
Send a DATA frame that is tracked by the local state machine.
|
||||
|
||||
Write a DATA frame using the H2 Connection object, will only work if the stream is in a state to send
|
||||
DATA frames. Uses flow control to split data into multiple data frames if it exceeds the size that can
|
||||
be in a single frame.
|
||||
|
||||
:param item: The content of the DATA frame
|
||||
:param last: Flag to signal if this is the last frame in stream.
|
||||
:param stream_id: Id of stream to send frame on. Will use the request stream ID if None
|
||||
"""
|
||||
if isinstance(item, (str, bytes)):
|
||||
data = BytesIO(self.encode(item))
|
||||
else:
|
||||
data = item
|
||||
|
||||
# Find the length of the data
|
||||
data.seek(0, 2)
|
||||
data_len = data.tell()
|
||||
data.seek(0)
|
||||
|
||||
# If the data is longer than max payload size, need to write it in chunks
|
||||
payload_size = self.get_max_payload_size()
|
||||
while data_len > payload_size:
|
||||
self.write_data_frame(data.read(payload_size), False, stream_id)
|
||||
data_len -= payload_size
|
||||
payload_size = self.get_max_payload_size()
|
||||
|
||||
self.write_data_frame(data.read(), last, stream_id)
|
||||
|
||||
def write_data_frame(self, data, last, stream_id=None):
|
||||
with self.h2conn as connection:
|
||||
connection.send_data(
|
||||
stream_id=self.request.h2_stream_id if stream_id is None else stream_id,
|
||||
data=data,
|
||||
end_stream=last,
|
||||
)
|
||||
self.write(connection)
|
||||
self.stream_ended = last
|
||||
|
||||
def write_push(self, promise_headers, push_stream_id=None, status=None, response_headers=None, response_data=None):
|
||||
"""Write a push promise, and optionally write the push content.
|
||||
|
||||
This will write a push promise to the request stream. If you do not provide headers and data for the response,
|
||||
then no response will be pushed, and you should push them yourself using the ID returned from this function
|
||||
|
||||
:param promise_headers: A list of header tuples that matches what the client would use to
|
||||
request the pushed response
|
||||
:param push_stream_id: The ID of the stream the response should be pushed to. If none given, will
|
||||
use the next available id.
|
||||
:param status: The status code of the response, REQUIRED if response_headers given
|
||||
:param response_headers: The headers of the response
|
||||
:param response_data: The response data.
|
||||
:return: The ID of the push stream
|
||||
"""
|
||||
with self.h2conn as connection:
|
||||
push_stream_id = push_stream_id if push_stream_id is not None else connection.get_next_available_stream_id()
|
||||
connection.push_stream(self.request.h2_stream_id, push_stream_id, promise_headers)
|
||||
self.write(connection)
|
||||
|
||||
has_data = response_data is not None
|
||||
if response_headers is not None:
|
||||
assert status is not None
|
||||
self.write_headers(response_headers, status, stream_id=push_stream_id, last=not has_data)
|
||||
|
||||
if has_data:
|
||||
self.write_data(response_data, last=True, stream_id=push_stream_id)
|
||||
|
||||
return push_stream_id
|
||||
|
||||
def end_stream(self, stream_id=None):
|
||||
"""Ends the stream with the given ID, or the one that request was made on if no ID given."""
|
||||
with self.h2conn as connection:
|
||||
connection.end_stream(stream_id if stream_id is not None else self.request.h2_stream_id)
|
||||
self.write(connection)
|
||||
self.stream_ended = True
|
||||
|
||||
def write_raw_header_frame(self, headers, stream_id=None, end_stream=False, end_headers=False, frame_cls=HeadersFrame):
|
||||
"""
|
||||
Ignores the statemachine of the stream and sends a HEADER frame regardless.
|
||||
|
||||
Unlike `write_headers`, this does not check to see if a stream is in the correct state to have HEADER frames
|
||||
sent through to it. It will build a HEADER frame and send it without using the H2 Connection object other than
|
||||
to HPACK encode the headers.
|
||||
|
||||
:param headers: List of (header, value) tuples
|
||||
:param stream_id: Id of stream to send frame on. Will use the request stream ID if None
|
||||
:param end_stream: Set to True to add END_STREAM flag to frame
|
||||
:param end_headers: Set to True to add END_HEADERS flag to frame
|
||||
"""
|
||||
if not stream_id:
|
||||
stream_id = self.request.h2_stream_id
|
||||
|
||||
header_t = []
|
||||
for header, value in headers:
|
||||
header_t.append(HeaderTuple(header, value))
|
||||
|
||||
with self.h2conn as connection:
|
||||
frame = frame_cls(stream_id, data=connection.encoder.encode(header_t))
|
||||
|
||||
if end_stream:
|
||||
self.stream_ended = True
|
||||
frame.flags.add('END_STREAM')
|
||||
if end_headers:
|
||||
frame.flags.add('END_HEADERS')
|
||||
|
||||
data = frame.serialize()
|
||||
self.write_raw(data)
|
||||
|
||||
def write_raw_data_frame(self, data, stream_id=None, end_stream=False):
|
||||
"""
|
||||
Ignores the statemachine of the stream and sends a DATA frame regardless.
|
||||
|
||||
Unlike `write_data`, this does not check to see if a stream is in the correct state to have DATA frames
|
||||
sent through to it. It will build a DATA frame and send it without using the H2 Connection object. It will
|
||||
not perform any flow control checks.
|
||||
|
||||
:param data: The data to be sent in the frame
|
||||
:param stream_id: Id of stream to send frame on. Will use the request stream ID if None
|
||||
:param end_stream: Set to True to add END_STREAM flag to frame
|
||||
"""
|
||||
if not stream_id:
|
||||
stream_id = self.request.h2_stream_id
|
||||
|
||||
frame = DataFrame(stream_id, data=data)
|
||||
|
||||
if end_stream:
|
||||
self.stream_ended = True
|
||||
frame.flags.add('END_STREAM')
|
||||
|
||||
data = frame.serialize()
|
||||
self.write_raw(data)
|
||||
|
||||
def write_raw_continuation_frame(self, headers, stream_id=None, end_headers=False):
|
||||
"""
|
||||
Ignores the statemachine of the stream and sends a CONTINUATION frame regardless.
|
||||
|
||||
This provides the ability to create and write a CONTINUATION frame to the stream, which is not exposed by
|
||||
`write_headers` as the h2 library handles the split between HEADER and CONTINUATION internally. Will perform
|
||||
HPACK encoding on the headers.
|
||||
|
||||
:param headers: List of (header, value) tuples
|
||||
:param stream_id: Id of stream to send frame on. Will use the request stream ID if None
|
||||
:param end_headers: Set to True to add END_HEADERS flag to frame
|
||||
"""
|
||||
self.write_raw_header_frame(headers, stream_id=stream_id, end_headers=end_headers, frame_cls=ContinuationFrame)
|
||||
|
||||
|
||||
def get_max_payload_size(self, stream_id=None):
|
||||
"""Returns the maximum size of a payload for the given stream."""
|
||||
stream_id = stream_id if stream_id is not None else self.request.h2_stream_id
|
||||
with self.h2conn as connection:
|
||||
return min(connection.remote_settings.max_frame_size, connection.local_flow_control_window(stream_id)) - 9
|
||||
|
||||
def write(self, connection):
|
||||
self.content_written = True
|
||||
data = connection.data_to_send()
|
||||
self.socket.sendall(data)
|
||||
|
||||
def write_raw(self, raw_data):
|
||||
"""Used for sending raw bytes/data through the socket"""
|
||||
|
||||
self.content_written = True
|
||||
self.socket.sendall(raw_data)
|
||||
|
||||
def decode(self, data):
|
||||
"""Convert bytes to unicode according to response.encoding."""
|
||||
if isinstance(data, bytes):
|
||||
return data.decode(self._response.encoding)
|
||||
elif isinstance(data, str):
|
||||
return data
|
||||
else:
|
||||
raise ValueError(type(data))
|
||||
|
||||
def encode(self, data):
|
||||
"""Convert unicode to bytes according to response.encoding."""
|
||||
if isinstance(data, bytes):
|
||||
return data
|
||||
elif isinstance(data, str):
|
||||
return data.encode(self._response.encoding)
|
||||
else:
|
||||
raise ValueError
|
||||
|
||||
|
||||
class ResponseWriter:
|
||||
"""Object providing an API to write out a HTTP response.
|
||||
|
||||
:param handler: The RequestHandler being used.
|
||||
:param response: The Response associated with this writer."""
|
||||
def __init__(self, handler, response):
|
||||
self._wfile = handler.wfile
|
||||
self._response = response
|
||||
self._handler = handler
|
||||
self._status_written = False
|
||||
self._headers_seen = set()
|
||||
self._headers_complete = False
|
||||
self.content_written = False
|
||||
self.request = response.request
|
||||
self.file_chunk_size = 32 * 1024
|
||||
self.default_status = 200
|
||||
|
||||
def _seen_header(self, name):
|
||||
return self.encode(name.lower()) in self._headers_seen
|
||||
|
||||
def write_status(self, code, message=None):
|
||||
"""Write out the status line of a response.
|
||||
|
||||
:param code: The integer status code of the response.
|
||||
:param message: The message of the response. Defaults to the message commonly used
|
||||
with the status code."""
|
||||
if message is None:
|
||||
if code in response_codes:
|
||||
message = response_codes[code][0]
|
||||
else:
|
||||
message = ''
|
||||
self.write(b"%s %d %s\r\n" %
|
||||
(isomorphic_encode(self._response.request.protocol_version), code, isomorphic_encode(message)))
|
||||
self._status_written = True
|
||||
|
||||
def write_header(self, name, value):
|
||||
"""Write out a single header for the response.
|
||||
|
||||
If a status has not been written, a default status will be written (currently 200)
|
||||
|
||||
:param name: Name of the header field
|
||||
:param value: Value of the header field
|
||||
:return: A boolean indicating whether the write succeeds
|
||||
"""
|
||||
if not self._status_written:
|
||||
self.write_status(self.default_status)
|
||||
self._headers_seen.add(self.encode(name.lower()))
|
||||
if not self.write(name):
|
||||
return False
|
||||
if not self.write(b": "):
|
||||
return False
|
||||
if isinstance(value, int):
|
||||
if not self.write(str(value)):
|
||||
return False
|
||||
elif not self.write(value):
|
||||
return False
|
||||
return self.write(b"\r\n")
|
||||
|
||||
def write_default_headers(self):
|
||||
for name, f in [("Server", self._handler.version_string),
|
||||
("Date", self._handler.date_time_string)]:
|
||||
if not self._seen_header(name):
|
||||
if not self.write_header(name, f()):
|
||||
return False
|
||||
|
||||
if (isinstance(self._response.content, (bytes, str)) and
|
||||
not self._seen_header("content-length")):
|
||||
#Would be nice to avoid double-encoding here
|
||||
if not self.write_header("Content-Length", len(self.encode(self._response.content))):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def end_headers(self):
|
||||
"""Finish writing headers and write the separator.
|
||||
|
||||
Unless add_required_headers on the response is False,
|
||||
this will also add HTTP-mandated headers that have not yet been supplied
|
||||
to the response headers.
|
||||
:return: A boolean indicating whether the write succeeds
|
||||
"""
|
||||
|
||||
if self._response.add_required_headers:
|
||||
if not self.write_default_headers():
|
||||
return False
|
||||
|
||||
if not self.write("\r\n"):
|
||||
return False
|
||||
if not self._seen_header("content-length"):
|
||||
self._response.close_connection = True
|
||||
self._headers_complete = True
|
||||
|
||||
return True
|
||||
|
||||
def write_content(self, data):
|
||||
"""Write the body of the response.
|
||||
|
||||
HTTP-mandated headers will be automatically added with status default to 200 if they have
|
||||
not been explicitly set.
|
||||
:return: A boolean indicating whether the write succeeds
|
||||
"""
|
||||
if not self._status_written:
|
||||
self.write_status(self.default_status)
|
||||
if not self._headers_complete:
|
||||
self._response.content = data
|
||||
self.end_headers()
|
||||
return self.write_raw_content(data)
|
||||
|
||||
def write_raw_content(self, data):
|
||||
"""Writes the data 'as is'"""
|
||||
if data is None:
|
||||
raise ValueError('data cannot be None')
|
||||
if isinstance(data, (str, bytes)):
|
||||
# Deliberately allows both text and binary types. See `self.encode`.
|
||||
return self.write(data)
|
||||
else:
|
||||
return self.write_content_file(data)
|
||||
|
||||
def write(self, data):
|
||||
"""Write directly to the response, converting unicode to bytes
|
||||
according to response.encoding.
|
||||
:return: A boolean indicating whether the write succeeds
|
||||
"""
|
||||
self.content_written = True
|
||||
try:
|
||||
self._wfile.write(self.encode(data))
|
||||
return True
|
||||
except OSError:
|
||||
# This can happen if the socket got closed by the remote end
|
||||
return False
|
||||
|
||||
def write_content_file(self, data):
|
||||
"""Write a file-like object directly to the response in chunks."""
|
||||
self.content_written = True
|
||||
success = True
|
||||
while True:
|
||||
buf = data.read(self.file_chunk_size)
|
||||
if not buf:
|
||||
success = False
|
||||
break
|
||||
try:
|
||||
self._wfile.write(buf)
|
||||
except OSError:
|
||||
success = False
|
||||
break
|
||||
data.close()
|
||||
return success
|
||||
|
||||
def encode(self, data):
|
||||
"""Convert unicode to bytes according to response.encoding."""
|
||||
if isinstance(data, bytes):
|
||||
return data
|
||||
elif isinstance(data, str):
|
||||
return data.encode(self._response.encoding)
|
||||
else:
|
||||
raise ValueError("data %r should be text or binary, but is %s" % (data, type(data)))
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
# mypy: allow-untyped-defs
|
||||
|
||||
import itertools
|
||||
import re
|
||||
import sys
|
||||
|
||||
from .logger import get_logger
|
||||
|
||||
any_method = object()
|
||||
|
||||
class RouteTokenizer:
|
||||
def literal(self, scanner, token):
|
||||
return ("literal", token)
|
||||
|
||||
def slash(self, scanner, token):
|
||||
return ("slash", None)
|
||||
|
||||
def group(self, scanner, token):
|
||||
return ("group", token[1:-1])
|
||||
|
||||
def star(self, scanner, token):
|
||||
return ("star", token[1:-3])
|
||||
|
||||
def scan(self, input_str):
|
||||
scanner = re.Scanner([(r"/", self.slash),
|
||||
(r"{\w*}", self.group),
|
||||
(r"\*", self.star),
|
||||
(r"(?:\\.|[^{\*/])*", self.literal),])
|
||||
return scanner.scan(input_str)
|
||||
|
||||
class RouteCompiler:
|
||||
def __init__(self):
|
||||
self.reset()
|
||||
|
||||
def reset(self):
|
||||
self.star_seen = False
|
||||
|
||||
def compile(self, tokens):
|
||||
self.reset()
|
||||
|
||||
func_map = {"slash":self.process_slash,
|
||||
"literal":self.process_literal,
|
||||
"group":self.process_group,
|
||||
"star":self.process_star}
|
||||
|
||||
re_parts = ["^"]
|
||||
|
||||
if not tokens or tokens[0][0] != "slash":
|
||||
tokens = itertools.chain([("slash", None)], tokens)
|
||||
|
||||
for token in tokens:
|
||||
re_parts.append(func_map[token[0]](token))
|
||||
|
||||
if self.star_seen:
|
||||
re_parts.append(")")
|
||||
re_parts.append("$")
|
||||
|
||||
return re.compile("".join(re_parts))
|
||||
|
||||
def process_literal(self, token):
|
||||
return re.escape(token[1])
|
||||
|
||||
def process_slash(self, token):
|
||||
return "/"
|
||||
|
||||
def process_group(self, token):
|
||||
if self.star_seen:
|
||||
raise ValueError("Group seen after star in regexp")
|
||||
return "(?P<%s>[^/]+)" % token[1]
|
||||
|
||||
def process_star(self, token):
|
||||
if self.star_seen:
|
||||
raise ValueError("Star seen after star in regexp")
|
||||
self.star_seen = True
|
||||
return "(.*"
|
||||
|
||||
def compile_path_match(route_pattern):
|
||||
"""tokens: / or literal or match or *"""
|
||||
|
||||
tokenizer = RouteTokenizer()
|
||||
tokens, unmatched = tokenizer.scan(route_pattern)
|
||||
|
||||
assert unmatched == "", unmatched
|
||||
|
||||
compiler = RouteCompiler()
|
||||
|
||||
return compiler.compile(tokens)
|
||||
|
||||
class Router:
|
||||
"""Object for matching handler functions to requests.
|
||||
|
||||
:param doc_root: Absolute path of the filesystem location from
|
||||
which to serve tests
|
||||
:param routes: Initial routes to add; a list of three item tuples
|
||||
(method, path_pattern, handler_function), defined
|
||||
as for register()
|
||||
"""
|
||||
|
||||
def __init__(self, doc_root, routes):
|
||||
self.doc_root = doc_root
|
||||
self.routes = []
|
||||
self.logger = get_logger()
|
||||
|
||||
# Add the doc_root to the Python path, so that any Python handler can
|
||||
# correctly locate helper scripts (see RFC_TO_BE_LINKED).
|
||||
#
|
||||
# TODO: In a perfect world, Router would not need to know about this
|
||||
# and the handler itself would take care of it. Currently, however, we
|
||||
# treat handlers like functions and so there's no easy way to do that.
|
||||
if self.doc_root not in sys.path:
|
||||
sys.path.insert(0, self.doc_root)
|
||||
|
||||
for route in reversed(routes):
|
||||
self.register(*route)
|
||||
|
||||
def register(self, methods, path, handler):
|
||||
r"""Register a handler for a set of paths.
|
||||
|
||||
:param methods: Set of methods this should match. "*" is a
|
||||
special value indicating that all methods should
|
||||
be matched.
|
||||
|
||||
:param path_pattern: Match pattern that will be used to determine if
|
||||
a request path matches this route. Match patterns
|
||||
consist of either literal text, match groups,
|
||||
denoted {name}, which match any character except /,
|
||||
and, at most one \*, which matches and character and
|
||||
creates a match group to the end of the string.
|
||||
If there is no leading "/" on the pattern, this is
|
||||
automatically implied. For example::
|
||||
|
||||
api/{resource}/*.json
|
||||
|
||||
Would match `/api/test/data.json` or
|
||||
`/api/test/test2/data.json`, but not `/api/test/data.py`.
|
||||
|
||||
The match groups are made available in the request object
|
||||
as a dictionary through the route_match property. For
|
||||
example, given the route pattern above and the path
|
||||
`/api/test/data.json`, the route_match property would
|
||||
contain::
|
||||
|
||||
{"resource": "test", "*": "data.json"}
|
||||
|
||||
:param handler: Function that will be called to process matching
|
||||
requests. This must take two parameters, the request
|
||||
object and the response object.
|
||||
|
||||
"""
|
||||
if isinstance(methods, (bytes, str)) or methods is any_method:
|
||||
methods = [methods]
|
||||
for method in methods:
|
||||
self.routes.append((method, compile_path_match(path), handler))
|
||||
self.logger.debug("Route pattern: %s" % self.routes[-1][1].pattern)
|
||||
|
||||
def get_handler(self, request):
|
||||
"""Get a handler for a request or None if there is no handler.
|
||||
|
||||
:param request: Request to get a handler for.
|
||||
:rtype: Callable or None
|
||||
"""
|
||||
for method, regexp, handler in reversed(self.routes):
|
||||
if (request.method == method or
|
||||
method in (any_method, "*") or
|
||||
(request.method == "HEAD" and method == "GET")):
|
||||
m = regexp.match(request.url_parts.path)
|
||||
if m:
|
||||
if not hasattr(handler, "__class__"):
|
||||
name = handler.__name__
|
||||
else:
|
||||
name = handler.__class__.__name__
|
||||
self.logger.debug("Found handler %s" % name)
|
||||
|
||||
match_parts = m.groupdict().copy()
|
||||
if len(match_parts) < len(m.groups()):
|
||||
match_parts["*"] = m.groups()[-1]
|
||||
request.route_match = match_parts
|
||||
|
||||
return handler
|
||||
return None
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
from . import handlers
|
||||
from .router import any_method
|
||||
routes = [(any_method, "*.py", handlers.python_script_handler),
|
||||
("GET", "*.asis", handlers.as_is_handler),
|
||||
("GET", "*", handlers.file_handler),
|
||||
]
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,16 @@
|
|||
# mypy: allow-untyped-defs
|
||||
|
||||
from .base import NoSSLEnvironment
|
||||
from .openssl import OpenSSLEnvironment
|
||||
from .pregenerated import PregeneratedSSLEnvironment
|
||||
|
||||
environments = {"none": NoSSLEnvironment,
|
||||
"openssl": OpenSSLEnvironment,
|
||||
"pregenerated": PregeneratedSSLEnvironment}
|
||||
|
||||
|
||||
def get_cls(name):
|
||||
try:
|
||||
return environments[name]
|
||||
except KeyError:
|
||||
raise ValueError("%s is not a valid SSL type." % name)
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
# mypy: allow-untyped-defs
|
||||
|
||||
class NoSSLEnvironment:
|
||||
ssl_enabled = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def host_cert_path(self, hosts):
|
||||
return None, None
|
||||
|
||||
def ca_cert_path(self, hosts):
|
||||
return None
|
||||
|
|
@ -0,0 +1,422 @@
|
|||
# mypy: allow-untyped-defs
|
||||
|
||||
import functools
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from email.utils import parsedate_to_datetime
|
||||
|
||||
# Amount of time beyond the present to consider certificates "expired." This
|
||||
# allows certificates to be proactively re-generated in the "buffer" period
|
||||
# prior to their exact expiration time.
|
||||
CERT_EXPIRY_BUFFER = dict(hours=6)
|
||||
|
||||
|
||||
class OpenSSL:
|
||||
def __init__(self, logger, binary, base_path, conf_path, hosts, duration,
|
||||
base_conf_path=None):
|
||||
"""Context manager for interacting with OpenSSL.
|
||||
Creates a config file for the duration of the context.
|
||||
|
||||
:param logger: stdlib logger or python structured logger
|
||||
:param binary: path to openssl binary
|
||||
:param base_path: path to directory for storing certificates
|
||||
:param conf_path: path for configuration file storing configuration data
|
||||
:param hosts: list of hosts to include in configuration (or None if not
|
||||
generating host certificates)
|
||||
:param duration: Certificate duration in days"""
|
||||
|
||||
self.base_path = base_path
|
||||
self.binary = binary
|
||||
self.conf_path = conf_path
|
||||
self.base_conf_path = base_conf_path
|
||||
self.logger = logger
|
||||
self.proc = None
|
||||
self.cmd = []
|
||||
self.hosts = hosts
|
||||
self.duration = duration
|
||||
|
||||
def __enter__(self):
|
||||
with open(self.conf_path, "w") as f:
|
||||
f.write(get_config(self.base_path, self.hosts, self.duration))
|
||||
return self
|
||||
|
||||
def __exit__(self, *args, **kwargs):
|
||||
os.unlink(self.conf_path)
|
||||
|
||||
def log(self, line):
|
||||
if hasattr(self.logger, "process_output"):
|
||||
self.logger.process_output(self.proc.pid if self.proc is not None else None,
|
||||
line.decode("utf8", "replace"),
|
||||
command=" ".join(self.cmd))
|
||||
else:
|
||||
self.logger.debug(line)
|
||||
|
||||
def __call__(self, cmd, *args, **kwargs):
|
||||
"""Run a command using OpenSSL in the current context.
|
||||
|
||||
:param cmd: The openssl subcommand to run
|
||||
:param *args: Additional arguments to pass to the command
|
||||
"""
|
||||
self.cmd = [self.binary, cmd]
|
||||
if cmd != "x509":
|
||||
self.cmd += ["-config", self.conf_path]
|
||||
self.cmd += list(args)
|
||||
|
||||
# Copy the environment and add OPENSSL_CONF if available.
|
||||
env = os.environ.copy()
|
||||
if self.base_conf_path is not None:
|
||||
env["OPENSSL_CONF"] = self.base_conf_path
|
||||
|
||||
self.proc = subprocess.Popen(self.cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
env=env)
|
||||
stdout, stderr = self.proc.communicate()
|
||||
self.log(stdout)
|
||||
if self.proc.returncode != 0:
|
||||
raise subprocess.CalledProcessError(self.proc.returncode, self.cmd,
|
||||
output=stdout)
|
||||
|
||||
self.cmd = []
|
||||
self.proc = None
|
||||
return stdout
|
||||
|
||||
|
||||
def make_subject(common_name,
|
||||
country=None,
|
||||
state=None,
|
||||
locality=None,
|
||||
organization=None,
|
||||
organization_unit=None):
|
||||
args = [("country", "C"),
|
||||
("state", "ST"),
|
||||
("locality", "L"),
|
||||
("organization", "O"),
|
||||
("organization_unit", "OU"),
|
||||
("common_name", "CN")]
|
||||
|
||||
rv = []
|
||||
|
||||
for var, key in args:
|
||||
value = locals()[var]
|
||||
if value is not None:
|
||||
rv.append("/%s=%s" % (key, value.replace("/", "\\/")))
|
||||
|
||||
return "".join(rv)
|
||||
|
||||
def make_alt_names(hosts):
|
||||
return ",".join("DNS:%s" % host for host in hosts)
|
||||
|
||||
def make_name_constraints(hosts):
|
||||
return ",".join("permitted;DNS:%s" % host for host in hosts)
|
||||
|
||||
def get_config(root_dir, hosts, duration=30):
|
||||
if hosts is None:
|
||||
san_line = ""
|
||||
constraints_line = ""
|
||||
else:
|
||||
san_line = "subjectAltName = %s" % make_alt_names(hosts)
|
||||
constraints_line = "nameConstraints = " + make_name_constraints(hosts)
|
||||
|
||||
if os.path.sep == "\\":
|
||||
# This seems to be needed for the Shining Light OpenSSL on
|
||||
# Windows, at least.
|
||||
root_dir = root_dir.replace("\\", "\\\\")
|
||||
|
||||
rv = """[ ca ]
|
||||
default_ca = CA_default
|
||||
|
||||
[ CA_default ]
|
||||
dir = %(root_dir)s
|
||||
certs = $dir
|
||||
new_certs_dir = $certs
|
||||
crl_dir = $dir%(sep)scrl
|
||||
database = $dir%(sep)sindex.txt
|
||||
private_key = $dir%(sep)scacert.key
|
||||
certificate = $dir%(sep)scacert.pem
|
||||
serial = $dir%(sep)sserial
|
||||
crldir = $dir%(sep)scrl
|
||||
crlnumber = $dir%(sep)scrlnumber
|
||||
crl = $crldir%(sep)scrl.pem
|
||||
RANDFILE = $dir%(sep)sprivate%(sep)s.rand
|
||||
x509_extensions = usr_cert
|
||||
name_opt = ca_default
|
||||
cert_opt = ca_default
|
||||
default_days = %(duration)d
|
||||
default_crl_days = %(duration)d
|
||||
default_md = sha256
|
||||
preserve = no
|
||||
policy = policy_anything
|
||||
copy_extensions = copy
|
||||
|
||||
[ policy_anything ]
|
||||
countryName = optional
|
||||
stateOrProvinceName = optional
|
||||
localityName = optional
|
||||
organizationName = optional
|
||||
organizationalUnitName = optional
|
||||
commonName = supplied
|
||||
emailAddress = optional
|
||||
|
||||
[ req ]
|
||||
default_bits = 2048
|
||||
default_keyfile = privkey.pem
|
||||
distinguished_name = req_distinguished_name
|
||||
attributes = req_attributes
|
||||
x509_extensions = v3_ca
|
||||
|
||||
# Passwords for private keys if not present they will be prompted for
|
||||
# input_password = secret
|
||||
# output_password = secret
|
||||
string_mask = utf8only
|
||||
req_extensions = v3_req
|
||||
|
||||
[ req_distinguished_name ]
|
||||
countryName = Country Name (2 letter code)
|
||||
countryName_default = AU
|
||||
countryName_min = 2
|
||||
countryName_max = 2
|
||||
stateOrProvinceName = State or Province Name (full name)
|
||||
stateOrProvinceName_default =
|
||||
localityName = Locality Name (eg, city)
|
||||
0.organizationName = Organization Name
|
||||
0.organizationName_default = Web Platform Tests
|
||||
organizationalUnitName = Organizational Unit Name (eg, section)
|
||||
#organizationalUnitName_default =
|
||||
commonName = Common Name (e.g. server FQDN or YOUR name)
|
||||
commonName_max = 64
|
||||
emailAddress = Email Address
|
||||
emailAddress_max = 64
|
||||
|
||||
[ req_attributes ]
|
||||
|
||||
[ usr_cert ]
|
||||
basicConstraints=CA:false
|
||||
subjectKeyIdentifier=hash
|
||||
authorityKeyIdentifier=keyid,issuer
|
||||
|
||||
[ v3_req ]
|
||||
basicConstraints = CA:FALSE
|
||||
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
|
||||
extendedKeyUsage = serverAuth
|
||||
%(san_line)s
|
||||
|
||||
[ v3_ca ]
|
||||
basicConstraints = CA:true
|
||||
subjectKeyIdentifier=hash
|
||||
authorityKeyIdentifier=keyid:always,issuer:always
|
||||
keyUsage = keyCertSign
|
||||
%(constraints_line)s
|
||||
""" % {"root_dir": root_dir,
|
||||
"san_line": san_line,
|
||||
"duration": duration,
|
||||
"constraints_line": constraints_line,
|
||||
"sep": os.path.sep.replace("\\", "\\\\")}
|
||||
|
||||
return rv
|
||||
|
||||
class OpenSSLEnvironment:
|
||||
ssl_enabled = True
|
||||
|
||||
def __init__(self, logger, openssl_binary="openssl", base_path=None,
|
||||
password="web-platform-tests", force_regenerate=False,
|
||||
duration=30, base_conf_path=None):
|
||||
"""SSL environment that creates a local CA and host certificate using OpenSSL.
|
||||
|
||||
By default this will look in base_path for existing certificates that are still
|
||||
valid and only create new certificates if there aren't any. This behaviour can
|
||||
be adjusted using the force_regenerate option.
|
||||
|
||||
:param logger: a stdlib logging compatible logger or mozlog structured logger
|
||||
:param openssl_binary: Path to the OpenSSL binary
|
||||
:param base_path: Path in which certificates will be stored. If None, a temporary
|
||||
directory will be used and removed when the server shuts down
|
||||
:param password: Password to use
|
||||
:param force_regenerate: Always create a new certificate even if one already exists.
|
||||
"""
|
||||
self.logger = logger
|
||||
|
||||
self.temporary = False
|
||||
if base_path is None:
|
||||
base_path = tempfile.mkdtemp()
|
||||
self.temporary = True
|
||||
|
||||
self.base_path = os.path.abspath(base_path)
|
||||
self.password = password
|
||||
self.force_regenerate = force_regenerate
|
||||
self.duration = duration
|
||||
self.base_conf_path = base_conf_path
|
||||
|
||||
self.path = None
|
||||
self.binary = openssl_binary
|
||||
self.openssl = None
|
||||
|
||||
self._ca_cert_path = None
|
||||
self._ca_key_path = None
|
||||
self.host_certificates = {}
|
||||
|
||||
def __enter__(self):
|
||||
if not os.path.exists(self.base_path):
|
||||
os.makedirs(self.base_path)
|
||||
|
||||
path = functools.partial(os.path.join, self.base_path)
|
||||
|
||||
with open(path("index.txt"), "w"):
|
||||
pass
|
||||
with open(path("serial"), "w") as f:
|
||||
serial = "%x" % random.randint(0, 1000000)
|
||||
if len(serial) % 2:
|
||||
serial = "0" + serial
|
||||
f.write(serial)
|
||||
|
||||
self.path = path
|
||||
|
||||
return self
|
||||
|
||||
def __exit__(self, *args, **kwargs):
|
||||
if self.temporary:
|
||||
shutil.rmtree(self.base_path)
|
||||
|
||||
def _config_openssl(self, hosts):
|
||||
conf_path = self.path("openssl.cfg")
|
||||
return OpenSSL(self.logger, self.binary, self.base_path, conf_path, hosts,
|
||||
self.duration, self.base_conf_path)
|
||||
|
||||
def ca_cert_path(self, hosts):
|
||||
"""Get the path to the CA certificate file, generating a
|
||||
new one if needed"""
|
||||
if self._ca_cert_path is None and not self.force_regenerate:
|
||||
self._load_ca_cert()
|
||||
if self._ca_cert_path is None:
|
||||
self._generate_ca(hosts)
|
||||
return self._ca_cert_path
|
||||
|
||||
def _load_ca_cert(self):
|
||||
key_path = self.path("cacert.key")
|
||||
cert_path = self.path("cacert.pem")
|
||||
|
||||
if self.check_key_cert(key_path, cert_path, None):
|
||||
self.logger.info("Using existing CA cert")
|
||||
self._ca_key_path, self._ca_cert_path = key_path, cert_path
|
||||
|
||||
def check_key_cert(self, key_path, cert_path, hosts):
|
||||
"""Check that a key and cert file exist and are valid"""
|
||||
if not os.path.exists(key_path) or not os.path.exists(cert_path):
|
||||
return False
|
||||
|
||||
with self._config_openssl(hosts) as openssl:
|
||||
end_date_str = openssl("x509",
|
||||
"-noout",
|
||||
"-enddate",
|
||||
"-in", cert_path).decode("utf8").split("=", 1)[1].strip()
|
||||
# openssl outputs an RFC 822 date
|
||||
end_date = parsedate_to_datetime(end_date_str)
|
||||
time_buffer = timedelta(**CERT_EXPIRY_BUFFER)
|
||||
if end_date < datetime.now(timezone.utc) + time_buffer:
|
||||
return False
|
||||
|
||||
#TODO: check the key actually signed the cert.
|
||||
return True
|
||||
|
||||
def _generate_ca(self, hosts):
|
||||
path = self.path
|
||||
self.logger.info("Generating new CA in %s" % self.base_path)
|
||||
|
||||
key_path = path("cacert.key")
|
||||
req_path = path("careq.pem")
|
||||
cert_path = path("cacert.pem")
|
||||
|
||||
with self._config_openssl(hosts) as openssl:
|
||||
openssl("req",
|
||||
"-batch",
|
||||
"-new",
|
||||
"-newkey", "rsa:2048",
|
||||
"-keyout", key_path,
|
||||
"-out", req_path,
|
||||
"-subj", make_subject("web-platform-tests"),
|
||||
"-passout", "pass:%s" % self.password)
|
||||
|
||||
openssl("ca",
|
||||
"-batch",
|
||||
"-create_serial",
|
||||
"-keyfile", key_path,
|
||||
"-passin", "pass:%s" % self.password,
|
||||
"-selfsign",
|
||||
"-extensions", "v3_ca",
|
||||
"-notext",
|
||||
"-in", req_path,
|
||||
"-out", cert_path)
|
||||
|
||||
os.unlink(req_path)
|
||||
|
||||
self._ca_key_path, self._ca_cert_path = key_path, cert_path
|
||||
|
||||
def host_cert_path(self, hosts):
|
||||
"""Get a tuple of (private key path, certificate path) for a host,
|
||||
generating new ones if necessary.
|
||||
|
||||
hosts must be a list of all hosts to appear on the certificate, with
|
||||
the primary hostname first."""
|
||||
hosts = tuple(sorted(hosts, key=lambda x:len(x)))
|
||||
if hosts not in self.host_certificates:
|
||||
if not self.force_regenerate:
|
||||
key_cert = self._load_host_cert(hosts)
|
||||
else:
|
||||
key_cert = None
|
||||
if key_cert is None:
|
||||
key, cert = self._generate_host_cert(hosts)
|
||||
else:
|
||||
key, cert = key_cert
|
||||
self.host_certificates[hosts] = key, cert
|
||||
|
||||
return self.host_certificates[hosts]
|
||||
|
||||
def _load_host_cert(self, hosts):
|
||||
host = hosts[0]
|
||||
key_path = self.path("%s.key" % host)
|
||||
cert_path = self.path("%s.pem" % host)
|
||||
|
||||
# TODO: check that this cert was signed by the CA cert
|
||||
if self.check_key_cert(key_path, cert_path, hosts):
|
||||
self.logger.info("Using existing host cert")
|
||||
return key_path, cert_path
|
||||
|
||||
def _generate_host_cert(self, hosts):
|
||||
host = hosts[0]
|
||||
if not self.force_regenerate:
|
||||
self._load_ca_cert()
|
||||
if self._ca_key_path is None:
|
||||
self._generate_ca(hosts)
|
||||
ca_key_path = self._ca_key_path
|
||||
|
||||
assert os.path.exists(ca_key_path)
|
||||
|
||||
path = self.path
|
||||
|
||||
req_path = path("wpt.req")
|
||||
cert_path = path("%s.pem" % host)
|
||||
key_path = path("%s.key" % host)
|
||||
|
||||
self.logger.info("Generating new host cert")
|
||||
|
||||
with self._config_openssl(hosts) as openssl:
|
||||
openssl("req",
|
||||
"-batch",
|
||||
"-newkey", "rsa:2048",
|
||||
"-keyout", key_path,
|
||||
"-in", ca_key_path,
|
||||
"-nodes",
|
||||
"-out", req_path)
|
||||
|
||||
openssl("ca",
|
||||
"-batch",
|
||||
"-in", req_path,
|
||||
"-passin", "pass:%s" % self.password,
|
||||
"-subj", make_subject(host),
|
||||
"-out", cert_path)
|
||||
|
||||
os.unlink(req_path)
|
||||
|
||||
return key_path, cert_path
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
# mypy: allow-untyped-defs
|
||||
|
||||
class PregeneratedSSLEnvironment:
|
||||
"""SSL environment to use with existing key/certificate files
|
||||
e.g. when running on a server with a public domain name
|
||||
"""
|
||||
ssl_enabled = True
|
||||
|
||||
def __init__(self, logger, host_key_path, host_cert_path,
|
||||
ca_cert_path=None):
|
||||
self._ca_cert_path = ca_cert_path
|
||||
self._host_key_path = host_key_path
|
||||
self._host_cert_path = host_cert_path
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def host_cert_path(self, hosts):
|
||||
"""Return the key and certificate paths for the host"""
|
||||
return self._host_key_path, self._host_cert_path
|
||||
|
||||
def ca_cert_path(self, hosts):
|
||||
"""Return the certificate path of the CA that signed the
|
||||
host certificates, or None if that isn't known"""
|
||||
return self._ca_cert_path
|
||||
|
|
@ -0,0 +1,239 @@
|
|||
# mypy: allow-untyped-defs
|
||||
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
import queue
|
||||
import uuid
|
||||
|
||||
from multiprocessing.managers import BaseManager, BaseProxy
|
||||
# We also depend on some undocumented parts of multiprocessing.managers which
|
||||
# don't have any type annotations.
|
||||
from multiprocessing.managers import AcquirerProxy, DictProxy, public_methods # type: ignore
|
||||
from typing import Dict
|
||||
|
||||
from .utils import isomorphic_encode
|
||||
|
||||
|
||||
class StashManager(BaseManager):
|
||||
shared_data: Dict[str, object] = {}
|
||||
lock = threading.Lock()
|
||||
|
||||
|
||||
def _get_shared():
|
||||
return StashManager.shared_data
|
||||
|
||||
|
||||
def _get_lock():
|
||||
return StashManager.lock
|
||||
|
||||
StashManager.register("get_dict",
|
||||
callable=_get_shared,
|
||||
proxytype=DictProxy)
|
||||
StashManager.register('Lock',
|
||||
callable=_get_lock,
|
||||
proxytype=AcquirerProxy)
|
||||
|
||||
|
||||
# We have to create an explicit class here because the built-in
|
||||
# AutoProxy has a bug with nested managers, and the MakeProxy
|
||||
# method doesn't work with spawn-based multiprocessing, since the
|
||||
# generated class can't be pickled for use in child processes.
|
||||
class QueueProxy(BaseProxy):
|
||||
_exposed_ = public_methods(queue.Queue)
|
||||
|
||||
|
||||
for method in QueueProxy._exposed_:
|
||||
|
||||
def impl_fn(method):
|
||||
def _impl(self, *args, **kwargs):
|
||||
return self._callmethod(method, args, kwargs)
|
||||
_impl.__name__ = method
|
||||
return _impl
|
||||
|
||||
setattr(QueueProxy, method, impl_fn(method)) # type: ignore
|
||||
|
||||
|
||||
StashManager.register("Queue",
|
||||
callable=queue.Queue,
|
||||
proxytype=QueueProxy)
|
||||
|
||||
|
||||
class StashServer:
|
||||
def __init__(self, address=None, authkey=None, mp_context=None):
|
||||
self.address = address
|
||||
self.authkey = authkey
|
||||
self.manager = None
|
||||
self.mp_context = mp_context
|
||||
|
||||
def __enter__(self):
|
||||
self.manager, self.address, self.authkey = start_server(self.address,
|
||||
self.authkey,
|
||||
self.mp_context)
|
||||
store_env_config(self.address, self.authkey)
|
||||
|
||||
def __exit__(self, *args, **kwargs):
|
||||
if self.manager is not None:
|
||||
self.manager.shutdown()
|
||||
|
||||
|
||||
def load_env_config():
|
||||
address, authkey = json.loads(os.environ["WPT_STASH_CONFIG"])
|
||||
if isinstance(address, list):
|
||||
address = tuple(address)
|
||||
else:
|
||||
address = str(address)
|
||||
authkey = base64.b64decode(authkey)
|
||||
return address, authkey
|
||||
|
||||
|
||||
def store_env_config(address, authkey):
|
||||
authkey = base64.b64encode(authkey)
|
||||
os.environ["WPT_STASH_CONFIG"] = json.dumps((address, authkey.decode("ascii")))
|
||||
|
||||
|
||||
def start_server(address=None, authkey=None, mp_context=None):
|
||||
if isinstance(authkey, str):
|
||||
authkey = authkey.encode("ascii")
|
||||
kwargs = {}
|
||||
if mp_context is not None:
|
||||
kwargs["ctx"] = mp_context
|
||||
manager = StashManager(address, authkey, **kwargs)
|
||||
manager.start()
|
||||
|
||||
address = manager._address
|
||||
if isinstance(address, bytes):
|
||||
address = address.decode("ascii")
|
||||
return (manager, address, manager._authkey)
|
||||
|
||||
|
||||
class LockWrapper:
|
||||
def __init__(self, lock):
|
||||
self.lock = lock
|
||||
|
||||
def acquire(self):
|
||||
self.lock.acquire()
|
||||
|
||||
def release(self):
|
||||
self.lock.release()
|
||||
|
||||
def __enter__(self):
|
||||
self.acquire()
|
||||
|
||||
def __exit__(self, *args, **kwargs):
|
||||
self.release()
|
||||
|
||||
|
||||
#TODO: Consider expiring values after some fixed time for long-running
|
||||
#servers
|
||||
|
||||
class Stash:
|
||||
"""Key-value store for persisting data across HTTP/S and WS/S requests.
|
||||
|
||||
This data store is specifically designed for persisting data across server
|
||||
requests. The synchronization is achieved by using the BaseManager from
|
||||
the multiprocessing module so different processes can acccess the same data.
|
||||
|
||||
Stash can be used interchangeably between HTTP, HTTPS, WS and WSS servers.
|
||||
A thing to note about WS/S servers is that they require additional steps in
|
||||
the handlers for accessing the same underlying shared data in the Stash.
|
||||
This can usually be achieved by using load_env_config(). When using Stash
|
||||
interchangeably between HTTP/S and WS/S request, the path part of the key
|
||||
should be expliclitly specified if accessing the same key/value subset.
|
||||
|
||||
The store has several unusual properties. Keys are of the form (path,
|
||||
uuid), where path is, by default, the path in the HTTP request and
|
||||
uuid is a unique id. In addition, the store is write-once, read-once,
|
||||
i.e. the value associated with a particular key cannot be changed once
|
||||
written and the read operation (called "take") is destructive. Taken together,
|
||||
these properties make it difficult for data to accidentally leak
|
||||
between different resources or different requests for the same
|
||||
resource.
|
||||
"""
|
||||
|
||||
_proxy = None
|
||||
lock = None
|
||||
manager = None
|
||||
_initializing = threading.Lock()
|
||||
|
||||
def __init__(self, default_path, address=None, authkey=None):
|
||||
self.default_path = default_path
|
||||
self._get_proxy(address, authkey)
|
||||
self.data = Stash._proxy
|
||||
|
||||
def _get_proxy(self, address=None, authkey=None):
|
||||
if address is None and authkey is None:
|
||||
Stash._proxy = {}
|
||||
Stash.lock = threading.Lock()
|
||||
|
||||
# Initializing the proxy involves connecting to the remote process and
|
||||
# retrieving two proxied objects. This process is not inherently
|
||||
# atomic, so a lock must be used to make it so. Atomicity ensures that
|
||||
# only one thread attempts to initialize the connection and that any
|
||||
# threads running in parallel correctly wait for initialization to be
|
||||
# fully complete.
|
||||
with Stash._initializing:
|
||||
if Stash.lock:
|
||||
return
|
||||
|
||||
Stash.manager = StashManager(address, authkey)
|
||||
Stash.manager.connect()
|
||||
Stash._proxy = self.manager.get_dict()
|
||||
Stash.lock = LockWrapper(self.manager.Lock())
|
||||
|
||||
def get_queue(self):
|
||||
return self.manager.Queue()
|
||||
|
||||
def _wrap_key(self, key, path):
|
||||
if path is None:
|
||||
path = self.default_path
|
||||
# This key format is required to support using the path. Since the data
|
||||
# passed into the stash can be a DictProxy which wouldn't detect
|
||||
# changes when writing to a subdict.
|
||||
if isinstance(key, bytes):
|
||||
# UUIDs are within the ASCII charset.
|
||||
key = key.decode('ascii')
|
||||
try:
|
||||
my_uuid = uuid.UUID(key).bytes
|
||||
except ValueError as e:
|
||||
raise ValueError(f"""Invalid UUID "{key}" used as stash key""") from e
|
||||
return (isomorphic_encode(path), my_uuid)
|
||||
|
||||
def put(self, key, value, path=None):
|
||||
"""Place a value in the shared stash.
|
||||
|
||||
:param key: A UUID to use as the data's key.
|
||||
:param value: The data to store. This can be any python object.
|
||||
:param path: The path that has access to read the data (by default
|
||||
the current request path)"""
|
||||
if value is None:
|
||||
raise ValueError("SharedStash value may not be set to None")
|
||||
internal_key = self._wrap_key(key, path)
|
||||
if internal_key in self.data:
|
||||
raise StashError("Tried to overwrite existing shared stash value "
|
||||
"for key %s (old value was %s, new value is %s)" %
|
||||
(internal_key, self.data[internal_key], value))
|
||||
else:
|
||||
self.data[internal_key] = value
|
||||
|
||||
def take(self, key, path=None):
|
||||
"""Remove a value from the shared stash and return it.
|
||||
|
||||
:param key: A UUID to use as the data's key.
|
||||
:param path: The path that has access to read the data (by default
|
||||
the current request path)"""
|
||||
internal_key = self._wrap_key(key, path)
|
||||
value = self.data.get(internal_key, None)
|
||||
if value is not None:
|
||||
try:
|
||||
self.data.pop(internal_key)
|
||||
except KeyError:
|
||||
# Silently continue when pop error occurs.
|
||||
pass
|
||||
|
||||
return value
|
||||
|
||||
|
||||
class StashError(Exception):
|
||||
pass
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
import socket
|
||||
from typing import AnyStr, Dict, List, TypeVar
|
||||
|
||||
from .logger import get_logger
|
||||
|
||||
KT = TypeVar('KT')
|
||||
VT = TypeVar('VT')
|
||||
|
||||
|
||||
def isomorphic_decode(s: AnyStr) -> str:
|
||||
"""Decodes a binary string into a text string using iso-8859-1.
|
||||
|
||||
Returns `str`. The function is a no-op if the argument already has a text
|
||||
type. iso-8859-1 is chosen because it is an 8-bit encoding whose code
|
||||
points range from 0x0 to 0xFF and the values are the same as the binary
|
||||
representations, so any binary string can be decoded into and encoded from
|
||||
iso-8859-1 without any errors or data loss. Python 3 also uses iso-8859-1
|
||||
(or latin-1) extensively in http:
|
||||
https://github.com/python/cpython/blob/273fc220b25933e443c82af6888eb1871d032fb8/Lib/http/client.py#L213
|
||||
"""
|
||||
if isinstance(s, str):
|
||||
return s
|
||||
|
||||
if isinstance(s, bytes):
|
||||
return s.decode("iso-8859-1")
|
||||
|
||||
raise TypeError("Unexpected value (expecting string-like): %r" % s)
|
||||
|
||||
|
||||
def isomorphic_encode(s: AnyStr) -> bytes:
|
||||
"""Encodes a text-type string into binary data using iso-8859-1.
|
||||
|
||||
Returns `bytes`. The function is a no-op if the argument already has a
|
||||
binary type. This is the counterpart of isomorphic_decode.
|
||||
"""
|
||||
if isinstance(s, bytes):
|
||||
return s
|
||||
|
||||
if isinstance(s, str):
|
||||
return s.encode("iso-8859-1")
|
||||
|
||||
raise TypeError("Unexpected value (expecting string-like): %r" % s)
|
||||
|
||||
|
||||
def invert_dict(dict: Dict[KT, List[VT]]) -> Dict[VT, KT]:
|
||||
rv = {}
|
||||
for key, values in dict.items():
|
||||
for value in values:
|
||||
if value in rv:
|
||||
raise ValueError
|
||||
rv[value] = key
|
||||
return rv
|
||||
|
||||
|
||||
class HTTPException(Exception):
|
||||
def __init__(self, code: int, message: str = ""):
|
||||
self.code = code
|
||||
self.message = message
|
||||
|
||||
|
||||
def _open_socket(host: str, port: int) -> socket.socket:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
if port != 0:
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.bind((host, port))
|
||||
sock.listen(5)
|
||||
return sock
|
||||
|
||||
|
||||
def is_bad_port(port: int) -> bool:
|
||||
"""
|
||||
Bad port as per https://fetch.spec.whatwg.org/#port-blocking
|
||||
"""
|
||||
return port in [
|
||||
0,
|
||||
1, # tcpmux
|
||||
7, # echo
|
||||
9, # discard
|
||||
11, # systat
|
||||
13, # daytime
|
||||
15, # netstat
|
||||
17, # qotd
|
||||
19, # chargen
|
||||
20, # ftp-data
|
||||
21, # ftp
|
||||
22, # ssh
|
||||
23, # telnet
|
||||
25, # smtp
|
||||
37, # time
|
||||
42, # name
|
||||
43, # nicname
|
||||
53, # domain
|
||||
69, # tftp
|
||||
77, # priv-rjs
|
||||
79, # finger
|
||||
87, # ttylink
|
||||
95, # supdup
|
||||
101, # hostriame
|
||||
102, # iso-tsap
|
||||
103, # gppitnp
|
||||
104, # acr-nema
|
||||
109, # pop2
|
||||
110, # pop3
|
||||
111, # sunrpc
|
||||
113, # auth
|
||||
115, # sftp
|
||||
117, # uucp-path
|
||||
119, # nntp
|
||||
123, # ntp
|
||||
135, # loc-srv / epmap
|
||||
137, # netbios-ns
|
||||
139, # netbios-ssn
|
||||
143, # imap2
|
||||
161, # snmp
|
||||
179, # bgp
|
||||
389, # ldap
|
||||
427, # afp (alternate)
|
||||
465, # smtp (alternate)
|
||||
512, # print / exec
|
||||
513, # login
|
||||
514, # shell
|
||||
515, # printer
|
||||
526, # tempo
|
||||
530, # courier
|
||||
531, # chat
|
||||
532, # netnews
|
||||
540, # uucp
|
||||
548, # afp
|
||||
554, # rtsp
|
||||
556, # remotefs
|
||||
563, # nntp+ssl
|
||||
587, # smtp (outgoing)
|
||||
601, # syslog-conn
|
||||
636, # ldap+ssl
|
||||
989, # ftps-data
|
||||
999, # ftps
|
||||
993, # ldap+ssl
|
||||
995, # pop3+ssl
|
||||
1719, # h323gatestat
|
||||
1720, # h323hostcall
|
||||
1723, # pptp
|
||||
2049, # nfs
|
||||
3659, # apple-sasl
|
||||
4045, # lockd
|
||||
4190, # sieve
|
||||
5060, # sip
|
||||
5061, # sips
|
||||
6000, # x11
|
||||
6566, # sane-port
|
||||
6665, # irc (alternate)
|
||||
6666, # irc (alternate)
|
||||
6667, # irc (default)
|
||||
6668, # irc (alternate)
|
||||
6669, # irc (alternate)
|
||||
6679, # osaut
|
||||
6697, # irc+tls
|
||||
10080, # amanda
|
||||
]
|
||||
|
||||
|
||||
def get_port(host: str = '') -> int:
|
||||
host = host or '127.0.0.1'
|
||||
port = 0
|
||||
while True:
|
||||
free_socket = _open_socket(host, 0)
|
||||
port = free_socket.getsockname()[1]
|
||||
free_socket.close()
|
||||
if not is_bad_port(port):
|
||||
break
|
||||
return port
|
||||
|
||||
def http2_compatible() -> bool:
|
||||
# The HTTP/2 server requires OpenSSL 1.0.2+.
|
||||
#
|
||||
# For systems using other SSL libraries (e.g. LibreSSL), we assume they
|
||||
# have the necessary support.
|
||||
import ssl
|
||||
if not ssl.OPENSSL_VERSION.startswith("OpenSSL"):
|
||||
logger = get_logger()
|
||||
logger.warning(
|
||||
'Skipping HTTP/2 compatibility check as system is not using '
|
||||
'OpenSSL (found: %s)' % ssl.OPENSSL_VERSION)
|
||||
return True
|
||||
|
||||
# Note that OpenSSL's versioning scheme differs between 1.1.1 and
|
||||
# earlier and 3.0.0. ssl.OPENSSL_VERSION_INFO returns a
|
||||
# (major, minor, 0, patch, 0)
|
||||
# tuple with OpenSSL 3.0.0 and later, and a
|
||||
# (major, minor, fix, patch, status)
|
||||
# tuple for older releases.
|
||||
# Semantically, "patch" in 3.0.0+ is similar to "fix" in previous versions.
|
||||
#
|
||||
# What we do in the check below is allow OpenSSL 3.x.y+, 1.1.x+ and 1.0.2+.
|
||||
ssl_v = ssl.OPENSSL_VERSION_INFO
|
||||
return (ssl_v[0] > 1 or
|
||||
(ssl_v[0] == 1 and
|
||||
(ssl_v[1] == 1 or
|
||||
(ssl_v[1] == 0 and ssl_v[2] >= 2))))
|
||||
|
||||
|
||||
def get_error_cause(exc: BaseException) -> BaseException:
|
||||
"""Get the parent cause/context from an exception"""
|
||||
if exc.__cause__ is not None:
|
||||
return exc.__cause__
|
||||
if exc.__context__ is not None:
|
||||
return exc.__context__
|
||||
return exc
|
||||
33
Tests/LibWeb/Text/input/wpt-import/_wpttools/wptserve/wptserve/wptserve.py
Executable file
33
Tests/LibWeb/Text/input/wpt-import/_wpttools/wptserve/wptserve/wptserve.py
Executable file
|
|
@ -0,0 +1,33 @@
|
|||
#!/usr/bin/env python3
|
||||
# mypy: allow-untyped-defs
|
||||
|
||||
import argparse
|
||||
import os
|
||||
|
||||
from .server import WebTestHttpd
|
||||
|
||||
def abs_path(path):
|
||||
return os.path.abspath(path)
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description="HTTP server designed for extreme flexibility "
|
||||
"required in testing situations.")
|
||||
parser.add_argument("document_root", type=abs_path,
|
||||
help="Root directory to serve files from")
|
||||
parser.add_argument("--port", "-p", type=int, default=8000,
|
||||
help="Port number to run server on")
|
||||
parser.add_argument("--host", "-H", type=str, default="127.0.0.1",
|
||||
help="Host to run server on")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
httpd = WebTestHttpd(host=args.host, port=args.port,
|
||||
use_ssl=False, certificate=None,
|
||||
doc_root=args.document_root)
|
||||
httpd.start()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main() # type: ignore
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
# mypy: allow-untyped-defs
|
||||
|
||||
"""This file provides the opening handshake processor for the Bootstrapping
|
||||
WebSockets with HTTP/2 protocol (RFC 8441).
|
||||
|
||||
Specification:
|
||||
https://tools.ietf.org/html/rfc8441
|
||||
"""
|
||||
|
||||
from pywebsocket3 import common
|
||||
|
||||
from pywebsocket3.handshake.base import get_mandatory_header
|
||||
from pywebsocket3.handshake.base import HandshakeException
|
||||
from pywebsocket3.handshake.base import validate_mandatory_header
|
||||
from pywebsocket3.handshake.base import HandshakerBase
|
||||
|
||||
|
||||
def check_connect_method(request):
|
||||
if request.method != 'CONNECT':
|
||||
raise HandshakeException('Method is not CONNECT: %r' % request.method)
|
||||
|
||||
|
||||
class WsH2Handshaker(HandshakerBase):
|
||||
def __init__(self, request, dispatcher):
|
||||
"""Bootstrapping handshake processor for the WebSocket protocol with HTTP/2 (RFC 8441).
|
||||
|
||||
:param request: mod_python request.
|
||||
|
||||
:param dispatcher: Dispatcher (dispatch.Dispatcher).
|
||||
|
||||
WsH2Handshaker will add attributes such as ws_resource during handshake.
|
||||
"""
|
||||
|
||||
super().__init__(request, dispatcher)
|
||||
|
||||
def _transform_header(self, header):
|
||||
return header.lower()
|
||||
|
||||
def _protocol_rfc(self):
|
||||
return 'RFC 8441'
|
||||
|
||||
def _validate_request(self):
|
||||
check_connect_method(self._request)
|
||||
validate_mandatory_header(self._request, ':protocol', 'websocket')
|
||||
get_mandatory_header(self._request, 'authority')
|
||||
|
||||
def _set_accept(self):
|
||||
# irrelevant for HTTP/2 handshake
|
||||
pass
|
||||
|
||||
def _send_handshake(self):
|
||||
# We are not actually sending the handshake, but just preparing it. It
|
||||
# will be flushed by the caller.
|
||||
self._request.status = 200
|
||||
|
||||
self._request.headers_out['upgrade'] = common.WEBSOCKET_UPGRADE_TYPE
|
||||
self._request.headers_out[
|
||||
'connection'] = common.UPGRADE_CONNECTION_TYPE
|
||||
|
||||
if self._request.ws_protocol is not None:
|
||||
self._request.headers_out[
|
||||
'sec-websocket-protocol'] = self._request.ws_protocol
|
||||
|
||||
if (self._request.ws_extensions is not None and
|
||||
len(self._request.ws_extensions) != 0):
|
||||
self._request.headers_out[
|
||||
'sec-websocket-extensions'] = common.format_extensions(
|
||||
self._request.ws_extensions)
|
||||
|
||||
# Headers not specific for WebSocket
|
||||
for name, value in self._request.extra_headers:
|
||||
self._request.headers_out[name] = value
|
||||
Loading…
Reference in a new issue