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