settings.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. """
  2. h2/settings
  3. ~~~~~~~~~~~
  4. This module contains a HTTP/2 settings object. This object provides a simple
  5. API for manipulating HTTP/2 settings, keeping track of both the current active
  6. state of the settings and the unacknowledged future values of the settings.
  7. """
  8. from __future__ import annotations
  9. import collections
  10. import enum
  11. from collections.abc import Iterator, MutableMapping
  12. from typing import Union
  13. from hyperframe.frame import SettingsFrame
  14. from .errors import ErrorCodes
  15. from .exceptions import InvalidSettingsValueError
  16. class SettingCodes(enum.IntEnum):
  17. """
  18. All known HTTP/2 setting codes.
  19. .. versionadded:: 2.6.0
  20. """
  21. #: Allows the sender to inform the remote endpoint of the maximum size of
  22. #: the header compression table used to decode header blocks, in octets.
  23. HEADER_TABLE_SIZE = SettingsFrame.HEADER_TABLE_SIZE
  24. #: This setting can be used to disable server push. To disable server push
  25. #: on a client, set this to 0.
  26. ENABLE_PUSH = SettingsFrame.ENABLE_PUSH
  27. #: Indicates the maximum number of concurrent streams that the sender will
  28. #: allow.
  29. MAX_CONCURRENT_STREAMS = SettingsFrame.MAX_CONCURRENT_STREAMS
  30. #: Indicates the sender's initial window size (in octets) for stream-level
  31. #: flow control.
  32. INITIAL_WINDOW_SIZE = SettingsFrame.INITIAL_WINDOW_SIZE
  33. #: Indicates the size of the largest frame payload that the sender is
  34. #: willing to receive, in octets.
  35. MAX_FRAME_SIZE = SettingsFrame.MAX_FRAME_SIZE
  36. #: This advisory setting informs a peer of the maximum size of header list
  37. #: that the sender is prepared to accept, in octets. The value is based on
  38. #: the uncompressed size of header fields, including the length of the name
  39. #: and value in octets plus an overhead of 32 octets for each header field.
  40. MAX_HEADER_LIST_SIZE = SettingsFrame.MAX_HEADER_LIST_SIZE
  41. #: This setting can be used to enable the connect protocol. To enable on a
  42. #: client set this to 1.
  43. ENABLE_CONNECT_PROTOCOL = SettingsFrame.ENABLE_CONNECT_PROTOCOL
  44. def _setting_code_from_int(code: int) -> SettingCodes | int:
  45. """
  46. Given an integer setting code, returns either one of :class:`SettingCodes
  47. <h2.settings.SettingCodes>` or, if not present in the known set of codes,
  48. returns the integer directly.
  49. """
  50. try:
  51. return SettingCodes(code)
  52. except ValueError:
  53. return code
  54. class ChangedSetting:
  55. def __init__(self, setting: SettingCodes | int, original_value: int | None, new_value: int) -> None:
  56. #: The setting code given. Either one of :class:`SettingCodes
  57. #: <h2.settings.SettingCodes>` or ``int``
  58. #:
  59. #: .. versionchanged:: 2.6.0
  60. self.setting = setting
  61. #: The original value before being changed.
  62. self.original_value = original_value
  63. #: The new value after being changed.
  64. self.new_value = new_value
  65. def __repr__(self) -> str:
  66. return (
  67. f"ChangedSetting(setting={self.setting!s}, original_value={self.original_value}, new_value={self.new_value})"
  68. )
  69. class Settings(MutableMapping[Union[SettingCodes, int], int]):
  70. """
  71. An object that encapsulates HTTP/2 settings state.
  72. HTTP/2 Settings are a complex beast. Each party, remote and local, has its
  73. own settings and a view of the other party's settings. When a settings
  74. frame is emitted by a peer it cannot assume that the new settings values
  75. are in place until the remote peer acknowledges the setting. In principle,
  76. multiple settings changes can be "in flight" at the same time, all with
  77. different values.
  78. This object encapsulates this mess. It provides a dict-like interface to
  79. settings, which return the *current* values of the settings in question.
  80. Additionally, it keeps track of the stack of proposed values: each time an
  81. acknowledgement is sent/received, it updates the current values with the
  82. stack of proposed values. On top of all that, it validates the values to
  83. make sure they're allowed, and raises :class:`InvalidSettingsValueError
  84. <h2.exceptions.InvalidSettingsValueError>` if they are not.
  85. Finally, this object understands what the default values of the HTTP/2
  86. settings are, and sets those defaults appropriately.
  87. .. versionchanged:: 2.2.0
  88. Added the ``initial_values`` parameter.
  89. .. versionchanged:: 2.5.0
  90. Added the ``max_header_list_size`` property.
  91. :param client: (optional) Whether these settings should be defaulted for a
  92. client implementation or a server implementation. Defaults to ``True``.
  93. :type client: ``bool``
  94. :param initial_values: (optional) Any initial values the user would like
  95. set, rather than RFC 7540's defaults.
  96. :type initial_vales: ``MutableMapping``
  97. """
  98. def __init__(self, client: bool = True, initial_values: dict[SettingCodes, int] | None = None) -> None:
  99. # Backing object for the settings. This is a dictionary of
  100. # (setting: [list of values]), where the first value in the list is the
  101. # current value of the setting. Strictly this doesn't use lists but
  102. # instead uses collections.deque to avoid repeated memory allocations.
  103. #
  104. # This contains the default values for HTTP/2.
  105. self._settings: dict[SettingCodes | int, collections.deque[int]] = {
  106. SettingCodes.HEADER_TABLE_SIZE: collections.deque([4096]),
  107. SettingCodes.ENABLE_PUSH: collections.deque([int(client)]),
  108. SettingCodes.INITIAL_WINDOW_SIZE: collections.deque([65535]),
  109. SettingCodes.MAX_FRAME_SIZE: collections.deque([16384]),
  110. SettingCodes.ENABLE_CONNECT_PROTOCOL: collections.deque([0]),
  111. }
  112. if initial_values is not None:
  113. for key, value in initial_values.items():
  114. invalid = _validate_setting(key, value)
  115. if invalid:
  116. msg = f"Setting {key} has invalid value {value}"
  117. raise InvalidSettingsValueError(
  118. msg,
  119. error_code=invalid,
  120. )
  121. self._settings[key] = collections.deque([value])
  122. def acknowledge(self) -> dict[SettingCodes | int, ChangedSetting]:
  123. """
  124. The settings have been acknowledged, either by the user (remote
  125. settings) or by the remote peer (local settings).
  126. :returns: A dict of {setting: ChangedSetting} that were applied.
  127. """
  128. changed_settings: dict[SettingCodes | int, ChangedSetting] = {}
  129. # If there is more than one setting in the list, we have a setting
  130. # value outstanding. Update them.
  131. for k, v in self._settings.items():
  132. if len(v) > 1:
  133. old_setting = v.popleft()
  134. new_setting = v[0]
  135. changed_settings[k] = ChangedSetting(
  136. k, old_setting, new_setting,
  137. )
  138. return changed_settings
  139. # Provide easy-access to well known settings.
  140. @property
  141. def header_table_size(self) -> int:
  142. """
  143. The current value of the :data:`HEADER_TABLE_SIZE
  144. <h2.settings.SettingCodes.HEADER_TABLE_SIZE>` setting.
  145. """
  146. return self[SettingCodes.HEADER_TABLE_SIZE]
  147. @header_table_size.setter
  148. def header_table_size(self, value: int) -> None:
  149. self[SettingCodes.HEADER_TABLE_SIZE] = value
  150. @property
  151. def enable_push(self) -> int:
  152. """
  153. The current value of the :data:`ENABLE_PUSH
  154. <h2.settings.SettingCodes.ENABLE_PUSH>` setting.
  155. """
  156. return self[SettingCodes.ENABLE_PUSH]
  157. @enable_push.setter
  158. def enable_push(self, value: int) -> None:
  159. self[SettingCodes.ENABLE_PUSH] = value
  160. @property
  161. def initial_window_size(self) -> int:
  162. """
  163. The current value of the :data:`INITIAL_WINDOW_SIZE
  164. <h2.settings.SettingCodes.INITIAL_WINDOW_SIZE>` setting.
  165. """
  166. return self[SettingCodes.INITIAL_WINDOW_SIZE]
  167. @initial_window_size.setter
  168. def initial_window_size(self, value: int) -> None:
  169. self[SettingCodes.INITIAL_WINDOW_SIZE] = value
  170. @property
  171. def max_frame_size(self) -> int:
  172. """
  173. The current value of the :data:`MAX_FRAME_SIZE
  174. <h2.settings.SettingCodes.MAX_FRAME_SIZE>` setting.
  175. """
  176. return self[SettingCodes.MAX_FRAME_SIZE]
  177. @max_frame_size.setter
  178. def max_frame_size(self, value: int) -> None:
  179. self[SettingCodes.MAX_FRAME_SIZE] = value
  180. @property
  181. def max_concurrent_streams(self) -> int:
  182. """
  183. The current value of the :data:`MAX_CONCURRENT_STREAMS
  184. <h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS>` setting.
  185. """
  186. return self.get(SettingCodes.MAX_CONCURRENT_STREAMS, 2**32+1)
  187. @max_concurrent_streams.setter
  188. def max_concurrent_streams(self, value: int) -> None:
  189. self[SettingCodes.MAX_CONCURRENT_STREAMS] = value
  190. @property
  191. def max_header_list_size(self) -> int | None:
  192. """
  193. The current value of the :data:`MAX_HEADER_LIST_SIZE
  194. <h2.settings.SettingCodes.MAX_HEADER_LIST_SIZE>` setting. If not set,
  195. returns ``None``, which means unlimited.
  196. .. versionadded:: 2.5.0
  197. """
  198. return self.get(SettingCodes.MAX_HEADER_LIST_SIZE, None)
  199. @max_header_list_size.setter
  200. def max_header_list_size(self, value: int) -> None:
  201. self[SettingCodes.MAX_HEADER_LIST_SIZE] = value
  202. @property
  203. def enable_connect_protocol(self) -> int:
  204. """
  205. The current value of the :data:`ENABLE_CONNECT_PROTOCOL
  206. <h2.settings.SettingCodes.ENABLE_CONNECT_PROTOCOL>` setting.
  207. """
  208. return self[SettingCodes.ENABLE_CONNECT_PROTOCOL]
  209. @enable_connect_protocol.setter
  210. def enable_connect_protocol(self, value: int) -> None:
  211. self[SettingCodes.ENABLE_CONNECT_PROTOCOL] = value
  212. # Implement the MutableMapping API.
  213. def __getitem__(self, key: SettingCodes | int) -> int:
  214. val = self._settings[key][0]
  215. # Things that were created when a setting was received should stay
  216. # KeyError'd.
  217. if val is None:
  218. raise KeyError
  219. return val
  220. def __setitem__(self, key: SettingCodes | int, value: int) -> None:
  221. invalid = _validate_setting(key, value)
  222. if invalid:
  223. msg = f"Setting {key} has invalid value {value}"
  224. raise InvalidSettingsValueError(
  225. msg,
  226. error_code=invalid,
  227. )
  228. try:
  229. items = self._settings[key]
  230. except KeyError:
  231. items = collections.deque([None]) # type: ignore
  232. self._settings[key] = items
  233. items.append(value)
  234. def __delitem__(self, key: SettingCodes | int) -> None:
  235. del self._settings[key]
  236. def __iter__(self) -> Iterator[SettingCodes | int]:
  237. return self._settings.__iter__()
  238. def __len__(self) -> int:
  239. return len(self._settings)
  240. def __eq__(self, other: object) -> bool:
  241. if isinstance(other, Settings):
  242. return self._settings == other._settings
  243. return NotImplemented
  244. def __ne__(self, other: object) -> bool:
  245. if isinstance(other, Settings):
  246. return not self == other
  247. return NotImplemented
  248. def _validate_setting(setting: SettingCodes | int, value: int) -> ErrorCodes:
  249. """
  250. Confirms that a specific setting has a well-formed value. If the setting is
  251. invalid, returns an error code. Otherwise, returns 0 (NO_ERROR).
  252. """
  253. if setting == SettingCodes.ENABLE_PUSH:
  254. if value not in (0, 1):
  255. return ErrorCodes.PROTOCOL_ERROR
  256. elif setting == SettingCodes.INITIAL_WINDOW_SIZE:
  257. if not 0 <= value <= 2147483647: # 2^31 - 1
  258. return ErrorCodes.FLOW_CONTROL_ERROR
  259. elif setting == SettingCodes.MAX_FRAME_SIZE:
  260. if not 16384 <= value <= 16777215: # 2^14 and 2^24 - 1
  261. return ErrorCodes.PROTOCOL_ERROR
  262. elif setting == SettingCodes.MAX_HEADER_LIST_SIZE:
  263. if value < 0:
  264. return ErrorCodes.PROTOCOL_ERROR
  265. elif setting == SettingCodes.ENABLE_CONNECT_PROTOCOL and value not in (0, 1):
  266. return ErrorCodes.PROTOCOL_ERROR
  267. return ErrorCodes.NO_ERROR