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