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:
Hyungi Ahn
2026-03-19 13:53:55 +09:00
parent dc08d29509
commit c2257d3a86
2709 changed files with 619549 additions and 10 deletions

View File

@@ -0,0 +1,10 @@
"""
h2
~~
A HTTP/2 implementation.
"""
from __future__ import annotations
__version__ = "5.0.10"

Binary file not shown.

View 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]]: ...

View 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

File diff suppressed because it is too large Load Diff

View 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"]

View 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")

View 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

View 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__()

View 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",
]

View 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

View 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

View 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)

View 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,
]

File diff suppressed because it is too large Load Diff

View 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

View 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()

View File

@@ -0,0 +1,6 @@
"""
hyperframe
~~~~~~~~~~
A module for providing a pure-Python HTTP/2 framing layer.
"""

View File

@@ -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

View 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)

File diff suppressed because it is too large Load Diff

View 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

File diff suppressed because it is too large Load Diff

View 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)

View 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