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