123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376 |
- # -*- coding: utf-8 -*-
- """
- h2/settings
- ~~~~~~~~~~~
- This module contains a HTTP/2 settings object. This object provides a simple
- API for manipulating HTTP/2 settings, keeping track of both the current active
- state of the settings and the unacknowledged future values of the settings.
- """
- import collections
- import enum
- from hyperframe.frame import SettingsFrame
- from h2.errors import ErrorCodes
- from h2.exceptions import InvalidSettingsValueError
- class SettingCodes(enum.IntEnum):
- """
- All known HTTP/2 setting codes.
- .. versionadded:: 2.6.0
- """
- #: Allows the sender to inform the remote endpoint of the maximum size of
- #: the header compression table used to decode header blocks, in octets.
- HEADER_TABLE_SIZE = SettingsFrame.HEADER_TABLE_SIZE
- #: This setting can be used to disable server push. To disable server push
- #: on a client, set this to 0.
- ENABLE_PUSH = SettingsFrame.ENABLE_PUSH
- #: Indicates the maximum number of concurrent streams that the sender will
- #: allow.
- MAX_CONCURRENT_STREAMS = SettingsFrame.MAX_CONCURRENT_STREAMS
- #: Indicates the sender's initial window size (in octets) for stream-level
- #: flow control.
- INITIAL_WINDOW_SIZE = SettingsFrame.INITIAL_WINDOW_SIZE
- try: # Platform-specific: Hyperframe < 4.0.0
- _max_frame_size = SettingsFrame.SETTINGS_MAX_FRAME_SIZE
- except AttributeError: # Platform-specific: Hyperframe >= 4.0.0
- _max_frame_size = SettingsFrame.MAX_FRAME_SIZE
- #: Indicates the size of the largest frame payload that the sender is
- #: willing to receive, in octets.
- MAX_FRAME_SIZE = _max_frame_size
- try: # Platform-specific: Hyperframe < 4.0.0
- _max_header_list_size = SettingsFrame.SETTINGS_MAX_HEADER_LIST_SIZE
- except AttributeError: # Platform-specific: Hyperframe >= 4.0.0
- _max_header_list_size = SettingsFrame.MAX_HEADER_LIST_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 = _max_header_list_size
- 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
- # Aliases for all the settings values.
- #: Allows the sender to inform the remote endpoint of the maximum size of the
- #: header compression table used to decode header blocks, in octets.
- #:
- #: .. deprecated:: 2.6.0
- #: Deprecated in favour of :data:`SettingCodes.HEADER_TABLE_SIZE
- #: <h2.settings.SettingCodes.HEADER_TABLE_SIZE>`.
- HEADER_TABLE_SIZE = SettingCodes.HEADER_TABLE_SIZE
- #: This setting can be used to disable server push. To disable server push on
- #: a client, set this to 0.
- #:
- #: .. deprecated:: 2.6.0
- #: Deprecated in favour of :data:`SettingCodes.ENABLE_PUSH
- #: <h2.settings.SettingCodes.ENABLE_PUSH>`.
- ENABLE_PUSH = SettingCodes.ENABLE_PUSH
- #: Indicates the maximum number of concurrent streams that the sender will
- #: allow.
- #:
- #: .. deprecated:: 2.6.0
- #: Deprecated in favour of :data:`SettingCodes.MAX_CONCURRENT_STREAMS
- #: <h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS>`.
- MAX_CONCURRENT_STREAMS = SettingCodes.MAX_CONCURRENT_STREAMS
- #: Indicates the sender's initial window size (in octets) for stream-level flow
- #: control.
- #:
- #: .. deprecated:: 2.6.0
- #: Deprecated in favour of :data:`SettingCodes.INITIAL_WINDOW_SIZE
- #: <h2.settings.SettingCodes.INITIAL_WINDOW_SIZE>`.
- INITIAL_WINDOW_SIZE = SettingCodes.INITIAL_WINDOW_SIZE
- #: Indicates the size of the largest frame payload that the sender is willing
- #: to receive, in octets.
- #:
- #: .. deprecated:: 2.6.0
- #: Deprecated in favour of :data:`SettingCodes.MAX_FRAME_SIZE
- #: <h2.settings.SettingCodes.MAX_FRAME_SIZE>`.
- MAX_FRAME_SIZE = SettingCodes.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.
- #:
- #: .. deprecated:: 2.6.0
- #: Deprecated in favour of :data:`SettingCodes.MAX_HEADER_LIST_SIZE
- #: <h2.settings.SettingCodes.MAX_HEADER_LIST_SIZE>`.
- MAX_HEADER_LIST_SIZE = SettingCodes.MAX_HEADER_LIST_SIZE
- 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(collections.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]),
- }
- if initial_values is not None:
- for key, value in initial_values.items():
- invalid = _validate_setting(key, value)
- if invalid:
- raise InvalidSettingsValueError(
- "Setting %d has invalid value %d" % (key, value),
- error_code=invalid
- )
- self._settings[key] = collections.deque([value])
- def acknowledge(self):
- """
- The settings have been acknowledged, either by the user (remote
- settings) or by the remote peer (local settings).
- :returns: A dict of {setting: ChangedSetting} that were applied.
- """
- changed_settings = {}
- # If there is more than one setting in the list, we have a setting
- # value outstanding. Update them.
- for k, v in self._settings.items():
- if len(v) > 1:
- old_setting = v.popleft()
- new_setting = v[0]
- changed_settings[k] = ChangedSetting(
- k, old_setting, new_setting
- )
- return changed_settings
- # Provide easy-access to well known settings.
- @property
- def header_table_size(self):
- """
- The current value of the :data:`HEADER_TABLE_SIZE
- <h2.settings.SettingCodes.HEADER_TABLE_SIZE>` setting.
- """
- return self[SettingCodes.HEADER_TABLE_SIZE]
- @header_table_size.setter
- def header_table_size(self, value):
- self[SettingCodes.HEADER_TABLE_SIZE] = value
- @property
- def enable_push(self):
- """
- The current value of the :data:`ENABLE_PUSH
- <h2.settings.SettingCodes.ENABLE_PUSH>` setting.
- """
- return self[SettingCodes.ENABLE_PUSH]
- @enable_push.setter
- def enable_push(self, value):
- self[SettingCodes.ENABLE_PUSH] = value
- @property
- def initial_window_size(self):
- """
- The current value of the :data:`INITIAL_WINDOW_SIZE
- <h2.settings.SettingCodes.INITIAL_WINDOW_SIZE>` setting.
- """
- return self[SettingCodes.INITIAL_WINDOW_SIZE]
- @initial_window_size.setter
- def initial_window_size(self, value):
- self[SettingCodes.INITIAL_WINDOW_SIZE] = value
- @property
- def max_frame_size(self):
- """
- The current value of the :data:`MAX_FRAME_SIZE
- <h2.settings.SettingCodes.MAX_FRAME_SIZE>` setting.
- """
- return self[SettingCodes.MAX_FRAME_SIZE]
- @max_frame_size.setter
- def max_frame_size(self, value):
- self[SettingCodes.MAX_FRAME_SIZE] = value
- @property
- def max_concurrent_streams(self):
- """
- The current value of the :data:`MAX_CONCURRENT_STREAMS
- <h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS>` setting.
- """
- return self.get(SettingCodes.MAX_CONCURRENT_STREAMS, 2**32+1)
- @max_concurrent_streams.setter
- def max_concurrent_streams(self, value):
- self[SettingCodes.MAX_CONCURRENT_STREAMS] = value
- @property
- def max_header_list_size(self):
- """
- The current value of the :data:`MAX_HEADER_LIST_SIZE
- <h2.settings.SettingCodes.MAX_HEADER_LIST_SIZE>` setting. If not set,
- returns ``None``, which means unlimited.
- .. versionadded:: 2.5.0
- """
- return self.get(SettingCodes.MAX_HEADER_LIST_SIZE, None)
- @max_header_list_size.setter
- def max_header_list_size(self, value):
- self[SettingCodes.MAX_HEADER_LIST_SIZE] = value
- # Implement the MutableMapping API.
- def __getitem__(self, key):
- val = self._settings[key][0]
- # Things that were created when a setting was received should stay
- # KeyError'd.
- if val is None:
- raise KeyError
- return val
- def __setitem__(self, key, value):
- invalid = _validate_setting(key, value)
- if invalid:
- raise InvalidSettingsValueError(
- "Setting %d has invalid value %d" % (key, value),
- error_code=invalid
- )
- try:
- items = self._settings[key]
- except KeyError:
- items = collections.deque([None])
- self._settings[key] = items
- items.append(value)
- def __delitem__(self, key):
- del self._settings[key]
- def __iter__(self):
- return self._settings.__iter__()
- def __len__(self):
- return len(self._settings)
- def __eq__(self, other):
- if isinstance(other, Settings):
- return self._settings == other._settings
- else:
- return NotImplemented
- def __ne__(self, other):
- if isinstance(other, Settings):
- return not self == other
- else:
- return NotImplemented
- def _validate_setting(setting, value):
- """
- 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
- return 0
|