table.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. # -*- coding: utf-8 -*-
  2. # flake8: noqa
  3. from collections import deque
  4. import logging
  5. from .exceptions import InvalidTableIndex
  6. log = logging.getLogger(__name__)
  7. def table_entry_size(name, value):
  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(object):
  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. def __init__(self):
  96. self._maxsize = HeaderTable.DEFAULT_SIZE
  97. self._current_size = 0
  98. self.resized = False
  99. self.dynamic_entries = deque()
  100. def get_by_index(self, index):
  101. """
  102. Returns the entry specified by index
  103. Note that the table is 1-based ie an index of 0 is
  104. invalid. This is due to the fact that a zero value
  105. index signals that a completely unindexed header
  106. follows.
  107. The entry will either be from the static table or
  108. the dynamic table depending on the value of index.
  109. """
  110. original_index = index
  111. index -= 1
  112. if 0 <= index:
  113. if index < HeaderTable.STATIC_TABLE_LENGTH:
  114. return HeaderTable.STATIC_TABLE[index]
  115. index -= HeaderTable.STATIC_TABLE_LENGTH
  116. if index < len(self.dynamic_entries):
  117. return self.dynamic_entries[index]
  118. raise InvalidTableIndex("Invalid table index %d" % original_index)
  119. def __repr__(self):
  120. return "HeaderTable(%d, %s, %r)" % (
  121. self._maxsize,
  122. self.resized,
  123. self.dynamic_entries
  124. )
  125. def add(self, name, value):
  126. """
  127. Adds a new entry to the table
  128. We reduce the table size if the entry will make the
  129. table size greater than maxsize.
  130. """
  131. # We just clear the table if the entry is too big
  132. size = table_entry_size(name, value)
  133. if size > self._maxsize:
  134. self.dynamic_entries.clear()
  135. self._current_size = 0
  136. else:
  137. # Add new entry
  138. self.dynamic_entries.appendleft((name, value))
  139. self._current_size += size
  140. self._shrink()
  141. def search(self, name, value):
  142. """
  143. Searches the table for the entry specified by name
  144. and value
  145. Returns one of the following:
  146. - ``None``, no match at all
  147. - ``(index, name, None)`` for partial matches on name only.
  148. - ``(index, name, value)`` for perfect matches.
  149. """
  150. offset = HeaderTable.STATIC_TABLE_LENGTH + 1
  151. partial = None
  152. for (i, (n, v)) in enumerate(HeaderTable.STATIC_TABLE):
  153. if n == name:
  154. if v == value:
  155. return i + 1, n, v
  156. elif partial is None:
  157. partial = (i + 1, n, None)
  158. for (i, (n, v)) in enumerate(self.dynamic_entries):
  159. if n == name:
  160. if v == value:
  161. return i + offset, n, v
  162. elif partial is None:
  163. partial = (i + offset, n, None)
  164. return partial
  165. @property
  166. def maxsize(self):
  167. return self._maxsize
  168. @maxsize.setter
  169. def maxsize(self, newmax):
  170. newmax = int(newmax)
  171. log.debug("Resizing header table to %d from %d", newmax, self._maxsize)
  172. oldmax = self._maxsize
  173. self._maxsize = newmax
  174. self.resized = (newmax != oldmax)
  175. if newmax <= 0:
  176. self.dynamic_entries.clear()
  177. self._current_size = 0
  178. elif oldmax > newmax:
  179. self._shrink()
  180. def _shrink(self):
  181. """
  182. Shrinks the dynamic table to be at or below maxsize
  183. """
  184. cursize = self._current_size
  185. while cursize > self._maxsize:
  186. name, value = self.dynamic_entries.pop()
  187. cursize -= table_entry_size(name, value)
  188. log.debug("Evicting %s: %s from the header table", name, value)
  189. self._current_size = cursize