fix: 포트 충돌 회피 — note_bridge 8098, intent_service 8099
Jellyfin(8096), OrbStack(8097) 포트 충돌으로 변경. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
10
.venv/lib/python3.9/site-packages/jh2/__init__.py
Normal file
10
.venv/lib/python3.9/site-packages/jh2/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
h2
|
||||
~~
|
||||
|
||||
A HTTP/2 implementation.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__version__ = "5.0.10"
|
||||
BIN
.venv/lib/python3.9/site-packages/jh2/_hazmat.abi3.so
Executable file
BIN
.venv/lib/python3.9/site-packages/jh2/_hazmat.abi3.so
Executable file
Binary file not shown.
21
.venv/lib/python3.9/site-packages/jh2/_hazmat.pyi
Normal file
21
.venv/lib/python3.9/site-packages/jh2/_hazmat.pyi
Normal file
@@ -0,0 +1,21 @@
|
||||
from __future__ import annotations
|
||||
|
||||
class Encoder:
|
||||
def __init__(self) -> None: ...
|
||||
@property
|
||||
def header_table_size(self) -> int: ...
|
||||
@header_table_size.setter
|
||||
def header_table_size(self, value) -> None: ...
|
||||
def encode(
|
||||
self, headers: list[tuple[bytes, bytes]], huffman: bool = True
|
||||
) -> bytes: ...
|
||||
|
||||
class Decoder:
|
||||
def __init__(self) -> None: ...
|
||||
@property
|
||||
def header_table_size(self) -> int: ...
|
||||
@header_table_size.setter
|
||||
def header_table_size(self, value: int) -> None: ...
|
||||
@property
|
||||
def max_header_list_size(self) -> int: ...
|
||||
def decode(self, data: bytes, raw: bool) -> list[tuple[bytes, bytes]]: ...
|
||||
203
.venv/lib/python3.9/site-packages/jh2/config.py
Normal file
203
.venv/lib/python3.9/site-packages/jh2/config.py
Normal file
@@ -0,0 +1,203 @@
|
||||
"""
|
||||
h2/config
|
||||
~~~~~~~~~
|
||||
|
||||
Objects for controlling the configuration of the HTTP/2 stack.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
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 split_outbound_cookies: Controls whether the outbound cookie
|
||||
headers are split before sending or not. According to RFC 7540
|
||||
- 8.1.2.5 the outbound header cookie headers may be split to improve
|
||||
headers compression. Default is ``False``.
|
||||
:type split_outbound_cookies: ``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")
|
||||
split_outbound_cookies = _BooleanConfigOption("split_outbound_cookies")
|
||||
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,
|
||||
split_outbound_cookies=False,
|
||||
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.split_outbound_cookies = split_outbound_cookies
|
||||
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
|
||||
2131
.venv/lib/python3.9/site-packages/jh2/connection.py
Normal file
2131
.venv/lib/python3.9/site-packages/jh2/connection.py
Normal file
File diff suppressed because it is too large
Load Diff
78
.venv/lib/python3.9/site-packages/jh2/errors.py
Normal file
78
.venv/lib/python3.9/site-packages/jh2/errors.py
Normal file
@@ -0,0 +1,78 @@
|
||||
"""
|
||||
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
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
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"]
|
||||
651
.venv/lib/python3.9/site-packages/jh2/events.py
Normal file
651
.venv/lib/python3.9/site-packages/jh2/events.py
Normal file
@@ -0,0 +1,651 @@
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
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:{}, headers:{}>".format(
|
||||
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:{}, headers:{}>".format(
|
||||
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:{}, headers:{}>".format(
|
||||
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:{}, headers:{}>".format(
|
||||
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:{}, delta:{}>".format(
|
||||
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:{{{}}}>".format(
|
||||
", ".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:{}>".format(
|
||||
_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:{}>".format(
|
||||
_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:{}, error_code:{}, remote_reset:{}>".format(
|
||||
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:{{{}}}>".format(
|
||||
", ".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:{}, field_value:{}>".format(
|
||||
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")
|
||||
205
.venv/lib/python3.9/site-packages/jh2/exceptions.py
Normal file
205
.venv/lib/python3.9/site-packages/jh2/exceptions.py
Normal file
@@ -0,0 +1,205 @@
|
||||
"""
|
||||
h2/exceptions
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Exceptions for the HTTP/2 module.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .errors import ErrorCodes
|
||||
|
||||
|
||||
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 = 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 = 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 = 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 = 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 = 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().__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 = ErrorCodes.ENHANCE_YOUR_CALM
|
||||
158
.venv/lib/python3.9/site-packages/jh2/frame_buffer.py
Normal file
158
.venv/lib/python3.9/site-packages/jh2/frame_buffer.py
Normal file
@@ -0,0 +1,158 @@
|
||||
"""
|
||||
h2/frame_buffer
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
A data structure that provides a way to iterate over a byte buffer in terms of
|
||||
frames.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .exceptions import FrameDataMissingError, FrameTooLargeError, ProtocolError
|
||||
from .hyperframe.exceptions import InvalidDataError, InvalidFrameError
|
||||
from .hyperframe.frame import ContinuationFrame, Frame, HeadersFrame, PushPromiseFrame
|
||||
|
||||
# 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 = bytearray()
|
||||
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(memoryview(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__()
|
||||
30
.venv/lib/python3.9/site-packages/jh2/hpack/__init__.py
Normal file
30
.venv/lib/python3.9/site-packages/jh2/hpack/__init__.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
hpack
|
||||
~~~~~
|
||||
|
||||
HTTP/2 header encoding for Python.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .exceptions import (
|
||||
HPACKDecodingError,
|
||||
HPACKError,
|
||||
InvalidTableIndex,
|
||||
InvalidTableSizeError,
|
||||
OversizedHeaderListError,
|
||||
)
|
||||
from .hpack import Decoder, Encoder
|
||||
from .struct import HeaderTuple, NeverIndexedHeaderTuple
|
||||
|
||||
__all__ = [
|
||||
"Encoder",
|
||||
"Decoder",
|
||||
"HeaderTuple",
|
||||
"NeverIndexedHeaderTuple",
|
||||
"HPACKError",
|
||||
"HPACKDecodingError",
|
||||
"InvalidTableIndex",
|
||||
"OversizedHeaderListError",
|
||||
"InvalidTableSizeError",
|
||||
]
|
||||
55
.venv/lib/python3.9/site-packages/jh2/hpack/exceptions.py
Normal file
55
.venv/lib/python3.9/site-packages/jh2/hpack/exceptions.py
Normal file
@@ -0,0 +1,55 @@
|
||||
"""
|
||||
hyper/http20/exceptions
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This defines exceptions used in the HTTP/2 portion of hyper.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
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
|
||||
610
.venv/lib/python3.9/site-packages/jh2/hpack/hpack.py
Normal file
610
.venv/lib/python3.9/site-packages/jh2/hpack/hpack.py
Normal file
@@ -0,0 +1,610 @@
|
||||
"""
|
||||
hpack/hpack
|
||||
~~~~~~~~~~~
|
||||
|
||||
Implements the HPACK header compression algorithm as detailed by the IETF.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from .exceptions import (
|
||||
HPACKDecodingError,
|
||||
InvalidTableSizeError,
|
||||
OversizedHeaderListError,
|
||||
)
|
||||
from .huffman import HuffmanEncoder
|
||||
from .huffman_constants import REQUEST_CODES, REQUEST_CODES_LENGTH
|
||||
from .huffman_table import decode_huffman
|
||||
from .struct import HeaderTuple, NeverIndexedHeaderTuple
|
||||
from .table import HeaderTable, table_entry_size
|
||||
|
||||
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)]
|
||||
|
||||
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
.venv/lib/python3.9/site-packages/jh2/hpack/huffman.py
Normal file
66
.venv/lib/python3.9/site-packages/jh2/hpack/huffman.py
Normal file
@@ -0,0 +1,66 @@
|
||||
"""
|
||||
hpack/huffman_decoder
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
An implementation of a bitwise prefix tree specially built for decoding
|
||||
Huffman-coded content where we already know the Huffman table.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
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)
|
||||
530
.venv/lib/python3.9/site-packages/jh2/hpack/huffman_constants.py
Normal file
530
.venv/lib/python3.9/site-packages/jh2/hpack/huffman_constants.py
Normal file
@@ -0,0 +1,530 @@
|
||||
"""
|
||||
hpack/huffman_constants
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Defines the constant Huffman table. This takes up an upsetting amount of space,
|
||||
but c'est la vie.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
# 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,
|
||||
]
|
||||
4486
.venv/lib/python3.9/site-packages/jh2/hpack/huffman_table.py
Normal file
4486
.venv/lib/python3.9/site-packages/jh2/hpack/huffman_table.py
Normal file
File diff suppressed because it is too large
Load Diff
42
.venv/lib/python3.9/site-packages/jh2/hpack/struct.py
Normal file
42
.venv/lib/python3.9/site-packages/jh2/hpack/struct.py
Normal file
@@ -0,0 +1,42 @@
|
||||
"""
|
||||
hpack/struct
|
||||
~~~~~~~~~~~~
|
||||
|
||||
Contains structures for representing header fields with associated metadata.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
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
|
||||
237
.venv/lib/python3.9/site-packages/jh2/hpack/table.py
Normal file
237
.venv/lib/python3.9/site-packages/jh2/hpack/table.py
Normal file
@@ -0,0 +1,237 @@
|
||||
# flake8: noqa
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections import deque
|
||||
|
||||
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,6 @@
|
||||
"""
|
||||
hyperframe
|
||||
~~~~~~~~~~
|
||||
|
||||
A module for providing a pure-Python HTTP/2 framing layer.
|
||||
"""
|
||||
@@ -0,0 +1,72 @@
|
||||
"""
|
||||
hyperframe/exceptions
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Defines the exceptions that can be thrown by hyperframe.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
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
|
||||
54
.venv/lib/python3.9/site-packages/jh2/hyperframe/flags.py
Normal file
54
.venv/lib/python3.9/site-packages/jh2/hyperframe/flags.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""
|
||||
hyperframe/flags
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Defines basic Flag and Flags data structures.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import MutableSet
|
||||
from typing import Iterable, Iterator, NamedTuple
|
||||
|
||||
|
||||
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 = {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)
|
||||
1000
.venv/lib/python3.9/site-packages/jh2/hyperframe/frame.py
Normal file
1000
.venv/lib/python3.9/site-packages/jh2/hyperframe/frame.py
Normal file
File diff suppressed because it is too large
Load Diff
338
.venv/lib/python3.9/site-packages/jh2/settings.py
Normal file
338
.venv/lib/python3.9/site-packages/jh2/settings.py
Normal file
@@ -0,0 +1,338 @@
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import collections
|
||||
import enum
|
||||
from collections.abc import MutableMapping
|
||||
|
||||
from .errors import ErrorCodes
|
||||
from .exceptions import InvalidSettingsValueError
|
||||
from .hyperframe.frame import SettingsFrame
|
||||
|
||||
|
||||
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])
|
||||
self._any_update = True
|
||||
|
||||
@property
|
||||
def has_update(self):
|
||||
try:
|
||||
return self._any_update
|
||||
finally:
|
||||
self._any_update = False
|
||||
|
||||
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)
|
||||
self._any_update = True
|
||||
|
||||
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
|
||||
1474
.venv/lib/python3.9/site-packages/jh2/stream.py
Normal file
1474
.venv/lib/python3.9/site-packages/jh2/stream.py
Normal file
File diff suppressed because it is too large
Load Diff
694
.venv/lib/python3.9/site-packages/jh2/utilities.py
Normal file
694
.venv/lib/python3.9/site-packages/jh2/utilities.py
Normal file
@@ -0,0 +1,694 @@
|
||||
"""
|
||||
h2/utilities
|
||||
~~~~~~~~~~~~
|
||||
|
||||
Utility functions that do not belong in a separate module.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import collections
|
||||
import re
|
||||
from string import whitespace
|
||||
|
||||
from .exceptions import FlowControlError, ProtocolError
|
||||
from .hpack import HeaderTuple, NeverIndexedHeaderTuple
|
||||
|
||||
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",
|
||||
"connection",
|
||||
b"proxy-connection",
|
||||
"proxy-connection",
|
||||
b"keep-alive",
|
||||
"keep-alive",
|
||||
b"transfer-encoding",
|
||||
"transfer-encoding",
|
||||
b"upgrade",
|
||||
"upgrade",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
_ALLOWED_PSEUDO_HEADER_FIELDS = frozenset(
|
||||
[
|
||||
b":method",
|
||||
":method",
|
||||
b":scheme",
|
||||
":scheme",
|
||||
b":authority",
|
||||
":authority",
|
||||
b":path",
|
||||
":path",
|
||||
b":status",
|
||||
":status",
|
||||
b":protocol",
|
||||
":protocol",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
_SECURE_HEADERS = frozenset(
|
||||
[
|
||||
# May have basic credentials which are vulnerable to dictionary attacks.
|
||||
b"authorization",
|
||||
"authorization",
|
||||
b"proxy-authorization",
|
||||
"proxy-authorization",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
_REQUEST_ONLY_HEADERS = frozenset(
|
||||
[
|
||||
b":scheme",
|
||||
":scheme",
|
||||
b":path",
|
||||
":path",
|
||||
b":authority",
|
||||
":authority",
|
||||
b":method",
|
||||
":method",
|
||||
b":protocol",
|
||||
":protocol",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
_RESPONSE_ONLY_HEADERS = frozenset([b":status", ":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", ":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", "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", ":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 = ":"
|
||||
status = ":status"
|
||||
informational_start = "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", ":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", "te"):
|
||||
if header[1].lower() not in (b"trailers", "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":", ":"):
|
||||
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", ":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(":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(":path", b":path", pseudo_headers)
|
||||
_assert_header_in_set(":method", b":method", pseudo_headers)
|
||||
_assert_header_in_set(":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", ":authority"):
|
||||
authority_header_val = header[1]
|
||||
elif header[0] in (b"host", "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", ":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 _split_outbound_cookie_fields(headers, hdr_validation_flags):
|
||||
"""
|
||||
RFC 7540 § 8.1.2.5 allows for better compression efficiency,
|
||||
to split the Cookie header field into separate header fields
|
||||
|
||||
We want to do it for outbound requests, as we are doing for
|
||||
inbound.
|
||||
"""
|
||||
for header in headers:
|
||||
if header[0] in (b"cookie", "cookie"):
|
||||
needle = b"; " if isinstance(header[0], bytes) else "; "
|
||||
|
||||
if needle in header[1]:
|
||||
for cookie_val in header[1].split(needle):
|
||||
if isinstance(header, HeaderTuple):
|
||||
yield header.__class__(header[0], cookie_val)
|
||||
else:
|
||||
yield header[0], cookie_val
|
||||
else:
|
||||
yield header
|
||||
else:
|
||||
yield header
|
||||
|
||||
|
||||
def normalize_outbound_headers(
|
||||
headers, hdr_validation_flags, should_split_outbound_cookies
|
||||
):
|
||||
"""
|
||||
Normalizes a header sequence that we are about to send.
|
||||
|
||||
:param headers: The HTTP header set.
|
||||
:param hdr_validation_flags: An instance of HeaderValidationFlags.
|
||||
:param should_split_outbound_cookies: boolean flag
|
||||
"""
|
||||
headers = _lowercase_header_names(headers, hdr_validation_flags)
|
||||
if should_split_outbound_cookies:
|
||||
headers = _split_outbound_cookie_fields(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().__init__(*args, **kwargs)
|
||||
|
||||
self._check_size_limit()
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
super().__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/jh2/windows.py
Normal file
139
.venv/lib/python3.9/site-packages/jh2/windows.py
Normal file
@@ -0,0 +1,139 @@
|
||||
"""
|
||||
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 annotations
|
||||
|
||||
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