PageRenderTime 64ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/third_party/wpt_tools/wpt/tools/wptserve/wptserve/request.py

https://github.com/chromium/chromium
Python | 690 lines | 658 code | 18 blank | 14 comment | 6 complexity | f8d8c1fb5f96d61bece77f5cfcc3dcf6 MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception, Apache-2.0, BSD-3-Clause
  1. # mypy: allow-untyped-defs
  2. import base64
  3. import cgi
  4. import tempfile
  5. from http.cookies import BaseCookie
  6. from io import BytesIO
  7. from typing import Dict, List, TypeVar
  8. from urllib.parse import parse_qsl, urlsplit
  9. from . import stash
  10. from .utils import HTTPException, isomorphic_encode, isomorphic_decode
  11. KT = TypeVar('KT')
  12. VT = TypeVar('VT')
  13. missing = object()
  14. class Server:
  15. """Data about the server environment
  16. .. attribute:: config
  17. Environment configuration information with information about the
  18. various servers running, their hostnames and ports.
  19. .. attribute:: stash
  20. Stash object holding state stored on the server between requests.
  21. """
  22. config = None
  23. def __init__(self, request):
  24. self._stash = None
  25. self._request = request
  26. @property
  27. def stash(self):
  28. if self._stash is None:
  29. address, authkey = stash.load_env_config()
  30. self._stash = stash.Stash(self._request.url_parts.path, address, authkey)
  31. return self._stash
  32. class InputFile:
  33. max_buffer_size = 1024*1024
  34. def __init__(self, rfile, length):
  35. """File-like object used to provide a seekable view of request body data"""
  36. self._file = rfile
  37. self.length = length
  38. self._file_position = 0
  39. if length > self.max_buffer_size:
  40. self._buf = tempfile.TemporaryFile()
  41. else:
  42. self._buf = BytesIO()
  43. @property
  44. def _buf_position(self):
  45. rv = self._buf.tell()
  46. assert rv <= self._file_position
  47. return rv
  48. def read(self, bytes=-1):
  49. assert self._buf_position <= self._file_position
  50. if bytes < 0:
  51. bytes = self.length - self._buf_position
  52. bytes_remaining = min(bytes, self.length - self._buf_position)
  53. if bytes_remaining == 0:
  54. return b""
  55. if self._buf_position != self._file_position:
  56. buf_bytes = min(bytes_remaining, self._file_position - self._buf_position)
  57. old_data = self._buf.read(buf_bytes)
  58. bytes_remaining -= buf_bytes
  59. else:
  60. old_data = b""
  61. assert bytes_remaining == 0 or self._buf_position == self._file_position, (
  62. "Before reading buffer position (%i) didn't match file position (%i)" %
  63. (self._buf_position, self._file_position))
  64. new_data = self._file.read(bytes_remaining)
  65. self._buf.write(new_data)
  66. self._file_position += bytes_remaining
  67. assert bytes_remaining == 0 or self._buf_position == self._file_position, (
  68. "After reading buffer position (%i) didn't match file position (%i)" %
  69. (self._buf_position, self._file_position))
  70. return old_data + new_data
  71. def tell(self):
  72. return self._buf_position
  73. def seek(self, offset):
  74. if offset > self.length or offset < 0:
  75. raise ValueError
  76. if offset <= self._file_position:
  77. self._buf.seek(offset)
  78. else:
  79. self.read(offset - self._file_position)
  80. def readline(self, max_bytes=None):
  81. if max_bytes is None:
  82. max_bytes = self.length - self._buf_position
  83. if self._buf_position < self._file_position:
  84. data = self._buf.readline(max_bytes)
  85. if data.endswith(b"\n") or len(data) == max_bytes:
  86. return data
  87. else:
  88. data = b""
  89. assert self._buf_position == self._file_position
  90. initial_position = self._file_position
  91. found = False
  92. buf = []
  93. max_bytes -= len(data)
  94. while not found:
  95. readahead = self.read(min(2, max_bytes))
  96. max_bytes -= len(readahead)
  97. for i, c in enumerate(readahead):
  98. if c == b"\n"[0]:
  99. buf.append(readahead[:i+1])
  100. found = True
  101. break
  102. if not found:
  103. buf.append(readahead)
  104. if not readahead or not max_bytes:
  105. break
  106. new_data = b"".join(buf)
  107. data += new_data
  108. self.seek(initial_position + len(new_data))
  109. return data
  110. def readlines(self):
  111. rv = []
  112. while True:
  113. data = self.readline()
  114. if data:
  115. rv.append(data)
  116. else:
  117. break
  118. return rv
  119. def __next__(self):
  120. data = self.readline()
  121. if data:
  122. return data
  123. else:
  124. raise StopIteration
  125. next = __next__
  126. def __iter__(self):
  127. return self
  128. class Request:
  129. """Object representing a HTTP request.
  130. .. attribute:: doc_root
  131. The local directory to use as a base when resolving paths
  132. .. attribute:: route_match
  133. Regexp match object from matching the request path to the route
  134. selected for the request.
  135. .. attribute:: client_address
  136. Contains a tuple of the form (host, port) representing the client's address.
  137. .. attribute:: protocol_version
  138. HTTP version specified in the request.
  139. .. attribute:: method
  140. HTTP method in the request.
  141. .. attribute:: request_path
  142. Request path as it appears in the HTTP request.
  143. .. attribute:: url_base
  144. The prefix part of the path; typically / unless the handler has a url_base set
  145. .. attribute:: url
  146. Absolute URL for the request.
  147. .. attribute:: url_parts
  148. Parts of the requested URL as obtained by urlparse.urlsplit(path)
  149. .. attribute:: request_line
  150. Raw request line
  151. .. attribute:: headers
  152. RequestHeaders object providing a dictionary-like representation of
  153. the request headers.
  154. .. attribute:: raw_headers.
  155. Dictionary of non-normalized request headers.
  156. .. attribute:: body
  157. Request body as a string
  158. .. attribute:: raw_input
  159. File-like object representing the body of the request.
  160. .. attribute:: GET
  161. MultiDict representing the parameters supplied with the request.
  162. Note that these may be present on non-GET requests; the name is
  163. chosen to be familiar to users of other systems such as PHP.
  164. Both keys and values are binary strings.
  165. .. attribute:: POST
  166. MultiDict representing the request body parameters. Most parameters
  167. are present as string values, but file uploads have file-like
  168. values. All string values (including keys) have binary type.
  169. .. attribute:: cookies
  170. A Cookies object representing cookies sent with the request with a
  171. dictionary-like interface.
  172. .. attribute:: auth
  173. An instance of Authentication with username and password properties
  174. representing any credentials supplied using HTTP authentication.
  175. .. attribute:: server
  176. Server object containing information about the server environment.
  177. """
  178. def __init__(self, request_handler):
  179. self.doc_root = request_handler.server.router.doc_root
  180. self.route_match = None # Set by the router
  181. self.client_address = request_handler.client_address
  182. self.protocol_version = request_handler.protocol_version
  183. self.method = request_handler.command
  184. # Keys and values in raw headers are native strings.
  185. self._headers = None
  186. self.raw_headers = request_handler.headers
  187. scheme = request_handler.server.scheme
  188. host = self.raw_headers.get("Host")
  189. port = request_handler.server.server_address[1]
  190. if host is None:
  191. host = request_handler.server.server_address[0]
  192. else:
  193. if ":" in host:
  194. host, port = host.split(":", 1)
  195. self.request_path = request_handler.path
  196. self.url_base = "/"
  197. if self.request_path.startswith(scheme + "://"):
  198. self.url = self.request_path
  199. else:
  200. # TODO(#23362): Stop using native strings for URLs.
  201. self.url = "%s://%s:%s%s" % (
  202. scheme, host, port, self.request_path)
  203. self.url_parts = urlsplit(self.url)
  204. self.request_line = request_handler.raw_requestline
  205. self.raw_input = InputFile(request_handler.rfile,
  206. int(self.raw_headers.get("Content-Length", 0)))
  207. self._body = None
  208. self._GET = None
  209. self._POST = None
  210. self._cookies = None
  211. self._auth = None
  212. self.server = Server(self)
  213. def __repr__(self):
  214. return "<Request %s %s>" % (self.method, self.url)
  215. @property
  216. def GET(self):
  217. if self._GET is None:
  218. kwargs = {
  219. "keep_blank_values": True,
  220. "encoding": "iso-8859-1",
  221. }
  222. params = parse_qsl(self.url_parts.query, **kwargs)
  223. self._GET = MultiDict()
  224. for key, value in params:
  225. self._GET.add(isomorphic_encode(key), isomorphic_encode(value))
  226. return self._GET
  227. @property
  228. def POST(self):
  229. if self._POST is None:
  230. # Work out the post parameters
  231. pos = self.raw_input.tell()
  232. self.raw_input.seek(0)
  233. kwargs = {
  234. "fp": self.raw_input,
  235. "environ": {"REQUEST_METHOD": self.method},
  236. "headers": self.raw_headers,
  237. "keep_blank_values": True,
  238. "encoding": "iso-8859-1",
  239. }
  240. fs = cgi.FieldStorage(**kwargs)
  241. self._POST = MultiDict.from_field_storage(fs)
  242. self.raw_input.seek(pos)
  243. return self._POST
  244. @property
  245. def cookies(self):
  246. if self._cookies is None:
  247. parser = BinaryCookieParser()
  248. cookie_headers = self.headers.get("cookie", b"")
  249. parser.load(cookie_headers)
  250. cookies = Cookies()
  251. for key, value in parser.items():
  252. cookies[isomorphic_encode(key)] = CookieValue(value)
  253. self._cookies = cookies
  254. return self._cookies
  255. @property
  256. def headers(self):
  257. if self._headers is None:
  258. self._headers = RequestHeaders(self.raw_headers)
  259. return self._headers
  260. @property
  261. def body(self):
  262. if self._body is None:
  263. pos = self.raw_input.tell()
  264. self.raw_input.seek(0)
  265. self._body = self.raw_input.read()
  266. self.raw_input.seek(pos)
  267. return self._body
  268. @property
  269. def auth(self):
  270. if self._auth is None:
  271. self._auth = Authentication(self.headers)
  272. return self._auth
  273. class H2Request(Request):
  274. def __init__(self, request_handler):
  275. self.h2_stream_id = request_handler.h2_stream_id
  276. self.frames = []
  277. super().__init__(request_handler)
  278. class RequestHeaders(Dict[bytes, List[bytes]]):
  279. """Read-only dictionary-like API for accessing request headers.
  280. Unlike BaseHTTPRequestHandler.headers, this class always returns all
  281. headers with the same name (separated by commas). And it ensures all keys
  282. (i.e. names of headers) and values have binary type.
  283. """
  284. def __init__(self, items):
  285. for header in items.keys():
  286. key = isomorphic_encode(header).lower()
  287. # get all headers with the same name
  288. values = items.getallmatchingheaders(header)
  289. if len(values) > 1:
  290. # collect the multiple variations of the current header
  291. multiples = []
  292. # loop through the values from getallmatchingheaders
  293. for value in values:
  294. # getallmatchingheaders returns raw header lines, so
  295. # split to get name, value
  296. multiples.append(isomorphic_encode(value).split(b':', 1)[1].strip())
  297. headers = multiples
  298. else:
  299. headers = [isomorphic_encode(items[header])]
  300. dict.__setitem__(self, key, headers)
  301. def __getitem__(self, key):
  302. """Get all headers of a certain (case-insensitive) name. If there is
  303. more than one, the values are returned comma separated"""
  304. key = isomorphic_encode(key)
  305. values = dict.__getitem__(self, key.lower())
  306. if len(values) == 1:
  307. return values[0]
  308. else:
  309. return b", ".join(values)
  310. def __setitem__(self, name, value):
  311. raise Exception
  312. def get(self, key, default=None):
  313. """Get a string representing all headers with a particular value,
  314. with multiple headers separated by a comma. If no header is found
  315. return a default value
  316. :param key: The header name to look up (case-insensitive)
  317. :param default: The value to return in the case of no match
  318. """
  319. try:
  320. return self[key]
  321. except KeyError:
  322. return default
  323. def get_list(self, key, default=missing):
  324. """Get all the header values for a particular field name as
  325. a list"""
  326. key = isomorphic_encode(key)
  327. try:
  328. return dict.__getitem__(self, key.lower())
  329. except KeyError:
  330. if default is not missing:
  331. return default
  332. else:
  333. raise
  334. def __contains__(self, key):
  335. key = isomorphic_encode(key)
  336. return dict.__contains__(self, key.lower())
  337. def iteritems(self):
  338. for item in self:
  339. yield item, self[item]
  340. def itervalues(self):
  341. for item in self:
  342. yield self[item]
  343. class CookieValue:
  344. """Representation of cookies.
  345. Note that cookies are considered read-only and the string value
  346. of the cookie will not change if you update the field values.
  347. However this is not enforced.
  348. .. attribute:: key
  349. The name of the cookie.
  350. .. attribute:: value
  351. The value of the cookie
  352. .. attribute:: expires
  353. The expiry date of the cookie
  354. .. attribute:: path
  355. The path of the cookie
  356. .. attribute:: comment
  357. The comment of the cookie.
  358. .. attribute:: domain
  359. The domain with which the cookie is associated
  360. .. attribute:: max_age
  361. The max-age value of the cookie.
  362. .. attribute:: secure
  363. Whether the cookie is marked as secure
  364. .. attribute:: httponly
  365. Whether the cookie is marked as httponly
  366. """
  367. def __init__(self, morsel):
  368. self.key = morsel.key
  369. self.value = morsel.value
  370. for attr in ["expires", "path",
  371. "comment", "domain", "max-age",
  372. "secure", "version", "httponly"]:
  373. setattr(self, attr.replace("-", "_"), morsel[attr])
  374. self._str = morsel.OutputString()
  375. def __str__(self):
  376. return self._str
  377. def __repr__(self):
  378. return self._str
  379. def __eq__(self, other):
  380. """Equality comparison for cookies. Compares to other cookies
  381. based on value alone and on non-cookies based on the equality
  382. of self.value with the other object so that a cookie with value
  383. "ham" compares equal to the string "ham"
  384. """
  385. if hasattr(other, "value"):
  386. return self.value == other.value
  387. return self.value == other
  388. class MultiDict(Dict[KT, VT]):
  389. """Dictionary type that holds multiple values for each key"""
  390. # TODO: this should perhaps also order the keys
  391. def __init__(self):
  392. pass
  393. def __setitem__(self, name, value):
  394. dict.__setitem__(self, name, [value])
  395. def add(self, name, value):
  396. if name in self:
  397. dict.__getitem__(self, name).append(value)
  398. else:
  399. dict.__setitem__(self, name, [value])
  400. def __getitem__(self, key):
  401. """Get the first value with a given key"""
  402. return self.first(key)
  403. def first(self, key, default=missing):
  404. """Get the first value with a given key
  405. :param key: The key to lookup
  406. :param default: The default to return if key is
  407. not found (throws if nothing is
  408. specified)
  409. """
  410. if key in self and dict.__getitem__(self, key):
  411. return dict.__getitem__(self, key)[0]
  412. elif default is not missing:
  413. return default
  414. raise KeyError(key)
  415. def last(self, key, default=missing):
  416. """Get the last value with a given key
  417. :param key: The key to lookup
  418. :param default: The default to return if key is
  419. not found (throws if nothing is
  420. specified)
  421. """
  422. if key in self and dict.__getitem__(self, key):
  423. return dict.__getitem__(self, key)[-1]
  424. elif default is not missing:
  425. return default
  426. raise KeyError(key)
  427. # We need to explicitly override dict.get; otherwise, it won't call
  428. # __getitem__ and would return a list instead.
  429. def get(self, key, default=None):
  430. """Get the first value with a given key
  431. :param key: The key to lookup
  432. :param default: The default to return if key is
  433. not found (None by default)
  434. """
  435. return self.first(key, default)
  436. def get_list(self, key):
  437. """Get all values with a given key as a list
  438. :param key: The key to lookup
  439. """
  440. if key in self:
  441. return dict.__getitem__(self, key)
  442. else:
  443. return []
  444. @classmethod
  445. def from_field_storage(cls, fs):
  446. """Construct a MultiDict from a cgi.FieldStorage
  447. Note that all keys and values are binary strings.
  448. """
  449. self = cls()
  450. if fs.list is None:
  451. return self
  452. for key in fs:
  453. values = fs[key]
  454. if not isinstance(values, list):
  455. values = [values]
  456. for value in values:
  457. if not value.filename:
  458. value = isomorphic_encode(value.value)
  459. else:
  460. assert isinstance(value, cgi.FieldStorage)
  461. self.add(isomorphic_encode(key), value)
  462. return self
  463. class BinaryCookieParser(BaseCookie): # type: ignore
  464. """A subclass of BaseCookie that returns values in binary strings
  465. This is not intended to store the cookies; use Cookies instead.
  466. """
  467. def value_decode(self, val):
  468. """Decode value from network to (real_value, coded_value).
  469. Override BaseCookie.value_decode.
  470. """
  471. return isomorphic_encode(val), val
  472. def value_encode(self, val):
  473. raise NotImplementedError('BinaryCookieParser is not for setting cookies')
  474. def load(self, rawdata):
  475. """Load cookies from a binary string.
  476. This overrides and calls BaseCookie.load. Unlike BaseCookie.load, it
  477. does not accept dictionaries.
  478. """
  479. assert isinstance(rawdata, bytes)
  480. # BaseCookie.load expects a native string
  481. super().load(isomorphic_decode(rawdata))
  482. class Cookies(MultiDict[bytes, CookieValue]):
  483. """MultiDict specialised for Cookie values
  484. Keys are binary strings and values are CookieValue objects.
  485. """
  486. def __init__(self):
  487. pass
  488. def __getitem__(self, key):
  489. return self.last(key)
  490. class Authentication:
  491. """Object for dealing with HTTP Authentication
  492. .. attribute:: username
  493. The username supplied in the HTTP Authorization
  494. header, or None
  495. .. attribute:: password
  496. The password supplied in the HTTP Authorization
  497. header, or None
  498. Both attributes are binary strings (`str` in Py2, `bytes` in Py3), since
  499. RFC7617 Section 2.1 does not specify the encoding for username & password
  500. (as long it's compatible with ASCII). UTF-8 should be a relatively safe
  501. choice if callers need to decode them as most browsers use it.
  502. """
  503. def __init__(self, headers):
  504. self.username = None
  505. self.password = None
  506. auth_schemes = {b"Basic": self.decode_basic}
  507. if "authorization" in headers:
  508. header = headers.get("authorization")
  509. assert isinstance(header, bytes)
  510. auth_type, data = header.split(b" ", 1)
  511. if auth_type in auth_schemes:
  512. self.username, self.password = auth_schemes[auth_type](data)
  513. else:
  514. raise HTTPException(400, "Unsupported authentication scheme %s" % auth_type)
  515. def decode_basic(self, data):
  516. assert isinstance(data, bytes)
  517. decoded_data = base64.b64decode(data)
  518. return decoded_data.split(b":", 1)