web_request.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916
  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 http.cookies import SimpleCookie
  11. from types import MappingProxyType
  12. from typing import (
  13. TYPE_CHECKING,
  14. Any,
  15. Dict,
  16. Final,
  17. Iterator,
  18. Mapping,
  19. MutableMapping,
  20. Optional,
  21. Pattern,
  22. Tuple,
  23. Union,
  24. cast,
  25. )
  26. from urllib.parse import parse_qsl
  27. import attr
  28. from multidict import (
  29. CIMultiDict,
  30. CIMultiDictProxy,
  31. MultiDict,
  32. MultiDictProxy,
  33. MultiMapping,
  34. )
  35. from yarl import URL
  36. from . import hdrs
  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. transport = protocol.transport
  177. assert transport is not None
  178. self._transport_sslcontext = transport.get_extra_info("sslcontext")
  179. self._transport_peername = transport.get_extra_info("peername")
  180. if remote is not None:
  181. self._cache["remote"] = remote
  182. def clone(
  183. self,
  184. *,
  185. method: Union[str, _SENTINEL] = sentinel,
  186. rel_url: Union[StrOrURL, _SENTINEL] = sentinel,
  187. headers: Union[LooseHeaders, _SENTINEL] = sentinel,
  188. scheme: Union[str, _SENTINEL] = sentinel,
  189. host: Union[str, _SENTINEL] = sentinel,
  190. remote: Union[str, _SENTINEL] = sentinel,
  191. client_max_size: Union[int, _SENTINEL] = sentinel,
  192. ) -> "BaseRequest":
  193. """Clone itself with replacement some attributes.
  194. Creates and returns a new instance of Request object. If no parameters
  195. are given, an exact copy is returned. If a parameter is not passed, it
  196. will reuse the one from the current request object.
  197. """
  198. if self._read_bytes:
  199. raise RuntimeError("Cannot clone request after reading its content")
  200. dct: Dict[str, Any] = {}
  201. if method is not sentinel:
  202. dct["method"] = method
  203. if rel_url is not sentinel:
  204. new_url: URL = URL(rel_url)
  205. dct["url"] = new_url
  206. dct["path"] = str(new_url)
  207. if headers is not sentinel:
  208. # a copy semantic
  209. dct["headers"] = CIMultiDictProxy(CIMultiDict(headers))
  210. dct["raw_headers"] = tuple(
  211. (k.encode("utf-8"), v.encode("utf-8"))
  212. for k, v in dct["headers"].items()
  213. )
  214. message = self._message._replace(**dct)
  215. kwargs = {}
  216. if scheme is not sentinel:
  217. kwargs["scheme"] = scheme
  218. if host is not sentinel:
  219. kwargs["host"] = host
  220. if remote is not sentinel:
  221. kwargs["remote"] = remote
  222. if client_max_size is sentinel:
  223. client_max_size = self._client_max_size
  224. return self.__class__(
  225. message,
  226. self._payload,
  227. self._protocol,
  228. self._payload_writer,
  229. self._task,
  230. self._loop,
  231. client_max_size=client_max_size,
  232. state=self._state.copy(),
  233. **kwargs,
  234. )
  235. @property
  236. def task(self) -> "asyncio.Task[None]":
  237. return self._task
  238. @property
  239. def protocol(self) -> "RequestHandler":
  240. return self._protocol
  241. @property
  242. def transport(self) -> Optional[asyncio.Transport]:
  243. if self._protocol is None:
  244. return None
  245. return self._protocol.transport
  246. @property
  247. def writer(self) -> AbstractStreamWriter:
  248. return self._payload_writer
  249. @property
  250. def client_max_size(self) -> int:
  251. return self._client_max_size
  252. @reify
  253. def message(self) -> RawRequestMessage:
  254. warnings.warn("Request.message is deprecated", DeprecationWarning, stacklevel=3)
  255. return self._message
  256. @reify
  257. def rel_url(self) -> URL:
  258. return self._rel_url
  259. @reify
  260. def loop(self) -> asyncio.AbstractEventLoop:
  261. warnings.warn(
  262. "request.loop property is deprecated", DeprecationWarning, stacklevel=2
  263. )
  264. return self._loop
  265. # MutableMapping API
  266. def __getitem__(self, key: str) -> Any:
  267. return self._state[key]
  268. def __setitem__(self, key: str, value: Any) -> None:
  269. self._state[key] = value
  270. def __delitem__(self, key: str) -> None:
  271. del self._state[key]
  272. def __len__(self) -> int:
  273. return len(self._state)
  274. def __iter__(self) -> Iterator[str]:
  275. return iter(self._state)
  276. ########
  277. @reify
  278. def secure(self) -> bool:
  279. """A bool indicating if the request is handled with SSL."""
  280. return self.scheme == "https"
  281. @reify
  282. def forwarded(self) -> Tuple[Mapping[str, str], ...]:
  283. """A tuple containing all parsed Forwarded header(s).
  284. Makes an effort to parse Forwarded headers as specified by RFC 7239:
  285. - It adds one (immutable) dictionary per Forwarded 'field-value', ie
  286. per proxy. The element corresponds to the data in the Forwarded
  287. field-value added by the first proxy encountered by the client. Each
  288. subsequent item corresponds to those added by later proxies.
  289. - It checks that every value has valid syntax in general as specified
  290. in section 4: either a 'token' or a 'quoted-string'.
  291. - It un-escapes found escape sequences.
  292. - It does NOT validate 'by' and 'for' contents as specified in section
  293. 6.
  294. - It does NOT validate 'host' contents (Host ABNF).
  295. - It does NOT validate 'proto' contents for valid URI scheme names.
  296. Returns a tuple containing one or more immutable dicts
  297. """
  298. elems = []
  299. for field_value in self._message.headers.getall(hdrs.FORWARDED, ()):
  300. length = len(field_value)
  301. pos = 0
  302. need_separator = False
  303. elem: Dict[str, str] = {}
  304. elems.append(types.MappingProxyType(elem))
  305. while 0 <= pos < length:
  306. match = _FORWARDED_PAIR_RE.match(field_value, pos)
  307. if match is not None: # got a valid forwarded-pair
  308. if need_separator:
  309. # bad syntax here, skip to next comma
  310. pos = field_value.find(",", pos)
  311. else:
  312. name, value, port = match.groups()
  313. if value[0] == '"':
  314. # quoted string: remove quotes and unescape
  315. value = _QUOTED_PAIR_REPLACE_RE.sub(r"\1", value[1:-1])
  316. if port:
  317. value += port
  318. elem[name.lower()] = value
  319. pos += len(match.group(0))
  320. need_separator = True
  321. elif field_value[pos] == ",": # next forwarded-element
  322. need_separator = False
  323. elem = {}
  324. elems.append(types.MappingProxyType(elem))
  325. pos += 1
  326. elif field_value[pos] == ";": # next forwarded-pair
  327. need_separator = False
  328. pos += 1
  329. elif field_value[pos] in " \t":
  330. # Allow whitespace even between forwarded-pairs, though
  331. # RFC 7239 doesn't. This simplifies code and is in line
  332. # with Postel's law.
  333. pos += 1
  334. else:
  335. # bad syntax here, skip to next comma
  336. pos = field_value.find(",", pos)
  337. return tuple(elems)
  338. @reify
  339. def scheme(self) -> str:
  340. """A string representing the scheme of the request.
  341. Hostname is resolved in this order:
  342. - overridden value by .clone(scheme=new_scheme) call.
  343. - type of connection to peer: HTTPS if socket is SSL, HTTP otherwise.
  344. 'http' or 'https'.
  345. """
  346. if self._transport_sslcontext:
  347. return "https"
  348. else:
  349. return "http"
  350. @reify
  351. def method(self) -> str:
  352. """Read only property for getting HTTP method.
  353. The value is upper-cased str like 'GET', 'POST', 'PUT' etc.
  354. """
  355. return self._method
  356. @reify
  357. def version(self) -> HttpVersion:
  358. """Read only property for getting HTTP version of request.
  359. Returns aiohttp.protocol.HttpVersion instance.
  360. """
  361. return self._version
  362. @reify
  363. def host(self) -> str:
  364. """Hostname of the request.
  365. Hostname is resolved in this order:
  366. - overridden value by .clone(host=new_host) call.
  367. - HOST HTTP header
  368. - socket.getfqdn() value
  369. For example, 'example.com' or 'localhost:8080'.
  370. For historical reasons, the port number may be included.
  371. """
  372. host = self._message.headers.get(hdrs.HOST)
  373. if host is not None:
  374. return host
  375. return socket.getfqdn()
  376. @reify
  377. def remote(self) -> Optional[str]:
  378. """Remote IP of client initiated HTTP request.
  379. The IP is resolved in this order:
  380. - overridden value by .clone(remote=new_remote) call.
  381. - peername of opened socket
  382. """
  383. if self._transport_peername is None:
  384. return None
  385. if isinstance(self._transport_peername, (list, tuple)):
  386. return str(self._transport_peername[0])
  387. return str(self._transport_peername)
  388. @reify
  389. def url(self) -> URL:
  390. """The full URL of the request."""
  391. # authority is used here because it may include the port number
  392. # and we want yarl to parse it correctly
  393. return URL.build(scheme=self.scheme, authority=self.host).join(self._rel_url)
  394. @reify
  395. def path(self) -> str:
  396. """The URL including *PATH INFO* without the host or scheme.
  397. E.g., ``/app/blog``
  398. """
  399. return self._rel_url.path
  400. @reify
  401. def path_qs(self) -> str:
  402. """The URL including PATH_INFO and the query string.
  403. E.g, /app/blog?id=10
  404. """
  405. return str(self._rel_url)
  406. @reify
  407. def raw_path(self) -> str:
  408. """The URL including raw *PATH INFO* without the host or scheme.
  409. Warning, the path is unquoted and may contains non valid URL characters
  410. E.g., ``/my%2Fpath%7Cwith%21some%25strange%24characters``
  411. """
  412. return self._message.path
  413. @reify
  414. def query(self) -> "MultiMapping[str]":
  415. """A multidict with all the variables in the query string."""
  416. return self._rel_url.query
  417. @reify
  418. def query_string(self) -> str:
  419. """The query string in the URL.
  420. E.g., id=10
  421. """
  422. return self._rel_url.query_string
  423. @reify
  424. def headers(self) -> CIMultiDictProxy[str]:
  425. """A case-insensitive multidict proxy with all headers."""
  426. return self._headers
  427. @reify
  428. def raw_headers(self) -> RawHeaders:
  429. """A sequence of pairs for all headers."""
  430. return self._message.raw_headers
  431. @reify
  432. def if_modified_since(self) -> Optional[datetime.datetime]:
  433. """The value of If-Modified-Since HTTP header, or None.
  434. This header is represented as a `datetime` object.
  435. """
  436. return parse_http_date(self.headers.get(hdrs.IF_MODIFIED_SINCE))
  437. @reify
  438. def if_unmodified_since(self) -> Optional[datetime.datetime]:
  439. """The value of If-Unmodified-Since HTTP header, or None.
  440. This header is represented as a `datetime` object.
  441. """
  442. return parse_http_date(self.headers.get(hdrs.IF_UNMODIFIED_SINCE))
  443. @staticmethod
  444. def _etag_values(etag_header: str) -> Iterator[ETag]:
  445. """Extract `ETag` objects from raw header."""
  446. if etag_header == ETAG_ANY:
  447. yield ETag(
  448. is_weak=False,
  449. value=ETAG_ANY,
  450. )
  451. else:
  452. for match in LIST_QUOTED_ETAG_RE.finditer(etag_header):
  453. is_weak, value, garbage = match.group(2, 3, 4)
  454. # Any symbol captured by 4th group means
  455. # that the following sequence is invalid.
  456. if garbage:
  457. break
  458. yield ETag(
  459. is_weak=bool(is_weak),
  460. value=value,
  461. )
  462. @classmethod
  463. def _if_match_or_none_impl(
  464. cls, header_value: Optional[str]
  465. ) -> Optional[Tuple[ETag, ...]]:
  466. if not header_value:
  467. return None
  468. return tuple(cls._etag_values(header_value))
  469. @reify
  470. def if_match(self) -> Optional[Tuple[ETag, ...]]:
  471. """The value of If-Match HTTP header, or None.
  472. This header is represented as a `tuple` of `ETag` objects.
  473. """
  474. return self._if_match_or_none_impl(self.headers.get(hdrs.IF_MATCH))
  475. @reify
  476. def if_none_match(self) -> Optional[Tuple[ETag, ...]]:
  477. """The value of If-None-Match HTTP header, or None.
  478. This header is represented as a `tuple` of `ETag` objects.
  479. """
  480. return self._if_match_or_none_impl(self.headers.get(hdrs.IF_NONE_MATCH))
  481. @reify
  482. def if_range(self) -> Optional[datetime.datetime]:
  483. """The value of If-Range HTTP header, or None.
  484. This header is represented as a `datetime` object.
  485. """
  486. return parse_http_date(self.headers.get(hdrs.IF_RANGE))
  487. @reify
  488. def keep_alive(self) -> bool:
  489. """Is keepalive enabled by client?"""
  490. return not self._message.should_close
  491. @reify
  492. def cookies(self) -> Mapping[str, str]:
  493. """Return request cookies.
  494. A read-only dictionary-like object.
  495. """
  496. raw = self.headers.get(hdrs.COOKIE, "")
  497. parsed = SimpleCookie(raw)
  498. return MappingProxyType({key: val.value for key, val in parsed.items()})
  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)[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. field = await multipart.next()
  602. while field is not None:
  603. size = 0
  604. field_ct = field.headers.get(hdrs.CONTENT_TYPE)
  605. if isinstance(field, BodyPartReader):
  606. assert field.name is not None
  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 = 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. field = await multipart.next()
  656. else:
  657. data = await self.read()
  658. if data:
  659. charset = self.charset or "utf-8"
  660. out.extend(
  661. parse_qsl(
  662. data.rstrip().decode(charset),
  663. keep_blank_values=True,
  664. encoding=charset,
  665. )
  666. )
  667. self._post = MultiDictProxy(out)
  668. return self._post
  669. def get_extra_info(self, name: str, default: Any = None) -> Any:
  670. """Extra info from protocol transport"""
  671. protocol = self._protocol
  672. if protocol is None:
  673. return default
  674. transport = protocol.transport
  675. if transport is None:
  676. return default
  677. return transport.get_extra_info(name, default)
  678. def __repr__(self) -> str:
  679. ascii_encodable_path = self.path.encode("ascii", "backslashreplace").decode(
  680. "ascii"
  681. )
  682. return "<{} {} {} >".format(
  683. self.__class__.__name__, self._method, ascii_encodable_path
  684. )
  685. def __eq__(self, other: object) -> bool:
  686. return id(self) == id(other)
  687. def __bool__(self) -> bool:
  688. return True
  689. async def _prepare_hook(self, response: StreamResponse) -> None:
  690. return
  691. def _cancel(self, exc: BaseException) -> None:
  692. set_exception(self._payload, exc)
  693. def _finish(self) -> None:
  694. if self._post is None or self.content_type != "multipart/form-data":
  695. return
  696. # NOTE: Release file descriptors for the
  697. # NOTE: `tempfile.Temporaryfile`-created `_io.BufferedRandom`
  698. # NOTE: instances of files sent within multipart request body
  699. # NOTE: via HTTP POST request.
  700. for file_name, file_field_object in self._post.items():
  701. if isinstance(file_field_object, FileField):
  702. file_field_object.file.close()
  703. class Request(BaseRequest):
  704. ATTRS = BaseRequest.ATTRS | frozenset(["_match_info"])
  705. _match_info: Optional["UrlMappingMatchInfo"] = None
  706. if DEBUG:
  707. def __setattr__(self, name: str, val: Any) -> None:
  708. if name not in self.ATTRS:
  709. warnings.warn(
  710. "Setting custom {}.{} attribute "
  711. "is discouraged".format(self.__class__.__name__, name),
  712. DeprecationWarning,
  713. stacklevel=2,
  714. )
  715. super().__setattr__(name, val)
  716. def clone(
  717. self,
  718. *,
  719. method: Union[str, _SENTINEL] = sentinel,
  720. rel_url: Union[StrOrURL, _SENTINEL] = sentinel,
  721. headers: Union[LooseHeaders, _SENTINEL] = sentinel,
  722. scheme: Union[str, _SENTINEL] = sentinel,
  723. host: Union[str, _SENTINEL] = sentinel,
  724. remote: Union[str, _SENTINEL] = sentinel,
  725. client_max_size: Union[int, _SENTINEL] = sentinel,
  726. ) -> "Request":
  727. ret = super().clone(
  728. method=method,
  729. rel_url=rel_url,
  730. headers=headers,
  731. scheme=scheme,
  732. host=host,
  733. remote=remote,
  734. client_max_size=client_max_size,
  735. )
  736. new_ret = cast(Request, ret)
  737. new_ret._match_info = self._match_info
  738. return new_ret
  739. @reify
  740. def match_info(self) -> "UrlMappingMatchInfo":
  741. """Result of route resolving."""
  742. match_info = self._match_info
  743. assert match_info is not None
  744. return match_info
  745. @property
  746. def app(self) -> "Application":
  747. """Application instance."""
  748. match_info = self._match_info
  749. assert match_info is not None
  750. return match_info.current_app
  751. @property
  752. def config_dict(self) -> ChainMapProxy:
  753. match_info = self._match_info
  754. assert match_info is not None
  755. lst = match_info.apps
  756. app = self.app
  757. idx = lst.index(app)
  758. sublist = list(reversed(lst[: idx + 1]))
  759. return ChainMapProxy(sublist)
  760. async def _prepare_hook(self, response: StreamResponse) -> None:
  761. match_info = self._match_info
  762. if match_info is None:
  763. return
  764. for app in match_info._apps:
  765. if on_response_prepare := app.on_response_prepare:
  766. await on_response_prepare.send(self, response)