new
This commit is contained in:
8
.venv/lib/python3.9/site-packages/h2/__init__.py
Normal file
8
.venv/lib/python3.9/site-packages/h2/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
hyper-h2
|
||||
~~
|
||||
|
||||
A HTTP/2 implementation.
|
||||
"""
|
||||
__version__ = '4.0.0'
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
170
.venv/lib/python3.9/site-packages/h2/config.py
Normal file
170
.venv/lib/python3.9/site-packages/h2/config.py
Normal file
@@ -0,0 +1,170 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
h2/config
|
||||
~~~~~~~~~
|
||||
|
||||
Objects for controlling the configuration of the HTTP/2 stack.
|
||||
"""
|
||||
|
||||
|
||||
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:
|
||||
"""
|
||||
An 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 hyper-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 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 hyper-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
|
2047
.venv/lib/python3.9/site-packages/h2/connection.py
Normal file
2047
.venv/lib/python3.9/site-packages/h2/connection.py
Normal file
File diff suppressed because it is too large
Load Diff
75
.venv/lib/python3.9/site-packages/h2/errors.py
Normal file
75
.venv/lib/python3.9/site-packages/h2/errors.py
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
.venv/lib/python3.9/site-packages/h2/events.py
Normal file
634
.venv/lib/python3.9/site-packages/h2/events.py
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. hyper-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: hyper-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, Hyper-h2 will terminate the stream early and return this event.
|
||||
|
||||
.. versionchanged:: 2.0.0
|
||||
This event is now fired when Hyper-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 hyper-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. Hyper-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
|
||||
#: hyper-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. Hyper-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 hyper-h2 does not understand. This occurs primarily when the remote
|
||||
peer is employing HTTP/2 extensions that hyper-h2 doesn't know anything
|
||||
about.
|
||||
|
||||
RFC 7540 requires that HTTP/2 implementations ignore these frames. hyper-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
.venv/lib/python3.9/site-packages/h2/exceptions.py
Normal file
187
.venv/lib/python3.9/site-packages/h2/exceptions.py
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
.venv/lib/python3.9/site-packages/h2/frame_buffer.py
Normal file
160
.venv/lib/python3.9/site-packages/h2/frame_buffer.py
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
.venv/lib/python3.9/site-packages/h2/settings.py
Normal file
334
.venv/lib/python3.9/site-packages/h2/settings.py
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
.venv/lib/python3.9/site-packages/h2/stream.py
Normal file
1371
.venv/lib/python3.9/site-packages/h2/stream.py
Normal file
File diff suppressed because it is too large
Load Diff
656
.venv/lib/python3.9/site-packages/h2/utilities.py
Normal file
656
.venv/lib/python3.9/site-packages/h2/utilities.py
Normal file
@@ -0,0 +1,656 @@
|
||||
# -*- 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 upacking in loops because it represents a
|
||||
# fixed cost that we don't want to spend, instead indexing into the header
|
||||
# tuples.
|
||||
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_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 Transfer-Encoding 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
.venv/lib/python3.9/site-packages/h2/windows.py
Normal file
139
.venv/lib/python3.9/site-packages/h2/windows.py
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
|
Reference in New Issue
Block a user