| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331 |
- """
- 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 Iterator, MutableMapping
- from typing import Union
- from hyperframe.frame import SettingsFrame
- from .errors import ErrorCodes
- from .exceptions import InvalidSettingsValueError
- class SettingCodes(enum.IntEnum):
- """
- All known HTTP/2 setting codes.
- .. versionadded:: 2.6.0
- """
- #: Allows the sender to inform the remote endpoint of the maximum size of
- #: the header compression table used to decode header blocks, in octets.
- HEADER_TABLE_SIZE = SettingsFrame.HEADER_TABLE_SIZE
- #: This setting can be used to disable server push. To disable server push
- #: on a client, set this to 0.
- ENABLE_PUSH = SettingsFrame.ENABLE_PUSH
- #: Indicates the maximum number of concurrent streams that the sender will
- #: allow.
- MAX_CONCURRENT_STREAMS = SettingsFrame.MAX_CONCURRENT_STREAMS
- #: Indicates the sender's initial window size (in octets) for stream-level
- #: flow control.
- INITIAL_WINDOW_SIZE = SettingsFrame.INITIAL_WINDOW_SIZE
- #: Indicates the size of the largest frame payload that the sender is
- #: willing to receive, in octets.
- MAX_FRAME_SIZE = SettingsFrame.MAX_FRAME_SIZE
- #: This advisory setting informs a peer of the maximum size of header list
- #: that the sender is prepared to accept, in octets. The value is based on
- #: the uncompressed size of header fields, including the length of the name
- #: and value in octets plus an overhead of 32 octets for each header field.
- MAX_HEADER_LIST_SIZE = SettingsFrame.MAX_HEADER_LIST_SIZE
- #: This setting can be used to enable the connect protocol. To enable on a
- #: client set this to 1.
- ENABLE_CONNECT_PROTOCOL = SettingsFrame.ENABLE_CONNECT_PROTOCOL
- def _setting_code_from_int(code: int) -> SettingCodes | int:
- """
- 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: SettingCodes | int, original_value: int | None, new_value: int) -> None:
- #: 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) -> str:
- return (
- f"ChangedSetting(setting={self.setting!s}, original_value={self.original_value}, new_value={self.new_value})"
- )
- class Settings(MutableMapping[Union[SettingCodes, int], int]):
- """
- 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: bool = True, initial_values: dict[SettingCodes, int] | None = None) -> 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: dict[SettingCodes | int, collections.deque[int]] = {
- 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:
- msg = f"Setting {key} has invalid value {value}"
- raise InvalidSettingsValueError(
- msg,
- error_code=invalid,
- )
- self._settings[key] = collections.deque([value])
- def acknowledge(self) -> dict[SettingCodes | int, ChangedSetting]:
- """
- 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: dict[SettingCodes | int, ChangedSetting] = {}
- # 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) -> int:
- """
- 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: int) -> None:
- self[SettingCodes.HEADER_TABLE_SIZE] = value
- @property
- def enable_push(self) -> int:
- """
- 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: int) -> None:
- self[SettingCodes.ENABLE_PUSH] = value
- @property
- def initial_window_size(self) -> int:
- """
- 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: int) -> None:
- self[SettingCodes.INITIAL_WINDOW_SIZE] = value
- @property
- def max_frame_size(self) -> int:
- """
- 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: int) -> None:
- self[SettingCodes.MAX_FRAME_SIZE] = value
- @property
- def max_concurrent_streams(self) -> int:
- """
- 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: int) -> None:
- self[SettingCodes.MAX_CONCURRENT_STREAMS] = value
- @property
- def max_header_list_size(self) -> int | None:
- """
- 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: int) -> None:
- self[SettingCodes.MAX_HEADER_LIST_SIZE] = value
- @property
- def enable_connect_protocol(self) -> int:
- """
- 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: int) -> None:
- self[SettingCodes.ENABLE_CONNECT_PROTOCOL] = value
- # Implement the MutableMapping API.
- def __getitem__(self, key: SettingCodes | int) -> int:
- 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: SettingCodes | int, value: int) -> None:
- invalid = _validate_setting(key, value)
- if invalid:
- msg = f"Setting {key} has invalid value {value}"
- raise InvalidSettingsValueError(
- msg,
- error_code=invalid,
- )
- try:
- items = self._settings[key]
- except KeyError:
- items = collections.deque([None]) # type: ignore
- self._settings[key] = items
- items.append(value)
- def __delitem__(self, key: SettingCodes | int) -> None:
- del self._settings[key]
- def __iter__(self) -> Iterator[SettingCodes | int]:
- return self._settings.__iter__()
- def __len__(self) -> int:
- return len(self._settings)
- def __eq__(self, other: object) -> bool:
- if isinstance(other, Settings):
- return self._settings == other._settings
- return NotImplemented
- def __ne__(self, other: object) -> bool:
- if isinstance(other, Settings):
- return not self == other
- return NotImplemented
- def _validate_setting(setting: SettingCodes | int, value: int) -> ErrorCodes:
- """
- 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 and value not in (0, 1):
- return ErrorCodes.PROTOCOL_ERROR
- return ErrorCodes.NO_ERROR
|