table.py 9.8 KB

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