codekingpro/portable-devtools
114k
1"""2h2/settings3~~~~~~~~~~~4 5This module contains a HTTP/2 settings object. This object provides a simple6API for manipulating HTTP/2 settings, keeping track of both the current active7state of the settings and the unacknowledged future values of the settings.8"""9from __future__ import annotations10 11import collections12import enum13from collections.abc import Iterator, MutableMapping14from typing import Union15 16from hyperframe.frame import SettingsFrame17 18from .errors import ErrorCodes19from .exceptions import InvalidSettingsValueError20 21 22class SettingCodes(enum.IntEnum):23 """24 All known HTTP/2 setting codes.25 26 .. versionadded:: 2.6.027 """28 29 #: Allows the sender to inform the remote endpoint of the maximum size of30 #: the header compression table used to decode header blocks, in octets.31 HEADER_TABLE_SIZE = SettingsFrame.HEADER_TABLE_SIZE32 33 #: This setting can be used to disable server push. To disable server push34 #: on a client, set this to 0.35 ENABLE_PUSH = SettingsFrame.ENABLE_PUSH36 37 #: Indicates the maximum number of concurrent streams that the sender will38 #: allow.39 MAX_CONCURRENT_STREAMS = SettingsFrame.MAX_CONCURRENT_STREAMS40 41 #: Indicates the sender's initial window size (in octets) for stream-level42 #: flow control.43 INITIAL_WINDOW_SIZE = SettingsFrame.INITIAL_WINDOW_SIZE44 45 #: Indicates the size of the largest frame payload that the sender is46 #: willing to receive, in octets.47 MAX_FRAME_SIZE = SettingsFrame.MAX_FRAME_SIZE48 49 #: This advisory setting informs a peer of the maximum size of header list50 #: that the sender is prepared to accept, in octets. The value is based on51 #: the uncompressed size of header fields, including the length of the name52 #: and value in octets plus an overhead of 32 octets for each header field.53 MAX_HEADER_LIST_SIZE = SettingsFrame.MAX_HEADER_LIST_SIZE54 55 #: This setting can be used to enable the connect protocol. To enable on a56 #: client set this to 1.57 ENABLE_CONNECT_PROTOCOL = SettingsFrame.ENABLE_CONNECT_PROTOCOL58 59 60def _setting_code_from_int(code: int) -> SettingCodes | int:61 """62 Given an integer setting code, returns either one of :class:`SettingCodes63 <h2.settings.SettingCodes>` or, if not present in the known set of codes,64 returns the integer directly.65 """66 try:67 return SettingCodes(code)68 except ValueError:69 return code70 71 72class ChangedSetting:73 74 def __init__(self, setting: SettingCodes | int, original_value: int | None, new_value: int) -> None:75 #: The setting code given. Either one of :class:`SettingCodes76 #: <h2.settings.SettingCodes>` or ``int``77 #:78 #: .. versionchanged:: 2.6.079 self.setting = setting80 81 #: The original value before being changed.82 self.original_value = original_value83 84 #: The new value after being changed.85 self.new_value = new_value86 87 def __repr__(self) -> str:88 return (89 f"ChangedSetting(setting={self.setting!s}, original_value={self.original_value}, new_value={self.new_value})"90 )91 92 93class Settings(MutableMapping[Union[SettingCodes, int], int]):94 """95 An object that encapsulates HTTP/2 settings state.96 97 HTTP/2 Settings are a complex beast. Each party, remote and local, has its98 own settings and a view of the other party's settings. When a settings99 frame is emitted by a peer it cannot assume that the new settings values100 are in place until the remote peer acknowledges the setting. In principle,101 multiple settings changes can be "in flight" at the same time, all with102 different values.103 104 This object encapsulates this mess. It provides a dict-like interface to105 settings, which return the *current* values of the settings in question.106 Additionally, it keeps track of the stack of proposed values: each time an107 acknowledgement is sent/received, it updates the current values with the108 stack of proposed values. On top of all that, it validates the values to109 make sure they're allowed, and raises :class:`InvalidSettingsValueError110 <h2.exceptions.InvalidSettingsValueError>` if they are not.111 112 Finally, this object understands what the default values of the HTTP/2113 settings are, and sets those defaults appropriately.114 115 .. versionchanged:: 2.2.0116 Added the ``initial_values`` parameter.117 118 .. versionchanged:: 2.5.0119 Added the ``max_header_list_size`` property.120 121 :param client: (optional) Whether these settings should be defaulted for a122 client implementation or a server implementation. Defaults to ``True``.123 :type client: ``bool``124 :param initial_values: (optional) Any initial values the user would like125 set, rather than RFC 7540's defaults.126 :type initial_vales: ``MutableMapping``127 """128 129 def __init__(self, client: bool = True, initial_values: dict[SettingCodes, int] | None = None) -> None:130 # Backing object for the settings. This is a dictionary of131 # (setting: [list of values]), where the first value in the list is the132 # current value of the setting. Strictly this doesn't use lists but133 # instead uses collections.deque to avoid repeated memory allocations.134 #135 # This contains the default values for HTTP/2.136 self._settings: dict[SettingCodes | int, collections.deque[int]] = {137 SettingCodes.HEADER_TABLE_SIZE: collections.deque([4096]),138 SettingCodes.ENABLE_PUSH: collections.deque([int(client)]),139 SettingCodes.INITIAL_WINDOW_SIZE: collections.deque([65535]),140 SettingCodes.MAX_FRAME_SIZE: collections.deque([16384]),141 SettingCodes.ENABLE_CONNECT_PROTOCOL: collections.deque([0]),142 }143 if initial_values is not None:144 for key, value in initial_values.items():145 invalid = _validate_setting(key, value)146 if invalid:147 msg = f"Setting {key} has invalid value {value}"148 raise InvalidSettingsValueError(149 msg,150 error_code=invalid,151 )152 self._settings[key] = collections.deque([value])153 154 def acknowledge(self) -> dict[SettingCodes | int, ChangedSetting]:155 """156 The settings have been acknowledged, either by the user (remote157 settings) or by the remote peer (local settings).158 159 :returns: A dict of {setting: ChangedSetting} that were applied.160 """161 changed_settings: dict[SettingCodes | int, ChangedSetting] = {}162 163 # If there is more than one setting in the list, we have a setting164 # value outstanding. Update them.165 for k, v in self._settings.items():166 if len(v) > 1:167 old_setting = v.popleft()168 new_setting = v[0]169 changed_settings[k] = ChangedSetting(170 k, old_setting, new_setting,171 )172 173 return changed_settings174 175 # Provide easy-access to well known settings.176 @property177 def header_table_size(self) -> int:178 """179 The current value of the :data:`HEADER_TABLE_SIZE180 <h2.settings.SettingCodes.HEADER_TABLE_SIZE>` setting.181 """182 return self[SettingCodes.HEADER_TABLE_SIZE]183 184 @header_table_size.setter185 def header_table_size(self, value: int) -> None:186 self[SettingCodes.HEADER_TABLE_SIZE] = value187 188 @property189 def enable_push(self) -> int:190 """191 The current value of the :data:`ENABLE_PUSH192 <h2.settings.SettingCodes.ENABLE_PUSH>` setting.193 """194 return self[SettingCodes.ENABLE_PUSH]195 196 @enable_push.setter197 def enable_push(self, value: int) -> None:198 self[SettingCodes.ENABLE_PUSH] = value199 200 @property201 def initial_window_size(self) -> int:202 """203 The current value of the :data:`INITIAL_WINDOW_SIZE204 <h2.settings.SettingCodes.INITIAL_WINDOW_SIZE>` setting.205 """206 return self[SettingCodes.INITIAL_WINDOW_SIZE]207 208 @initial_window_size.setter209 def initial_window_size(self, value: int) -> None:210 self[SettingCodes.INITIAL_WINDOW_SIZE] = value211 212 @property213 def max_frame_size(self) -> int:214 """215 The current value of the :data:`MAX_FRAME_SIZE216 <h2.settings.SettingCodes.MAX_FRAME_SIZE>` setting.217 """218 return self[SettingCodes.MAX_FRAME_SIZE]219 220 @max_frame_size.setter221 def max_frame_size(self, value: int) -> None:222 self[SettingCodes.MAX_FRAME_SIZE] = value223 224 @property225 def max_concurrent_streams(self) -> int:226 """227 The current value of the :data:`MAX_CONCURRENT_STREAMS228 <h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS>` setting.229 """230 return self.get(SettingCodes.MAX_CONCURRENT_STREAMS, 2**32+1)231 232 @max_concurrent_streams.setter233 def max_concurrent_streams(self, value: int) -> None:234 self[SettingCodes.MAX_CONCURRENT_STREAMS] = value235 236 @property237 def max_header_list_size(self) -> int | None:238 """239 The current value of the :data:`MAX_HEADER_LIST_SIZE240 <h2.settings.SettingCodes.MAX_HEADER_LIST_SIZE>` setting. If not set,241 returns ``None``, which means unlimited.242 243 .. versionadded:: 2.5.0244 """245 return self.get(SettingCodes.MAX_HEADER_LIST_SIZE, None)246 247 @max_header_list_size.setter248 def max_header_list_size(self, value: int) -> None:249 self[SettingCodes.MAX_HEADER_LIST_SIZE] = value250 251 @property252 def enable_connect_protocol(self) -> int:253 """254 The current value of the :data:`ENABLE_CONNECT_PROTOCOL255 <h2.settings.SettingCodes.ENABLE_CONNECT_PROTOCOL>` setting.256 """257 return self[SettingCodes.ENABLE_CONNECT_PROTOCOL]258 259 @enable_connect_protocol.setter260 def enable_connect_protocol(self, value: int) -> None:261 self[SettingCodes.ENABLE_CONNECT_PROTOCOL] = value262 263 # Implement the MutableMapping API.264 def __getitem__(self, key: SettingCodes | int) -> int:265 val = self._settings[key][0]266 267 # Things that were created when a setting was received should stay268 # KeyError'd.269 if val is None:270 raise KeyError271 272 return val273 274 def __setitem__(self, key: SettingCodes | int, value: int) -> None:275 invalid = _validate_setting(key, value)276 if invalid:277 msg = f"Setting {key} has invalid value {value}"278 raise InvalidSettingsValueError(279 msg,280 error_code=invalid,281 )282 283 try:284 items = self._settings[key]285 except KeyError:286 items = collections.deque([None]) # type: ignore287 self._settings[key] = items288 289 items.append(value)290 291 def __delitem__(self, key: SettingCodes | int) -> None:292 del self._settings[key]293 294 def __iter__(self) -> Iterator[SettingCodes | int]:295 return self._settings.__iter__()296 297 def __len__(self) -> int:298 return len(self._settings)299 300 def __eq__(self, other: object) -> bool:301 if isinstance(other, Settings):302 return self._settings == other._settings303 return NotImplemented304 305 def __ne__(self, other: object) -> bool:306 if isinstance(other, Settings):307 return not self == other308 return NotImplemented309 310 311def _validate_setting(setting: SettingCodes | int, value: int) -> ErrorCodes:312 """313 Confirms that a specific setting has a well-formed value. If the setting is314 invalid, returns an error code. Otherwise, returns 0 (NO_ERROR).315 """316 if setting == SettingCodes.ENABLE_PUSH:317 if value not in (0, 1):318 return ErrorCodes.PROTOCOL_ERROR319 elif setting == SettingCodes.INITIAL_WINDOW_SIZE:320 if not 0 <= value <= 2147483647: # 2^31 - 1321 return ErrorCodes.FLOW_CONTROL_ERROR322 elif setting == SettingCodes.MAX_FRAME_SIZE:323 if not 16384 <= value <= 16777215: # 2^14 and 2^24 - 1324 return ErrorCodes.PROTOCOL_ERROR325 elif setting == SettingCodes.MAX_HEADER_LIST_SIZE:326 if value < 0:327 return ErrorCodes.PROTOCOL_ERROR328 elif setting == SettingCodes.ENABLE_CONNECT_PROTOCOL and value not in (0, 1):329 return ErrorCodes.PROTOCOL_ERROR330 331 return ErrorCodes.NO_ERROR332 