123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215 |
- # -*- coding: utf-8 -*-
- # flake8: noqa
- from collections import deque
- import logging
- from .exceptions import InvalidTableIndex
- log = logging.getLogger(__name__)
- def table_entry_size(name, value):
- """
- Calculates the size of a single entry
- This size is mostly irrelevant to us and defined
- specifically to accommodate memory management for
- lower level implementations. The 32 extra bytes are
- considered the "maximum" overhead that would be
- required to represent each entry in the table.
- See RFC7541 Section 4.1
- """
- return 32 + len(name) + len(value)
- class HeaderTable(object):
- """
- Implements the combined static and dynamic header table
- The name and value arguments for all the functions
- should ONLY be byte strings (b'') however this is not
- strictly enforced in the interface.
- See RFC7541 Section 2.3
- """
- #: Default maximum size of the dynamic table. See
- #: RFC7540 Section 6.5.2.
- DEFAULT_SIZE = 4096
- #: Constant list of static headers. See RFC7541 Section
- #: 2.3.1 and Appendix A
- STATIC_TABLE = (
- (b':authority' , b'' ), # noqa
- (b':method' , b'GET' ), # noqa
- (b':method' , b'POST' ), # noqa
- (b':path' , b'/' ), # noqa
- (b':path' , b'/index.html' ), # noqa
- (b':scheme' , b'http' ), # noqa
- (b':scheme' , b'https' ), # noqa
- (b':status' , b'200' ), # noqa
- (b':status' , b'204' ), # noqa
- (b':status' , b'206' ), # noqa
- (b':status' , b'304' ), # noqa
- (b':status' , b'400' ), # noqa
- (b':status' , b'404' ), # noqa
- (b':status' , b'500' ), # noqa
- (b'accept-charset' , b'' ), # noqa
- (b'accept-encoding' , b'gzip, deflate'), # noqa
- (b'accept-language' , b'' ), # noqa
- (b'accept-ranges' , b'' ), # noqa
- (b'accept' , b'' ), # noqa
- (b'access-control-allow-origin' , b'' ), # noqa
- (b'age' , b'' ), # noqa
- (b'allow' , b'' ), # noqa
- (b'authorization' , b'' ), # noqa
- (b'cache-control' , b'' ), # noqa
- (b'content-disposition' , b'' ), # noqa
- (b'content-encoding' , b'' ), # noqa
- (b'content-language' , b'' ), # noqa
- (b'content-length' , b'' ), # noqa
- (b'content-location' , b'' ), # noqa
- (b'content-range' , b'' ), # noqa
- (b'content-type' , b'' ), # noqa
- (b'cookie' , b'' ), # noqa
- (b'date' , b'' ), # noqa
- (b'etag' , b'' ), # noqa
- (b'expect' , b'' ), # noqa
- (b'expires' , b'' ), # noqa
- (b'from' , b'' ), # noqa
- (b'host' , b'' ), # noqa
- (b'if-match' , b'' ), # noqa
- (b'if-modified-since' , b'' ), # noqa
- (b'if-none-match' , b'' ), # noqa
- (b'if-range' , b'' ), # noqa
- (b'if-unmodified-since' , b'' ), # noqa
- (b'last-modified' , b'' ), # noqa
- (b'link' , b'' ), # noqa
- (b'location' , b'' ), # noqa
- (b'max-forwards' , b'' ), # noqa
- (b'proxy-authenticate' , b'' ), # noqa
- (b'proxy-authorization' , b'' ), # noqa
- (b'range' , b'' ), # noqa
- (b'referer' , b'' ), # noqa
- (b'refresh' , b'' ), # noqa
- (b'retry-after' , b'' ), # noqa
- (b'server' , b'' ), # noqa
- (b'set-cookie' , b'' ), # noqa
- (b'strict-transport-security' , b'' ), # noqa
- (b'transfer-encoding' , b'' ), # noqa
- (b'user-agent' , b'' ), # noqa
- (b'vary' , b'' ), # noqa
- (b'via' , b'' ), # noqa
- (b'www-authenticate' , b'' ), # noqa
- ) # noqa
- STATIC_TABLE_LENGTH = len(STATIC_TABLE)
- def __init__(self):
- self._maxsize = HeaderTable.DEFAULT_SIZE
- self._current_size = 0
- self.resized = False
- self.dynamic_entries = deque()
- def get_by_index(self, index):
- """
- Returns the entry specified by index
- Note that the table is 1-based ie an index of 0 is
- invalid. This is due to the fact that a zero value
- index signals that a completely unindexed header
- follows.
- The entry will either be from the static table or
- the dynamic table depending on the value of index.
- """
- original_index = index
- index -= 1
- if 0 <= index:
- if index < HeaderTable.STATIC_TABLE_LENGTH:
- return HeaderTable.STATIC_TABLE[index]
- index -= HeaderTable.STATIC_TABLE_LENGTH
- if index < len(self.dynamic_entries):
- return self.dynamic_entries[index]
- raise InvalidTableIndex("Invalid table index %d" % original_index)
- def __repr__(self):
- return "HeaderTable(%d, %s, %r)" % (
- self._maxsize,
- self.resized,
- self.dynamic_entries
- )
- def add(self, name, value):
- """
- Adds a new entry to the table
- We reduce the table size if the entry will make the
- table size greater than maxsize.
- """
- # We just clear the table if the entry is too big
- size = table_entry_size(name, value)
- if size > self._maxsize:
- self.dynamic_entries.clear()
- self._current_size = 0
- else:
- # Add new entry
- self.dynamic_entries.appendleft((name, value))
- self._current_size += size
- self._shrink()
- def search(self, name, value):
- """
- Searches the table for the entry specified by name
- and value
- Returns one of the following:
- - ``None``, no match at all
- - ``(index, name, None)`` for partial matches on name only.
- - ``(index, name, value)`` for perfect matches.
- """
- offset = HeaderTable.STATIC_TABLE_LENGTH + 1
- partial = None
- for (i, (n, v)) in enumerate(HeaderTable.STATIC_TABLE):
- if n == name:
- if v == value:
- return i + 1, n, v
- elif partial is None:
- partial = (i + 1, n, None)
- for (i, (n, v)) in enumerate(self.dynamic_entries):
- if n == name:
- if v == value:
- return i + offset, n, v
- elif partial is None:
- partial = (i + offset, n, None)
- return partial
- @property
- def maxsize(self):
- return self._maxsize
- @maxsize.setter
- def maxsize(self, newmax):
- newmax = int(newmax)
- log.debug("Resizing header table to %d from %d", newmax, self._maxsize)
- oldmax = self._maxsize
- self._maxsize = newmax
- self.resized = (newmax != oldmax)
- if newmax <= 0:
- self.dynamic_entries.clear()
- self._current_size = 0
- elif oldmax > newmax:
- self._shrink()
- def _shrink(self):
- """
- Shrinks the dynamic table to be at or below maxsize
- """
- cursize = self._current_size
- while cursize > self._maxsize:
- name, value = self.dynamic_entries.pop()
- cursize -= table_entry_size(name, value)
- log.debug("Evicting %s: %s from the header table", name, value)
- self._current_size = cursize
|