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 #: Indicates the size of the largest frame payload that the sender is 43 #: willing to receive, in octets. 44 MAX_FRAME_SIZE = SettingsFrame.MAX_FRAME_SIZE 45 46 #: This advisory setting informs a peer of the maximum size of header list 47 #: that the sender is prepared to accept, in octets. The value is based on 48 #: the uncompressed size of header fields, including the length of the name 49 #: and value in octets plus an overhead of 32 octets for each header field. 50 MAX_HEADER_LIST_SIZE = SettingsFrame.MAX_HEADER_LIST_SIZE 51 52 53def _setting_code_from_int(code): 54 """ 55 Given an integer setting code, returns either one of :class:`SettingCodes 56 <h2.settings.SettingCodes>` or, if not present in the known set of codes, 57 returns the integer directly. 58 """ 59 try: 60 return SettingCodes(code) 61 except ValueError: 62 return code 63 64 65class ChangedSetting: 66 67 def __init__(self, setting, original_value, new_value): 68 #: The setting code given. Either one of :class:`SettingCodes 69 #: <h2.settings.SettingCodes>` or ``int`` 70 #: 71 #: .. versionchanged:: 2.6.0 72 self.setting = setting 73 74 #: The original value before being changed. 75 self.original_value = original_value 76 77 #: The new value after being changed. 78 self.new_value = new_value 79 80 def __repr__(self): 81 return ( 82 "ChangedSetting(setting=%s, original_value=%s, " 83 "new_value=%s)" 84 ) % ( 85 self.setting, 86 self.original_value, 87 self.new_value 88 ) 89 90 91class Settings(collections.MutableMapping): 92 """ 93 An object that encapsulates HTTP/2 settings state. 94 95 HTTP/2 Settings are a complex beast. Each party, remote and local, has its 96 own settings and a view of the other party's settings. When a settings 97 frame is emitted by a peer it cannot assume that the new settings values 98 are in place until the remote peer acknowledges the setting. In principle, 99 multiple settings changes can be "in flight" at the same time, all with 100 different values. 101 102 This object encapsulates this mess. It provides a dict-like interface to 103 settings, which return the *current* values of the settings in question. 104 Additionally, it keeps track of the stack of proposed values: each time an 105 acknowledgement is sent/received, it updates the current values with the 106 stack of proposed values. On top of all that, it validates the values to 107 make sure they're allowed, and raises :class:`InvalidSettingsValueError 108 <h2.exceptions.InvalidSettingsValueError>` if they are not. 109 110 Finally, this object understands what the default values of the HTTP/2 111 settings are, and sets those defaults appropriately. 112 113 .. versionchanged:: 2.2.0 114 Added the ``initial_values`` parameter. 115 116 .. versionchanged:: 2.5.0 117 Added the ``max_header_list_size`` property. 118 119 :param client: (optional) Whether these settings should be defaulted for a 120 client implementation or a server implementation. Defaults to ``True``. 121 :type client: ``bool`` 122 :param initial_values: (optional) Any initial values the user would like 123 set, rather than RFC 7540's defaults. 124 :type initial_vales: ``MutableMapping`` 125 """ 126 def __init__(self, client=True, initial_values=None): 127 # Backing object for the settings. This is a dictionary of 128 # (setting: [list of values]), where the first value in the list is the 129 # current value of the setting. Strictly this doesn't use lists but 130 # instead uses collections.deque to avoid repeated memory allocations. 131 # 132 # This contains the default values for HTTP/2. 133 self._settings = { 134 SettingCodes.HEADER_TABLE_SIZE: collections.deque([4096]), 135 SettingCodes.ENABLE_PUSH: collections.deque([int(client)]), 136 SettingCodes.INITIAL_WINDOW_SIZE: collections.deque([65535]), 137 SettingCodes.MAX_FRAME_SIZE: collections.deque([16384]), 138 } 139 if initial_values is not None: 140 for key, value in initial_values.items(): 141 invalid = _validate_setting(key, value) 142 if invalid: 143 raise InvalidSettingsValueError( 144 "Setting %d has invalid value %d" % (key, value), 145 error_code=invalid 146 ) 147 self._settings[key] = collections.deque([value]) 148 149 def acknowledge(self): 150 """ 151 The settings have been acknowledged, either by the user (remote 152 settings) or by the remote peer (local settings). 153 154 :returns: A dict of {setting: ChangedSetting} that were applied. 155 """ 156 changed_settings = {} 157 158 # If there is more than one setting in the list, we have a setting 159 # value outstanding. Update them. 160 for k, v in self._settings.items(): 161 if len(v) > 1: 162 old_setting = v.popleft() 163 new_setting = v[0] 164 changed_settings[k] = ChangedSetting( 165 k, old_setting, new_setting 166 ) 167 168 return changed_settings 169 170 # Provide easy-access to well known settings. 171 @property 172 def header_table_size(self): 173 """ 174 The current value of the :data:`HEADER_TABLE_SIZE 175 <h2.settings.SettingCodes.HEADER_TABLE_SIZE>` setting. 176 """ 177 return self[SettingCodes.HEADER_TABLE_SIZE] 178 179 @header_table_size.setter 180 def header_table_size(self, value): 181 self[SettingCodes.HEADER_TABLE_SIZE] = value 182 183 @property 184 def enable_push(self): 185 """ 186 The current value of the :data:`ENABLE_PUSH 187 <h2.settings.SettingCodes.ENABLE_PUSH>` setting. 188 """ 189 return self[SettingCodes.ENABLE_PUSH] 190 191 @enable_push.setter 192 def enable_push(self, value): 193 self[SettingCodes.ENABLE_PUSH] = value 194 195 @property 196 def initial_window_size(self): 197 """ 198 The current value of the :data:`INITIAL_WINDOW_SIZE 199 <h2.settings.SettingCodes.INITIAL_WINDOW_SIZE>` setting. 200 """ 201 return self[SettingCodes.INITIAL_WINDOW_SIZE] 202 203 @initial_window_size.setter 204 def initial_window_size(self, value): 205 self[SettingCodes.INITIAL_WINDOW_SIZE] = value 206 207 @property 208 def max_frame_size(self): 209 """ 210 The current value of the :data:`MAX_FRAME_SIZE 211 <h2.settings.SettingCodes.MAX_FRAME_SIZE>` setting. 212 """ 213 return self[SettingCodes.MAX_FRAME_SIZE] 214 215 @max_frame_size.setter 216 def max_frame_size(self, value): 217 self[SettingCodes.MAX_FRAME_SIZE] = value 218 219 @property 220 def max_concurrent_streams(self): 221 """ 222 The current value of the :data:`MAX_CONCURRENT_STREAMS 223 <h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS>` setting. 224 """ 225 return self.get(SettingCodes.MAX_CONCURRENT_STREAMS, 2**32+1) 226 227 @max_concurrent_streams.setter 228 def max_concurrent_streams(self, value): 229 self[SettingCodes.MAX_CONCURRENT_STREAMS] = value 230 231 @property 232 def max_header_list_size(self): 233 """ 234 The current value of the :data:`MAX_HEADER_LIST_SIZE 235 <h2.settings.SettingCodes.MAX_HEADER_LIST_SIZE>` setting. If not set, 236 returns ``None``, which means unlimited. 237 238 .. versionadded:: 2.5.0 239 """ 240 return self.get(SettingCodes.MAX_HEADER_LIST_SIZE, None) 241 242 @max_header_list_size.setter 243 def max_header_list_size(self, value): 244 self[SettingCodes.MAX_HEADER_LIST_SIZE] = value 245 246 # Implement the MutableMapping API. 247 def __getitem__(self, key): 248 val = self._settings[key][0] 249 250 # Things that were created when a setting was received should stay 251 # KeyError'd. 252 if val is None: 253 raise KeyError 254 255 return val 256 257 def __setitem__(self, key, value): 258 invalid = _validate_setting(key, value) 259 if invalid: 260 raise InvalidSettingsValueError( 261 "Setting %d has invalid value %d" % (key, value), 262 error_code=invalid 263 ) 264 265 try: 266 items = self._settings[key] 267 except KeyError: 268 items = collections.deque([None]) 269 self._settings[key] = items 270 271 items.append(value) 272 273 def __delitem__(self, key): 274 del self._settings[key] 275 276 def __iter__(self): 277 return self._settings.__iter__() 278 279 def __len__(self): 280 return len(self._settings) 281 282 def __eq__(self, other): 283 if isinstance(other, Settings): 284 return self._settings == other._settings 285 else: 286 return NotImplemented 287 288 def __ne__(self, other): 289 if isinstance(other, Settings): 290 return not self == other 291 else: 292 return NotImplemented 293 294 295def _validate_setting(setting, value): 296 """ 297 Confirms that a specific setting has a well-formed value. If the setting is 298 invalid, returns an error code. Otherwise, returns 0 (NO_ERROR). 299 """ 300 if setting == SettingCodes.ENABLE_PUSH: 301 if value not in (0, 1): 302 return ErrorCodes.PROTOCOL_ERROR 303 elif setting == SettingCodes.INITIAL_WINDOW_SIZE: 304 if not 0 <= value <= 2147483647: # 2^31 - 1 305 return ErrorCodes.FLOW_CONTROL_ERROR 306 elif setting == SettingCodes.MAX_FRAME_SIZE: 307 if not 16384 <= value <= 16777215: # 2^14 and 2^24 - 1 308 return ErrorCodes.PROTOCOL_ERROR 309 elif setting == SettingCodes.MAX_HEADER_LIST_SIZE: 310 if value < 0: 311 return ErrorCodes.PROTOCOL_ERROR 312 313 return 0 314