web_request.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914
  1. import asyncio
  2. import datetime
  3. import io
  4. import re
  5. import socket
  6. import string
  7. import tempfile
  8. import types
  9. import warnings
  10. from types import MappingProxyType
  11. from typing import (
  12. TYPE_CHECKING,
  13. Any,
  14. Dict,
  15. Final,
  16. Iterator,
  17. Mapping,
  18. MutableMapping,
  19. Optional,
  20. Pattern,
  21. Tuple,
  22. Union,
  23. cast,
  24. )
  25. from urllib.parse import parse_qsl
  26. import attr
  27. from multidict import (
  28. CIMultiDict,
  29. CIMultiDictProxy,
  30. MultiDict,
  31. MultiDictProxy,
  32. MultiMapping,
  33. )
  34. from yarl import URL
  35. from . import hdrs
  36. from ._cookie_helpers import parse_cookie_header
  37. from .abc import AbstractStreamWriter
  38. from .helpers import (
  39. _SENTINEL,
  40. DEBUG,
  41. ETAG_ANY,
  42. LIST_QUOTED_ETAG_RE,
  43. ChainMapProxy,
  44. ETag,
  45. HeadersMixin,
  46. parse_http_date,
  47. reify,
  48. sentinel,
  49. set_exception,
  50. )
  51. from .http_parser import RawRequestMessage
  52. from .http_writer import HttpVersion
  53. from .multipart import BodyPartReader, MultipartReader
  54. from .streams import EmptyStreamReader, StreamReader
  55. from .typedefs import (
  56. DEFAULT_JSON_DECODER,
  57. JSONDecoder,
  58. LooseHeaders,
  59. RawHeaders,
  60. StrOrURL,
  61. )
  62. from .web_exceptions import HTTPRequestEntityTooLarge
  63. from .web_response import StreamResponse
  64. __all__ = ("BaseRequest", "FileField", "Request")
  65. if TYPE_CHECKING:
  66. from .web_app import Application
  67. from .web_protocol import RequestHandler
  68. from .web_urldispatcher import UrlMappingMatchInfo
  69. @attr.s(auto_attribs=True, frozen=True, slots=True)
  70. class FileField:
  71. name: str
  72. filename: str
  73. file: io.BufferedReader
  74. content_type: str
  75. headers: CIMultiDictProxy[str]
  76. _TCHAR: Final[str] = string.digits + string.ascii_letters + r"!#$%&'*+.^_`|~-"
  77. # '-' at the end to prevent interpretation as range in a char class
  78. _TOKEN: Final[str] = rf"[{_TCHAR}]+"
  79. _QDTEXT: Final[str] = r"[{}]".format(
  80. r"".join(chr(c) for c in (0x09, 0x20, 0x21) + tuple(range(0x23, 0x7F)))
  81. )
  82. # qdtext includes 0x5C to escape 0x5D ('\]')
  83. # qdtext excludes obs-text (because obsoleted, and encoding not specified)
  84. _QUOTED_PAIR: Final[str] = r"\\[\t !-~]"
  85. _QUOTED_STRING: Final[str] = r'"(?:{quoted_pair}|{qdtext})*"'.format(
  86. qdtext=_QDTEXT, quoted_pair=_QUOTED_PAIR
  87. )
  88. _FORWARDED_PAIR: Final[str] = (
  89. r"({token})=({token}|{quoted_string})(:\d{{1,4}})?".format(
  90. token=_TOKEN, quoted_string=_QUOTED_STRING
  91. )
  92. )
  93. _QUOTED_PAIR_REPLACE_RE: Final[Pattern[str]] = re.compile(r"\\([\t !-~])")
  94. # same pattern as _QUOTED_PAIR but contains a capture group
  95. _FORWARDED_PAIR_RE: Final[Pattern[str]] = re.compile(_FORWARDED_PAIR)
  96. ############################################################
  97. # HTTP Request
  98. ############################################################
  99. class BaseRequest(MutableMapping[str, Any], HeadersMixin):
  100. POST_METHODS = {
  101. hdrs.METH_PATCH,
  102. hdrs.METH_POST,
  103. hdrs.METH_PUT,
  104. hdrs.METH_TRACE,
  105. hdrs.METH_DELETE,
  106. }
  107. ATTRS = HeadersMixin.ATTRS | frozenset(
  108. [
  109. "_message",
  110. "_protocol",
  111. "_payload_writer",
  112. "_payload",
  113. "_headers",
  114. "_method",
  115. "_version",
  116. "_rel_url",
  117. "_post",
  118. "_read_bytes",
  119. "_state",
  120. "_cache",
  121. "_task",
  122. "_client_max_size",
  123. "_loop",
  124. "_transport_sslcontext",
  125. "_transport_peername",
  126. ]
  127. )
  128. _post: Optional[MultiDictProxy[Union[str, bytes, FileField]]] = None
  129. _read_bytes: Optional[bytes] = None
  130. def __init__(
  131. self,
  132. message: RawRequestMessage,
  133. payload: StreamReader,
  134. protocol: "RequestHandler",
  135. payload_writer: AbstractStreamWriter,
  136. task: "asyncio.Task[None]",
  137. loop: asyncio.AbstractEventLoop,
  138. *,
  139. client_max_size: int = 1024**2,
  140. state: Optional[Dict[str, Any]] = None,
  141. scheme: Optional[str] = None,
  142. host: Optional[str] = None,
  143. remote: Optional[str] = None,
  144. ) -> None:
  145. self._message = message
  146. self._protocol = protocol
  147. self._payload_writer = payload_writer
  148. self._payload = payload
  149. self._headers: CIMultiDictProxy[str] = message.headers
  150. self._method = message.method
  151. self._version = message.version
  152. self._cache: Dict[str, Any] = {}
  153. url = message.url
  154. if url.absolute:
  155. if scheme is not None:
  156. url = url.with_scheme(scheme)
  157. if host is not None:
  158. url = url.with_host(host)
  159. # absolute URL is given,
  160. # override auto-calculating url, host, and scheme
  161. # all other properties should be good
  162. self._cache["url"] = url
  163. self._cache["host"] = url.host
  164. self._cache["scheme"] = url.scheme
  165. self._rel_url = url.relative()
  166. else:
  167. self._rel_url = url
  168. if scheme is not None:
  169. self._cache["scheme"] = scheme
  170. if host is not None:
  171. self._cache["host"] = host
  172. self._state = {} if state is None else state
  173. self._task = task
  174. self._client_max_size = client_max_size
  175. self._loop = loop
  176. self._transport_sslcontext = protocol.ssl_context
  177. self._transport_peername = protocol.peername
  178. if remote is not None:
  179. self._cache["remote"] = remote
  180. def clone(
  181. self,
  182. *,
  183. method: Union[str, _SENTINEL] = sentinel,
  184. rel_url: Union[StrOrURL, _SENTINEL] = sentinel,
  185. headers: Union[LooseHeaders, _SENTINEL] = sentinel,
  186. scheme: Union[str, _SENTINEL] = sentinel,
  187. host: Union[str, _SENTINEL] = sentinel,
  188. remote: Union[str, _SENTINEL] = sentinel,
  189. client_max_size: Union[int, _SENTINEL] = sentinel,
  190. ) -> "BaseRequest":
  191. """Clone itself with replacement some attributes.
  192. Creates and returns a new instance of Request object. If no parameters
  193. are given, an exact copy is returned. If a parameter is not passed, it
  194. will reuse the one from the current request object.
  195. """
  196. if self._read_bytes:
  197. raise RuntimeError("Cannot clone request after reading its content")
  198. dct: Dict[str, Any] = {}
  199. if method is not sentinel:
  200. dct["method"] = method
  201. if rel_url is not sentinel:
  202. new_url: URL = URL(rel_url)
  203. dct["url"] = new_url
  204. dct["path"] = str(new_url)
  205. if headers is not sentinel:
  206. # a copy semantic
  207. dct["headers"] = CIMultiDictProxy(CIMultiDict(headers))
  208. dct["raw_headers"] = tuple(
  209. (k.encode("utf-8"), v.encode("utf-8"))
  210. for k, v in dct["headers"].items()
  211. )
  212. message = self._message._replace(**dct)
  213. kwargs = {}
  214. if scheme is not sentinel:
  215. kwargs["scheme"] = scheme
  216. if host is not sentinel:
  217. kwargs["host"] = host
  218. if remote is not sentinel:
  219. kwargs["remote"] = remote
  220. if client_max_size is sentinel:
  221. client_max_size = self._client_max_size
  222. return self.__class__(
  223. message,
  224. self._payload,
  225. self._protocol,
  226. self._payload_writer,
  227. self._task,
  228. self._loop,
  229. client_max_size=client_max_size,
  230. state=self._state.copy(),
  231. **kwargs,
  232. )
  233. @property
  234. def task(self) -> "asyncio.Task[None]":
  235. return self._task
  236. @property
  237. def protocol(self) -> "RequestHandler":
  238. return self._protocol
  239. @property
  240. def transport(self) -> Optional[asyncio.Transport]:
  241. if self._protocol is None:
  242. return None
  243. return self._protocol.transport
  244. @property
  245. def writer(self) -> AbstractStreamWriter:
  246. return self._payload_writer
  247. @property
  248. def client_max_size(self) -> int:
  249. return self._client_max_size
  250. @reify
  251. def message(self) -> RawRequestMessage:
  252. warnings.warn("Request.message is deprecated", DeprecationWarning, stacklevel=3)
  253. return self._message
  254. @reify
  255. def rel_url(self) -> URL:
  256. return self._rel_url
  257. @reify
  258. def loop(self) -> asyncio.AbstractEventLoop:
  259. warnings.warn(
  260. "request.loop property is deprecated", DeprecationWarning, stacklevel=2
  261. )
  262. return self._loop
  263. # MutableMapping API
  264. def __getitem__(self, key: str) -> Any:
  265. return self._state[key]
  266. def __setitem__(self, key: str, value: Any) -> None:
  267. self._state[key] = value
  268. def __delitem__(self, key: str) -> None:
  269. del self._state[key]
  270. def __len__(self) -> int:
  271. return len(self._state)
  272. def __iter__(self) -> Iterator[str]:
  273. return iter(self._state)
  274. ########
  275. @reify
  276. def secure(self) -> bool:
  277. """A bool indicating if the request is handled with SSL."""
  278. return self.scheme == "https"
  279. @reify
  280. def forwarded(self) -> Tuple[Mapping[str, str], ...]:
  281. """A tuple containing all parsed Forwarded header(s).
  282. Makes an effort to parse Forwarded headers as specified by RFC 7239:
  283. - It adds one (immutable) dictionary per Forwarded 'field-value', ie
  284. per proxy. The element corresponds to the data in the Forwarded
  285. field-value added by the first proxy encountered by the client. Each
  286. subsequent item corresponds to those added by later proxies.
  287. - It checks that every value has valid syntax in general as specified
  288. in section 4: either a 'token' or a 'quoted-string'.
  289. - It un-escapes found escape sequences.
  290. - It does NOT validate 'by' and 'for' contents as specified in section
  291. 6.
  292. - It does NOT validate 'host' contents (Host ABNF).
  293. - It does NOT validate 'proto' contents for valid URI scheme names.
  294. Returns a tuple containing one or more immutable dicts
  295. """
  296. elems = []
  297. for field_value in self._message.headers.getall(hdrs.FORWARDED, ()):
  298. length = len(field_value)
  299. pos = 0
  300. need_separator = False
  301. elem: Dict[str, str] = {}
  302. elems.append(types.MappingProxyType(elem))
  303. while 0 <= pos < length:
  304. match = _FORWARDED_PAIR_RE.match(field_value, pos)
  305. if match is not None: # got a valid forwarded-pair
  306. if need_separator:
  307. # bad syntax here, skip to next comma
  308. pos = field_value.find(",", pos)
  309. else:
  310. name, value, port = match.groups()
  311. if value[0] == '"':
  312. # quoted string: remove quotes and unescape
  313. value = _QUOTED_PAIR_REPLACE_RE.sub(r"\1", value[1:-1])
  314. if port:
  315. value += port
  316. elem[name.lower()] = value
  317. pos += len(match.group(0))
  318. need_separator = True
  319. elif field_value[pos] == ",": # next forwarded-element
  320. need_separator = False
  321. elem = {}
  322. elems.append(types.MappingProxyType(elem))
  323. pos += 1
  324. elif field_value[pos] == ";": # next forwarded-pair
  325. need_separator = False
  326. pos += 1
  327. elif field_value[pos] in " \t":
  328. # Allow whitespace even between forwarded-pairs, though
  329. # RFC 7239 doesn't. This simplifies code and is in line
  330. # with Postel's law.
  331. pos += 1
  332. else:
  333. # bad syntax here, skip to next comma
  334. pos = field_value.find(",", pos)
  335. return tuple(elems)
  336. @reify
  337. def scheme(self) -> str:
  338. """A string representing the scheme of the request.
  339. Hostname is resolved in this order:
  340. - overridden value by .clone(scheme=new_scheme) call.
  341. - type of connection to peer: HTTPS if socket is SSL, HTTP otherwise.
  342. 'http' or 'https'.
  343. """
  344. if self._transport_sslcontext:
  345. return "https"
  346. else:
  347. return "http"
  348. @reify
  349. def method(self) -> str:
  350. """Read only property for getting HTTP method.
  351. The value is upper-cased str like 'GET', 'POST', 'PUT' etc.
  352. """
  353. return self._method
  354. @reify
  355. def version(self) -> HttpVersion:
  356. """Read only property for getting HTTP version of request.
  357. Returns aiohttp.protocol.HttpVersion instance.
  358. """
  359. return self._version
  360. @reify
  361. def host(self) -> str:
  362. """Hostname of the request.
  363. Hostname is resolved in this order:
  364. - overridden value by .clone(host=new_host) call.
  365. - HOST HTTP header
  366. - socket.getfqdn() value
  367. For example, 'example.com' or 'localhost:8080'.
  368. For historical reasons, the port number may be included.
  369. """
  370. host = self._message.headers.get(hdrs.HOST)
  371. if host is not None:
  372. return host
  373. return socket.getfqdn()
  374. @reify
  375. def remote(self) -> Optional[str]:
  376. """Remote IP of client initiated HTTP request.
  377. The IP is resolved in this order:
  378. - overridden value by .clone(remote=new_remote) call.
  379. - peername of opened socket
  380. """
  381. if self._transport_peername is None:
  382. return None
  383. if isinstance(self._transport_peername, (list, tuple)):
  384. return str(self._transport_peername[0])
  385. return str(self._transport_peername)
  386. @reify
  387. def url(self) -> URL:
  388. """The full URL of the request."""
  389. # authority is used here because it may include the port number
  390. # and we want yarl to parse it correctly
  391. return URL.build(scheme=self.scheme, authority=self.host).join(self._rel_url)
  392. @reify
  393. def path(self) -> str:
  394. """The URL including *PATH INFO* without the host or scheme.
  395. E.g., ``/app/blog``
  396. """
  397. return self._rel_url.path
  398. @reify
  399. def path_qs(self) -> str:
  400. """The URL including PATH_INFO and the query string.
  401. E.g, /app/blog?id=10
  402. """
  403. return str(self._rel_url)
  404. @reify
  405. def raw_path(self) -> str:
  406. """The URL including raw *PATH INFO* without the host or scheme.
  407. Warning, the path is unquoted and may contains non valid URL characters
  408. E.g., ``/my%2Fpath%7Cwith%21some%25strange%24characters``
  409. """
  410. return self._message.path
  411. @reify
  412. def query(self) -> "MultiMapping[str]":
  413. """A multidict with all the variables in the query string."""
  414. return self._rel_url.query
  415. @reify
  416. def query_string(self) -> str:
  417. """The query string in the URL.
  418. E.g., id=10
  419. """
  420. return self._rel_url.query_string
  421. @reify
  422. def headers(self) -> CIMultiDictProxy[str]:
  423. """A case-insensitive multidict proxy with all headers."""
  424. return self._headers
  425. @reify
  426. def raw_headers(self) -> RawHeaders:
  427. """A sequence of pairs for all headers."""
  428. return self._message.raw_headers
  429. @reify
  430. def if_modified_since(self) -> Optional[datetime.datetime]:
  431. """The value of If-Modified-Since HTTP header, or None.
  432. This header is represented as a `datetime` object.
  433. """
  434. return parse_http_date(self.headers.get(hdrs.IF_MODIFIED_SINCE))
  435. @reify
  436. def if_unmodified_since(self) -> Optional[datetime.datetime]:
  437. """The value of If-Unmodified-Since HTTP header, or None.
  438. This header is represented as a `datetime` object.
  439. """
  440. return parse_http_date(self.headers.get(hdrs.IF_UNMODIFIED_SINCE))
  441. @staticmethod
  442. def _etag_values(etag_header: str) -> Iterator[ETag]:
  443. """Extract `ETag` objects from raw header."""
  444. if etag_header == ETAG_ANY:
  445. yield ETag(
  446. is_weak=False,
  447. value=ETAG_ANY,
  448. )
  449. else:
  450. for match in LIST_QUOTED_ETAG_RE.finditer(etag_header):
  451. is_weak, value, garbage = match.group(2, 3, 4)
  452. # Any symbol captured by 4th group means
  453. # that the following sequence is invalid.
  454. if garbage:
  455. break
  456. yield ETag(
  457. is_weak=bool(is_weak),
  458. value=value,
  459. )
  460. @classmethod
  461. def _if_match_or_none_impl(
  462. cls, header_value: Optional[str]
  463. ) -> Optional[Tuple[ETag, ...]]:
  464. if not header_value:
  465. return None
  466. return tuple(cls._etag_values(header_value))
  467. @reify
  468. def if_match(self) -> Optional[Tuple[ETag, ...]]:
  469. """The value of If-Match HTTP header, or None.
  470. This header is represented as a `tuple` of `ETag` objects.
  471. """
  472. return self._if_match_or_none_impl(self.headers.get(hdrs.IF_MATCH))
  473. @reify
  474. def if_none_match(self) -> Optional[Tuple[ETag, ...]]:
  475. """The value of If-None-Match HTTP header, or None.
  476. This header is represented as a `tuple` of `ETag` objects.
  477. """
  478. return self._if_match_or_none_impl(self.headers.get(hdrs.IF_NONE_MATCH))
  479. @reify
  480. def if_range(self) -> Optional[datetime.datetime]:
  481. """The value of If-Range HTTP header, or None.
  482. This header is represented as a `datetime` object.
  483. """
  484. return parse_http_date(self.headers.get(hdrs.IF_RANGE))
  485. @reify
  486. def keep_alive(self) -> bool:
  487. """Is keepalive enabled by client?"""
  488. return not self._message.should_close
  489. @reify
  490. def cookies(self) -> Mapping[str, str]:
  491. """Return request cookies.
  492. A read-only dictionary-like object.
  493. """
  494. # Use parse_cookie_header for RFC 6265 compliant Cookie header parsing
  495. # that accepts special characters in cookie names (fixes #2683)
  496. parsed = parse_cookie_header(self.headers.get(hdrs.COOKIE, ""))
  497. # Extract values from Morsel objects
  498. return MappingProxyType({name: morsel.value for name, morsel in parsed})
  499. @reify
  500. def http_range(self) -> slice:
  501. """The content of Range HTTP header.
  502. Return a slice instance.
  503. """
  504. rng = self._headers.get(hdrs.RANGE)
  505. start, end = None, None
  506. if rng is not None:
  507. try:
  508. pattern = r"^bytes=(\d*)-(\d*)$"
  509. start, end = re.findall(pattern, rng, re.ASCII)[0]
  510. except IndexError: # pattern was not found in header
  511. raise ValueError("range not in acceptable format")
  512. end = int(end) if end else None
  513. start = int(start) if start else None
  514. if start is None and end is not None:
  515. # end with no start is to return tail of content
  516. start = -end
  517. end = None
  518. if start is not None and end is not None:
  519. # end is inclusive in range header, exclusive for slice
  520. end += 1
  521. if start >= end:
  522. raise ValueError("start cannot be after end")
  523. if start is end is None: # No valid range supplied
  524. raise ValueError("No start or end of range specified")
  525. return slice(start, end, 1)
  526. @reify
  527. def content(self) -> StreamReader:
  528. """Return raw payload stream."""
  529. return self._payload
  530. @property
  531. def has_body(self) -> bool:
  532. """Return True if request's HTTP BODY can be read, False otherwise."""
  533. warnings.warn(
  534. "Deprecated, use .can_read_body #2005", DeprecationWarning, stacklevel=2
  535. )
  536. return not self._payload.at_eof()
  537. @property
  538. def can_read_body(self) -> bool:
  539. """Return True if request's HTTP BODY can be read, False otherwise."""
  540. return not self._payload.at_eof()
  541. @reify
  542. def body_exists(self) -> bool:
  543. """Return True if request has HTTP BODY, False otherwise."""
  544. return type(self._payload) is not EmptyStreamReader
  545. async def release(self) -> None:
  546. """Release request.
  547. Eat unread part of HTTP BODY if present.
  548. """
  549. while not self._payload.at_eof():
  550. await self._payload.readany()
  551. async def read(self) -> bytes:
  552. """Read request body if present.
  553. Returns bytes object with full request content.
  554. """
  555. if self._read_bytes is None:
  556. body = bytearray()
  557. while True:
  558. chunk = await self._payload.readany()
  559. body.extend(chunk)
  560. if self._client_max_size:
  561. body_size = len(body)
  562. if body_size >= self._client_max_size:
  563. raise HTTPRequestEntityTooLarge(
  564. max_size=self._client_max_size, actual_size=body_size
  565. )
  566. if not chunk:
  567. break
  568. self._read_bytes = bytes(body)
  569. return self._read_bytes
  570. async def text(self) -> str:
  571. """Return BODY as text using encoding from .charset."""
  572. bytes_body = await self.read()
  573. encoding = self.charset or "utf-8"
  574. return bytes_body.decode(encoding)
  575. async def json(self, *, loads: JSONDecoder = DEFAULT_JSON_DECODER) -> Any:
  576. """Return BODY as JSON."""
  577. body = await self.text()
  578. return loads(body)
  579. async def multipart(self) -> MultipartReader:
  580. """Return async iterator to process BODY as multipart."""
  581. return MultipartReader(self._headers, self._payload)
  582. async def post(self) -> "MultiDictProxy[Union[str, bytes, FileField]]":
  583. """Return POST parameters."""
  584. if self._post is not None:
  585. return self._post
  586. if self._method not in self.POST_METHODS:
  587. self._post = MultiDictProxy(MultiDict())
  588. return self._post
  589. content_type = self.content_type
  590. if content_type not in (
  591. "",
  592. "application/x-www-form-urlencoded",
  593. "multipart/form-data",
  594. ):
  595. self._post = MultiDictProxy(MultiDict())
  596. return self._post
  597. out: MultiDict[Union[str, bytes, FileField]] = MultiDict()
  598. if content_type == "multipart/form-data":
  599. multipart = await self.multipart()
  600. max_size = self._client_max_size
  601. size = 0
  602. while (field := await multipart.next()) is not None:
  603. field_ct = field.headers.get(hdrs.CONTENT_TYPE)
  604. if isinstance(field, BodyPartReader):
  605. if field.name is None:
  606. raise ValueError("Multipart field missing name.")
  607. # Note that according to RFC 7578, the Content-Type header
  608. # is optional, even for files, so we can't assume it's
  609. # present.
  610. # https://tools.ietf.org/html/rfc7578#section-4.4
  611. if field.filename:
  612. # store file in temp file
  613. tmp = await self._loop.run_in_executor(
  614. None, tempfile.TemporaryFile
  615. )
  616. chunk = await field.read_chunk(size=2**16)
  617. while chunk:
  618. chunk = await field.decode(chunk)
  619. await self._loop.run_in_executor(None, tmp.write, chunk)
  620. size += len(chunk)
  621. if 0 < max_size < size:
  622. await self._loop.run_in_executor(None, tmp.close)
  623. raise HTTPRequestEntityTooLarge(
  624. max_size=max_size, actual_size=size
  625. )
  626. chunk = await field.read_chunk(size=2**16)
  627. await self._loop.run_in_executor(None, tmp.seek, 0)
  628. if field_ct is None:
  629. field_ct = "application/octet-stream"
  630. ff = FileField(
  631. field.name,
  632. field.filename,
  633. cast(io.BufferedReader, tmp),
  634. field_ct,
  635. field.headers,
  636. )
  637. out.add(field.name, ff)
  638. else:
  639. # deal with ordinary data
  640. value = await field.read(decode=True)
  641. if field_ct is None or field_ct.startswith("text/"):
  642. charset = field.get_charset(default="utf-8")
  643. out.add(field.name, value.decode(charset))
  644. else:
  645. out.add(field.name, value)
  646. size += len(value)
  647. if 0 < max_size < size:
  648. raise HTTPRequestEntityTooLarge(
  649. max_size=max_size, actual_size=size
  650. )
  651. else:
  652. raise ValueError(
  653. "To decode nested multipart you need to use custom reader",
  654. )
  655. else:
  656. data = await self.read()
  657. if data:
  658. charset = self.charset or "utf-8"
  659. out.extend(
  660. parse_qsl(
  661. data.rstrip().decode(charset),
  662. keep_blank_values=True,
  663. encoding=charset,
  664. )
  665. )
  666. self._post = MultiDictProxy(out)
  667. return self._post
  668. def get_extra_info(self, name: str, default: Any = None) -> Any:
  669. """Extra info from protocol transport"""
  670. protocol = self._protocol
  671. if protocol is None:
  672. return default
  673. transport = protocol.transport
  674. if transport is None:
  675. return default
  676. return transport.get_extra_info(name, default)
  677. def __repr__(self) -> str:
  678. ascii_encodable_path = self.path.encode("ascii", "backslashreplace").decode(
  679. "ascii"
  680. )
  681. return "<{} {} {} >".format(
  682. self.__class__.__name__, self._method, ascii_encodable_path
  683. )
  684. def __eq__(self, other: object) -> bool:
  685. return id(self) == id(other)
  686. def __bool__(self) -> bool:
  687. return True
  688. async def _prepare_hook(self, response: StreamResponse) -> None:
  689. return
  690. def _cancel(self, exc: BaseException) -> None:
  691. set_exception(self._payload, exc)
  692. def _finish(self) -> None:
  693. if self._post is None or self.content_type != "multipart/form-data":
  694. return
  695. # NOTE: Release file descriptors for the
  696. # NOTE: `tempfile.Temporaryfile`-created `_io.BufferedRandom`
  697. # NOTE: instances of files sent within multipart request body
  698. # NOTE: via HTTP POST request.
  699. for file_name, file_field_object in self._post.items():
  700. if isinstance(file_field_object, FileField):
  701. file_field_object.file.close()
  702. class Request(BaseRequest):
  703. ATTRS = BaseRequest.ATTRS | frozenset(["_match_info"])
  704. _match_info: Optional["UrlMappingMatchInfo"] = None
  705. if DEBUG:
  706. def __setattr__(self, name: str, val: Any) -> None:
  707. if name not in self.ATTRS:
  708. warnings.warn(
  709. "Setting custom {}.{} attribute "
  710. "is discouraged".format(self.__class__.__name__, name),
  711. DeprecationWarning,
  712. stacklevel=2,
  713. )
  714. super().__setattr__(name, val)
  715. def clone(
  716. self,
  717. *,
  718. method: Union[str, _SENTINEL] = sentinel,
  719. rel_url: Union[StrOrURL, _SENTINEL] = sentinel,
  720. headers: Union[LooseHeaders, _SENTINEL] = sentinel,
  721. scheme: Union[str, _SENTINEL] = sentinel,
  722. host: Union[str, _SENTINEL] = sentinel,
  723. remote: Union[str, _SENTINEL] = sentinel,
  724. client_max_size: Union[int, _SENTINEL] = sentinel,
  725. ) -> "Request":
  726. ret = super().clone(
  727. method=method,
  728. rel_url=rel_url,
  729. headers=headers,
  730. scheme=scheme,
  731. host=host,
  732. remote=remote,
  733. client_max_size=client_max_size,
  734. )
  735. new_ret = cast(Request, ret)
  736. new_ret._match_info = self._match_info
  737. return new_ret
  738. @reify
  739. def match_info(self) -> "UrlMappingMatchInfo":
  740. """Result of route resolving."""
  741. match_info = self._match_info
  742. assert match_info is not None
  743. return match_info
  744. @property
  745. def app(self) -> "Application":
  746. """Application instance."""
  747. match_info = self._match_info
  748. assert match_info is not None
  749. return match_info.current_app
  750. @property
  751. def config_dict(self) -> ChainMapProxy:
  752. match_info = self._match_info
  753. assert match_info is not None
  754. lst = match_info.apps
  755. app = self.app
  756. idx = lst.index(app)
  757. sublist = list(reversed(lst[: idx + 1]))
  758. return ChainMapProxy(sublist)
  759. async def _prepare_hook(self, response: StreamResponse) -> None:
  760. match_info = self._match_info
  761. if match_info is None:
  762. return
  763. for app in match_info._apps:
  764. if on_response_prepare := app.on_response_prepare:
  765. await on_response_prepare.send(self, response)