1# -*- coding: utf-8 -*-
2# flake8: noqa
3from collections import deque
4import logging
5
6from .exceptions import InvalidTableIndex
7
8log = logging.getLogger(__name__)
9
10
11def table_entry_size(name, value):
12    """
13    Calculates the size of a single entry
14
15    This size is mostly irrelevant to us and defined
16    specifically to accommodate memory management for
17    lower level implementations. The 32 extra bytes are
18    considered the "maximum" overhead that would be
19    required to represent each entry in the table.
20
21    See RFC7541 Section 4.1
22    """
23    return 32 + len(name) + len(value)
24
25
26class HeaderTable(object):
27    """
28    Implements the combined static and dynamic header table
29
30    The name and value arguments for all the functions
31    should ONLY be byte strings (b'') however this is not
32    strictly enforced in the interface.
33
34    See RFC7541 Section 2.3
35    """
36    #: Default maximum size of the dynamic table. See
37    #:  RFC7540 Section 6.5.2.
38    DEFAULT_SIZE = 4096
39
40    #: Constant list of static headers. See RFC7541 Section
41    #:  2.3.1 and Appendix A
42    STATIC_TABLE = (
43        (b':authority'                  , b''             ),  # noqa
44        (b':method'                     , b'GET'          ),  # noqa
45        (b':method'                     , b'POST'         ),  # noqa
46        (b':path'                       , b'/'            ),  # noqa
47        (b':path'                       , b'/index.html'  ),  # noqa
48        (b':scheme'                     , b'http'         ),  # noqa
49        (b':scheme'                     , b'https'        ),  # noqa
50        (b':status'                     , b'200'          ),  # noqa
51        (b':status'                     , b'204'          ),  # noqa
52        (b':status'                     , b'206'          ),  # noqa
53        (b':status'                     , b'304'          ),  # noqa
54        (b':status'                     , b'400'          ),  # noqa
55        (b':status'                     , b'404'          ),  # noqa
56        (b':status'                     , b'500'          ),  # noqa
57        (b'accept-charset'              , b''             ),  # noqa
58        (b'accept-encoding'             , b'gzip, deflate'),  # noqa
59        (b'accept-language'             , b''             ),  # noqa
60        (b'accept-ranges'               , b''             ),  # noqa
61        (b'accept'                      , b''             ),  # noqa
62        (b'access-control-allow-origin' , b''             ),  # noqa
63        (b'age'                         , b''             ),  # noqa
64        (b'allow'                       , b''             ),  # noqa
65        (b'authorization'               , b''             ),  # noqa
66        (b'cache-control'               , b''             ),  # noqa
67        (b'content-disposition'         , b''             ),  # noqa
68        (b'content-encoding'            , b''             ),  # noqa
69        (b'content-language'            , b''             ),  # noqa
70        (b'content-length'              , b''             ),  # noqa
71        (b'content-location'            , b''             ),  # noqa
72        (b'content-range'               , b''             ),  # noqa
73        (b'content-type'                , b''             ),  # noqa
74        (b'cookie'                      , b''             ),  # noqa
75        (b'date'                        , b''             ),  # noqa
76        (b'etag'                        , b''             ),  # noqa
77        (b'expect'                      , b''             ),  # noqa
78        (b'expires'                     , b''             ),  # noqa
79        (b'from'                        , b''             ),  # noqa
80        (b'host'                        , b''             ),  # noqa
81        (b'if-match'                    , b''             ),  # noqa
82        (b'if-modified-since'           , b''             ),  # noqa
83        (b'if-none-match'               , b''             ),  # noqa
84        (b'if-range'                    , b''             ),  # noqa
85        (b'if-unmodified-since'         , b''             ),  # noqa
86        (b'last-modified'               , b''             ),  # noqa
87        (b'link'                        , b''             ),  # noqa
88        (b'location'                    , b''             ),  # noqa
89        (b'max-forwards'                , b''             ),  # noqa
90        (b'proxy-authenticate'          , b''             ),  # noqa
91        (b'proxy-authorization'         , b''             ),  # noqa
92        (b'range'                       , b''             ),  # noqa
93        (b'referer'                     , b''             ),  # noqa
94        (b'refresh'                     , b''             ),  # noqa
95        (b'retry-after'                 , b''             ),  # noqa
96        (b'server'                      , b''             ),  # noqa
97        (b'set-cookie'                  , b''             ),  # noqa
98        (b'strict-transport-security'   , b''             ),  # noqa
99        (b'transfer-encoding'           , b''             ),  # noqa
100        (b'user-agent'                  , b''             ),  # noqa
101        (b'vary'                        , b''             ),  # noqa
102        (b'via'                         , b''             ),  # noqa
103        (b'www-authenticate'            , b''             ),  # noqa
104    )  # noqa
105
106    STATIC_TABLE_LENGTH = len(STATIC_TABLE)
107
108    def __init__(self):
109        self._maxsize = HeaderTable.DEFAULT_SIZE
110        self._current_size = 0
111        self.resized = False
112        self.dynamic_entries = deque()
113
114    def get_by_index(self, index):
115        """
116        Returns the entry specified by index
117
118        Note that the table is 1-based ie an index of 0 is
119        invalid.  This is due to the fact that a zero value
120        index signals that a completely unindexed header
121        follows.
122
123        The entry will either be from the static table or
124        the dynamic table depending on the value of index.
125        """
126        original_index = index
127        index -= 1
128        if 0 <= index:
129            if index < HeaderTable.STATIC_TABLE_LENGTH:
130                return HeaderTable.STATIC_TABLE[index]
131
132            index -= HeaderTable.STATIC_TABLE_LENGTH
133            if index < len(self.dynamic_entries):
134                return self.dynamic_entries[index]
135
136        raise InvalidTableIndex("Invalid table index %d" % original_index)
137
138    def __repr__(self):
139        return "HeaderTable(%d, %s, %r)" % (
140            self._maxsize,
141            self.resized,
142            self.dynamic_entries
143        )
144
145    def add(self, name, value):
146        """
147        Adds a new entry to the table
148
149        We reduce the table size if the entry will make the
150        table size greater than maxsize.
151        """
152        # We just clear the table if the entry is too big
153        size = table_entry_size(name, value)
154        if size > self._maxsize:
155            self.dynamic_entries.clear()
156            self._current_size = 0
157        else:
158            # Add new entry
159            self.dynamic_entries.appendleft((name, value))
160            self._current_size += size
161            self._shrink()
162
163    def search(self, name, value):
164        """
165        Searches the table for the entry specified by name
166        and value
167
168        Returns one of the following:
169            - ``None``, no match at all
170            - ``(index, name, None)`` for partial matches on name only.
171            - ``(index, name, value)`` for perfect matches.
172        """
173        offset = HeaderTable.STATIC_TABLE_LENGTH + 1
174        partial = None
175        for (i, (n, v)) in enumerate(HeaderTable.STATIC_TABLE):
176            if n == name:
177                if v == value:
178                    return i + 1, n, v
179                elif partial is None:
180                    partial = (i + 1, n, None)
181        for (i, (n, v)) in enumerate(self.dynamic_entries):
182            if n == name:
183                if v == value:
184                    return i + offset, n, v
185                elif partial is None:
186                    partial = (i + offset, n, None)
187        return partial
188
189    @property
190    def maxsize(self):
191        return self._maxsize
192
193    @maxsize.setter
194    def maxsize(self, newmax):
195        newmax = int(newmax)
196        log.debug("Resizing header table to %d from %d", newmax, self._maxsize)
197        oldmax = self._maxsize
198        self._maxsize = newmax
199        self.resized = (newmax != oldmax)
200        if newmax <= 0:
201            self.dynamic_entries.clear()
202            self._current_size = 0
203        elif oldmax > newmax:
204            self._shrink()
205
206    def _shrink(self):
207        """
208        Shrinks the dynamic table to be at or below maxsize
209        """
210        cursize = self._current_size
211        while cursize > self._maxsize:
212            name, value = self.dynamic_entries.pop()
213            cursize -= table_entry_size(name, value)
214            log.debug("Evicting %s: %s from the header table", name, value)
215        self._current_size = cursize
216