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