aio-libs / aiohttp
1 10
import asyncio
2 10
import dataclasses
3 10
import datetime
4 10
import io
5 10
import re
6 10
import socket
7 10
import string
8 10
import tempfile
9 10
import types
10 10
from email.utils import parsedate
11 10
from http.cookies import SimpleCookie
12 10
from types import MappingProxyType
13 10
from typing import (
14
    TYPE_CHECKING,
15
    Any,
16
    Dict,
17
    Iterator,
18
    Mapping,
19
    MutableMapping,
20
    Optional,
21
    Pattern,
22
    Set,
23
    Tuple,
24
    Union,
25
    cast,
26
)
27 10
from urllib.parse import parse_qsl
28

29 10
from multidict import CIMultiDict, CIMultiDictProxy, MultiDict, MultiDictProxy
30 10
from typing_extensions import Final
31 10
from yarl import URL
32

33 10
from . import hdrs
34 10
from .abc import AbstractStreamWriter
35 10
from .helpers import (
36
    _SENTINEL,
37
    ETAG_ANY,
38
    LIST_QUOTED_ETAG_RE,
39
    ChainMapProxy,
40
    ETag,
41
    HeadersMixin,
42
    is_expected_content_type,
43
    reify,
44
    sentinel,
45
    set_result,
46
)
47 10
from .http_parser import RawRequestMessage
48 10
from .http_writer import HttpVersion
49 10
from .multipart import BodyPartReader, MultipartReader
50 10
from .streams import EmptyStreamReader, StreamReader
51 10
from .typedefs import (
52
    DEFAULT_JSON_DECODER,
53
    JSONDecoder,
54
    LooseHeaders,
55
    RawHeaders,
56
    StrOrURL,
57
)
58 10
from .web_exceptions import (
59
    HTTPBadRequest,
60
    HTTPRequestEntityTooLarge,
61
    HTTPUnsupportedMediaType,
62
)
63 10
from .web_response import StreamResponse
64

65 10
__all__ = ("BaseRequest", "FileField", "Request")
66

67

68
if TYPE_CHECKING:  # pragma: no cover
69
    from .web_app import Application
70
    from .web_protocol import RequestHandler
71
    from .web_urldispatcher import UrlMappingMatchInfo
72

73

74 10
@dataclasses.dataclass(frozen=True)
75 7
class FileField:
76 10
    name: str
77 10
    filename: str
78 10
    file: io.BufferedReader
79 10
    content_type: str
80 10
    headers: "CIMultiDictProxy[str]"
81

82

83 10
_TCHAR: Final[str] = string.digits + string.ascii_letters + r"!#$%&'*+.^_`|~-"
84
# '-' at the end to prevent interpretation as range in a char class
85

86 10
_TOKEN: Final[str] = fr"[{_TCHAR}]+"
87

88 10
_QDTEXT: Final[str] = r"[{}]".format(
89
    r"".join(chr(c) for c in (0x09, 0x20, 0x21) + tuple(range(0x23, 0x7F)))
90
)
91
# qdtext includes 0x5C to escape 0x5D ('\]')
92
# qdtext excludes obs-text (because obsoleted, and encoding not specified)
93

94 10
_QUOTED_PAIR: Final[str] = r"\\[\t !-~]"
95

96 10
_QUOTED_STRING: Final[str] = r'"(?:{quoted_pair}|{qdtext})*"'.format(
97
    qdtext=_QDTEXT, quoted_pair=_QUOTED_PAIR
98
)
99

100 10
_FORWARDED_PAIR: Final[
101
    str
102
] = r"({token})=({token}|{quoted_string})(:\d{{1,4}})?".format(
103
    token=_TOKEN, quoted_string=_QUOTED_STRING
104
)
105

106 10
_QUOTED_PAIR_REPLACE_RE: Final[Pattern[str]] = re.compile(r"\\([\t !-~])")
107
# same pattern as _QUOTED_PAIR but contains a capture group
108

109 10
_FORWARDED_PAIR_RE: Final[Pattern[str]] = re.compile(_FORWARDED_PAIR)
110

111
############################################################
112
# HTTP Request
113
############################################################
114

115

116 10
class BaseRequest(MutableMapping[str, Any], HeadersMixin):
117

118 10
    POST_METHODS = {
119
        hdrs.METH_PATCH,
120
        hdrs.METH_POST,
121
        hdrs.METH_PUT,
122
        hdrs.METH_TRACE,
123
        hdrs.METH_DELETE,
124
    }
125

126 10
    __slots__ = (
127
        "_message",
128
        "_protocol",
129
        "_payload_writer",
130
        "_payload",
131
        "_headers",
132
        "_method",
133
        "_version",
134
        "_rel_url",
135
        "_post",
136
        "_read_bytes",
137
        "_state",
138
        "_cache",
139
        "_task",
140
        "_client_max_size",
141
        "_loop",
142
        "_transport_sslcontext",
143
        "_transport_peername",
144
        "_disconnection_waiters",
145
        "__weakref__",
146
    )
147

148 10
    def __init__(
149
        self,
150
        message: RawRequestMessage,
151
        payload: StreamReader,
152
        protocol: "RequestHandler",
153
        payload_writer: AbstractStreamWriter,
154
        task: "asyncio.Task[None]",
155
        loop: asyncio.AbstractEventLoop,
156
        *,
157
        client_max_size: int = 1024 ** 2,
158
        state: Optional[Dict[str, Any]] = None,
159
        scheme: Optional[str] = None,
160
        host: Optional[str] = None,
161
        remote: Optional[str] = None,
162
    ) -> None:
163 10
        super().__init__()
164 10
        if state is None:
165 10
            state = {}
166 10
        self._message = message
167 10
        self._protocol = protocol
168 10
        self._payload_writer = payload_writer
169

170 10
        self._payload = payload
171 10
        self._headers = message.headers
172 10
        self._method = message.method
173 10
        self._version = message.version
174 10
        self._rel_url = message.url
175 10
        self._post = (
176
            None
177
        )  # type: Optional[MultiDictProxy[Union[str, bytes, FileField]]]
178 10
        self._read_bytes = None  # type: Optional[bytes]
179

180 10
        self._state = state
181 10
        self._cache = {}  # type: Dict[str, Any]
182 10
        self._task = task
183 10
        self._client_max_size = client_max_size
184 10
        self._loop = loop
185 10
        self._disconnection_waiters = set()  # type: Set[asyncio.Future[None]]
186

187 10
        transport = self._protocol.transport
188 10
        assert transport is not None
189 10
        self._transport_sslcontext = transport.get_extra_info("sslcontext")
190 10
        self._transport_peername = transport.get_extra_info("peername")
191

192 10
        if scheme is not None:
193 10
            self._cache["scheme"] = scheme
194 10
        if host is not None:
195 10
            self._cache["host"] = host
196 10
        if remote is not None:
197 10
            self._cache["remote"] = remote
198

199 10
    def clone(
200
        self,
201
        *,
202
        method: Union[str, _SENTINEL] = sentinel,
203
        rel_url: Union[StrOrURL, _SENTINEL] = sentinel,
204
        headers: Union[LooseHeaders, _SENTINEL] = sentinel,
205
        scheme: Union[str, _SENTINEL] = sentinel,
206
        host: Union[str, _SENTINEL] = sentinel,
207
        remote: Union[str, _SENTINEL] = sentinel,
208
    ) -> "BaseRequest":
209
        """Clone itself with replacement some attributes.
210

211
        Creates and returns a new instance of Request object. If no parameters
212
        are given, an exact copy is returned. If a parameter is not passed, it
213
        will reuse the one from the current request object.
214

215
        """
216

217 10
        if self._read_bytes:
218 10
            raise RuntimeError("Cannot clone request " "after reading its content")
219

220 10
        dct = {}  # type: Dict[str, Any]
221 10
        if method is not sentinel:
222 10
            dct["method"] = method
223 10
        if rel_url is not sentinel:
224 10
            new_url: URL = URL(rel_url)  # type: ignore[arg-type]
225 10
            dct["url"] = new_url
226 10
            dct["path"] = str(new_url)
227 10
        if headers is not sentinel:
228
            # a copy semantic
229 10
            new_headers = CIMultiDictProxy(
230
                CIMultiDict(headers)  # type: ignore[arg-type]
231
            )
232 10
            dct["headers"] = new_headers
233 10
            dct["raw_headers"] = tuple(
234
                (k.encode("utf-8"), v.encode("utf-8")) for k, v in new_headers.items()
235
            )
236

237 10
        message = self._message._replace(**dct)
238

239 10
        kwargs: Dict[str, str] = {}
240 10
        if scheme is not sentinel:
241 10
            kwargs["scheme"] = scheme  # type: ignore[assignment]
242 10
        if host is not sentinel:
243 10
            kwargs["host"] = host  # type: ignore[assignment]
244 10
        if remote is not sentinel:
245 10
            kwargs["remote"] = remote  # type: ignore[assignment]
246

247 10
        return self.__class__(
248
            message,
249
            self._payload,
250
            self._protocol,
251
            self._payload_writer,
252
            self._task,
253
            self._loop,
254
            client_max_size=self._client_max_size,
255
            state=self._state.copy(),
256
            **kwargs,
257
        )
258

259 10
    @property
260 10
    def task(self) -> "asyncio.Task[None]":
261 10
        return self._task
262

263 10
    @property
264 10
    def protocol(self) -> "RequestHandler":
265 10
        return self._protocol
266

267 10
    @property
268 10
    def transport(self) -> Optional[asyncio.Transport]:
269 10
        if self._protocol is None:
270 0
            return None
271 10
        return self._protocol.transport
272

273 10
    @property
274 10
    def writer(self) -> AbstractStreamWriter:
275 10
        return self._payload_writer
276

277 10
    @reify
278 10
    def rel_url(self) -> URL:
279 10
        return self._rel_url
280

281
    # MutableMapping API
282

283 10
    def __getitem__(self, key: str) -> Any:
284 10
        return self._state[key]
285

286 10
    def __setitem__(self, key: str, value: Any) -> None:
287 10
        self._state[key] = value
288

289 10
    def __delitem__(self, key: str) -> None:
290 10
        del self._state[key]
291

292 10
    def __len__(self) -> int:
293 10
        return len(self._state)
294

295 10
    def __iter__(self) -> Iterator[str]:
296 10
        return iter(self._state)
297

298
    ########
299

300 10
    @reify
301 10
    def secure(self) -> bool:
302
        """A bool indicating if the request is handled with SSL."""
303 10
        return self.scheme == "https"
304

305 10
    @reify
306 10
    def forwarded(self) -> Tuple[Mapping[str, str], ...]:
307
        """A tuple containing all parsed Forwarded header(s).
308

309
        Makes an effort to parse Forwarded headers as specified by RFC 7239:
310

311
        - It adds one (immutable) dictionary per Forwarded 'field-value', ie
312
          per proxy. The element corresponds to the data in the Forwarded
313
          field-value added by the first proxy encountered by the client. Each
314
          subsequent item corresponds to those added by later proxies.
315
        - It checks that every value has valid syntax in general as specified
316
          in section 4: either a 'token' or a 'quoted-string'.
317
        - It un-escapes found escape sequences.
318
        - It does NOT validate 'by' and 'for' contents as specified in section
319
          6.
320
        - It does NOT validate 'host' contents (Host ABNF).
321
        - It does NOT validate 'proto' contents for valid URI scheme names.
322

323
        Returns a tuple containing one or more immutable dicts
324
        """
325 10
        elems = []
326 10
        for field_value in self._message.headers.getall(hdrs.FORWARDED, ()):
327 10
            length = len(field_value)
328 10
            pos = 0
329 10
            need_separator = False
330 10
            elem = {}  # type: Dict[str, str]
331 10
            elems.append(types.MappingProxyType(elem))
332 10
            while 0 <= pos < length:
333 10
                match = _FORWARDED_PAIR_RE.match(field_value, pos)
334 10
                if match is not None:  # got a valid forwarded-pair
335 10
                    if need_separator:
336
                        # bad syntax here, skip to next comma
337 10
                        pos = field_value.find(",", pos)
338
                    else:
339 10
                        name, value, port = match.groups()
340 10
                        if value[0] == '"':
341
                            # quoted string: remove quotes and unescape
342 10
                            value = _QUOTED_PAIR_REPLACE_RE.sub(r"\1", value[1:-1])
343 10
                        if port:
344 10
                            value += port
345 10
                        elem[name.lower()] = value
346 10
                        pos += len(match.group(0))
347 10
                        need_separator = True
348 10
                elif field_value[pos] == ",":  # next forwarded-element
349 10
                    need_separator = False
350 10
                    elem = {}
351 10
                    elems.append(types.MappingProxyType(elem))
352 10
                    pos += 1
353 10
                elif field_value[pos] == ";":  # next forwarded-pair
354 10
                    need_separator = False
355 10
                    pos += 1
356 10
                elif field_value[pos] in " \t":
357
                    # Allow whitespace even between forwarded-pairs, though
358
                    # RFC 7239 doesn't. This simplifies code and is in line
359
                    # with Postel's law.
360 10
                    pos += 1
361
                else:
362
                    # bad syntax here, skip to next comma
363 10
                    pos = field_value.find(",", pos)
364 10
        return tuple(elems)
365

366 10
    @reify
367 10
    def scheme(self) -> str:
368
        """A string representing the scheme of the request.
369

370
        Hostname is resolved in this order:
371

372
        - overridden value by .clone(scheme=new_scheme) call.
373
        - type of connection to peer: HTTPS if socket is SSL, HTTP otherwise.
374

375
        'http' or 'https'.
376
        """
377 10
        if self._transport_sslcontext:
378 10
            return "https"
379
        else:
380 10
            return "http"
381

382 10
    @reify
383 10
    def method(self) -> str:
384
        """Read only property for getting HTTP method.
385

386
        The value is upper-cased str like 'GET', 'POST', 'PUT' etc.
387
        """
388 10
        return self._method
389

390 10
    @reify
391 10
    def version(self) -> HttpVersion:
392
        """Read only property for getting HTTP version of request.
393

394
        Returns aiohttp.protocol.HttpVersion instance.
395
        """
396 10
        return self._version
397

398 10
    @reify
399 10
    def host(self) -> str:
400
        """Hostname of the request.
401

402
        Hostname is resolved in this order:
403

404
        - overridden value by .clone(host=new_host) call.
405
        - HOST HTTP header
406
        - socket.getfqdn() value
407
        """
408 10
        host = self._message.headers.get(hdrs.HOST)
409 10
        if host is not None:
410 10
            return host
411 10
        return socket.getfqdn()
412

413 10
    @reify
414 10
    def remote(self) -> Optional[str]:
415
        """Remote IP of client initiated HTTP request.
416

417
        The IP is resolved in this order:
418

419
        - overridden value by .clone(remote=new_remote) call.
420
        - peername of opened socket
421
        """
422 10
        if self._transport_peername is None:
423 3
            return None
424 10
        if isinstance(self._transport_peername, (list, tuple)):
425 10
            return str(self._transport_peername[0])
426 10
        return str(self._transport_peername)
427

428 10
    @reify
429 10
    def url(self) -> URL:
430 10
        url = URL.build(scheme=self.scheme, host=self.host)
431 10
        return url.join(self._rel_url)
432

433 10
    @reify
434 10
    def path(self) -> str:
435
        """The URL including *PATH INFO* without the host or scheme.
436

437
        E.g., ``/app/blog``
438
        """
439 10
        return self._rel_url.path
440

441 10
    @reify
442 10
    def path_qs(self) -> str:
443
        """The URL including PATH_INFO and the query string.
444

445
        E.g, /app/blog?id=10
446
        """
447 10
        return str(self._rel_url)
448

449 10
    @reify
450 10
    def raw_path(self) -> str:
451
        """The URL including raw *PATH INFO* without the host or scheme.
452
        Warning, the path is unquoted and may contains non valid URL characters
453

454
        E.g., ``/my%2Fpath%7Cwith%21some%25strange%24characters``
455
        """
456 10
        return self._message.path
457

458 10
    @reify
459 10
    def query(self) -> MultiDictProxy[str]:
460
        """A multidict with all the variables in the query string."""
461 10
        return MultiDictProxy(self._rel_url.query)
462

463 10
    @reify
464 10
    def query_string(self) -> str:
465
        """The query string in the URL.
466

467
        E.g., id=10
468
        """
469 10
        return self._rel_url.query_string
470

471 10
    @reify
472 10
    def headers(self) -> "CIMultiDictProxy[str]":
473
        """A case-insensitive multidict proxy with all headers."""
474 10
        return self._headers
475

476 10
    @reify
477 10
    def raw_headers(self) -> RawHeaders:
478
        """A sequence of pairs for all headers."""
479 10
        return self._message.raw_headers
480

481 10
    @staticmethod
482 10
    def _http_date(_date_str: Optional[str]) -> Optional[datetime.datetime]:
483
        """Process a date string, return a datetime object"""
484 10
        if _date_str is not None:
485 10
            timetuple = parsedate(_date_str)
486 10
            if timetuple is not None:
487 10
                return datetime.datetime(*timetuple[:6], tzinfo=datetime.timezone.utc)
488 10
        return None
489

490 10
    @reify
491 10
    def if_modified_since(self) -> Optional[datetime.datetime]:
492
        """The value of If-Modified-Since HTTP header, or None.
493

494
        This header is represented as a `datetime` object.
495
        """
496 10
        return self._http_date(self.headers.get(hdrs.IF_MODIFIED_SINCE))
497

498 10
    @reify
499 10
    def if_unmodified_since(self) -> Optional[datetime.datetime]:
500
        """The value of If-Unmodified-Since HTTP header, or None.
501

502
        This header is represented as a `datetime` object.
503
        """
504 10
        return self._http_date(self.headers.get(hdrs.IF_UNMODIFIED_SINCE))
505

506 10
    @staticmethod
507 10
    def _etag_values(etag_header: str) -> Iterator[ETag]:
508
        """Extract `ETag` objects from raw header."""
509 10
        if etag_header == ETAG_ANY:
510 10
            yield ETag(
511
                is_weak=False,
512
                value=ETAG_ANY,
513
            )
514
        else:
515 10
            for match in LIST_QUOTED_ETAG_RE.finditer(etag_header):
516 10
                is_weak, value, garbage = match.group(2, 3, 4)
517
                # Any symbol captured by 4th group means
518
                # that the following sequence is invalid.
519 10
                if garbage:
520 10
                    break
521

522 10
                yield ETag(
523
                    is_weak=bool(is_weak),
524
                    value=value,
525
                )
526

527 10
    @classmethod
528 10
    def _if_match_or_none_impl(
529
        cls, header_value: Optional[str]
530
    ) -> Optional[Tuple[ETag, ...]]:
531 10
        if not header_value:
532 10
            return None
533

534 10
        return tuple(cls._etag_values(header_value))
535

536 10
    @reify
537 10
    def if_match(self) -> Optional[Tuple[ETag, ...]]:
538
        """The value of If-Match HTTP header, or None.
539

540
        This header is represented as a `tuple` of `ETag` objects.
541
        """
542 10
        return self._if_match_or_none_impl(self.headers.get(hdrs.IF_MATCH))
543

544 10
    @reify
545 10
    def if_none_match(self) -> Optional[Tuple[ETag, ...]]:
546
        """The value of If-None-Match HTTP header, or None.
547

548
        This header is represented as a `tuple` of `ETag` objects.
549
        """
550 10
        return self._if_match_or_none_impl(self.headers.get(hdrs.IF_NONE_MATCH))
551

552 10
    @reify
553 10
    def if_range(self) -> Optional[datetime.datetime]:
554
        """The value of If-Range HTTP header, or None.
555

556
        This header is represented as a `datetime` object.
557
        """
558 10
        return self._http_date(self.headers.get(hdrs.IF_RANGE))
559

560 10
    @reify
561 10
    def keep_alive(self) -> bool:
562
        """Is keepalive enabled by client?"""
563 10
        return not self._message.should_close
564

565 10
    @reify
566 10
    def cookies(self) -> Mapping[str, str]:
567
        """Return request cookies.
568

569
        A read-only dictionary-like object.
570
        """
571 10
        raw = self.headers.get(hdrs.COOKIE, "")
572 10
        parsed = SimpleCookie(raw)  # type: SimpleCookie[str]
573 10
        return MappingProxyType({key: val.value for key, val in parsed.items()})
574

575 10
    @reify
576 10
    def http_range(self) -> slice:
577
        """The content of Range HTTP header.
578

579
        Return a slice instance.
580

581
        """
582 10
        rng = self._headers.get(hdrs.RANGE)
583 10
        start, end = None, None
584 10
        if rng is not None:
585 10
            try:
586 10
                pattern = r"^bytes=(\d*)-(\d*)$"
587 10
                start, end = re.findall(pattern, rng)[0]
588 10
            except IndexError:  # pattern was not found in header
589 10
                raise ValueError("range not in acceptable format")
590

591 10
            end = int(end) if end else None
592 10
            start = int(start) if start else None
593

594 10
            if start is None and end is not None:
595
                # end with no start is to return tail of content
596 10
                start = -end
597 10
                end = None
598

599 10
            if start is not None and end is not None:
600
                # end is inclusive in range header, exclusive for slice
601 10
                end += 1
602

603 10
                if start >= end:
604 10
                    raise ValueError("start cannot be after end")
605

606 10
            if start is end is None:  # No valid range supplied
607 10
                raise ValueError("No start or end of range specified")
608

609 10
        return slice(start, end, 1)
610

611 10
    @reify
612 10
    def content(self) -> StreamReader:
613
        """Return raw payload stream."""
614 10
        return self._payload
615

616 10
    @property
617 10
    def can_read_body(self) -> bool:
618
        """Return True if request's HTTP BODY can be read, False otherwise."""
619 10
        return not self._payload.at_eof()
620

621 10
    @reify
622 10
    def body_exists(self) -> bool:
623
        """Return True if request has HTTP BODY, False otherwise."""
624 10
        return type(self._payload) is not EmptyStreamReader
625

626 10
    async def release(self) -> None:
627
        """Release request.
628

629
        Eat unread part of HTTP BODY if present.
630
        """
631 10
        while not self._payload.at_eof():
632 10
            await self._payload.readany()
633

634 10
    async def read(self) -> bytes:
635
        """Read request body if present.
636

637
        Returns bytes object with full request content.
638
        """
639 10
        if self._read_bytes is None:
640 10
            body = bytearray()
641 3
            while True:
642 10
                chunk = await self._payload.readany()
643 10
                body.extend(chunk)
644 10
                if self._client_max_size:
645 10
                    body_size = len(body)
646 10
                    if body_size > self._client_max_size:
647 10
                        raise HTTPRequestEntityTooLarge(
648
                            max_size=self._client_max_size, actual_size=body_size
649
                        )
650 10
                if not chunk:
651 10
                    break
652 10
            self._read_bytes = bytes(body)
653 10
        return self._read_bytes
654

655 10
    async def text(self) -> str:
656
        """Return BODY as text using encoding from .charset."""
657 10
        bytes_body = await self.read()
658 10
        encoding = self.charset or "utf-8"
659 10
        try:
660 10
            return bytes_body.decode(encoding)
661 10
        except LookupError:
662 10
            raise HTTPUnsupportedMediaType()
663

664 10
    async def json(
665
        self,
666
        *,
667
        loads: JSONDecoder = DEFAULT_JSON_DECODER,
668
        content_type: Optional[str] = "application/json",
669
    ) -> Any:
670
        """Return BODY as JSON."""
671 10
        body = await self.text()
672 10
        if content_type:
673 10
            ctype = self.headers.get(hdrs.CONTENT_TYPE, "").lower()
674 10
            if not is_expected_content_type(ctype, content_type):
675 10
                raise HTTPBadRequest(
676
                    text=(
677
                        "Attempt to decode JSON with " "unexpected mimetype: %s" % ctype
678
                    )
679
                )
680

681 10
        return loads(body)
682

683 10
    async def multipart(self) -> MultipartReader:
684
        """Return async iterator to process BODY as multipart."""
685 10
        return MultipartReader(self._headers, self._payload)
686

687 10
    async def post(self) -> "MultiDictProxy[Union[str, bytes, FileField]]":
688
        """Return POST parameters."""
689 10
        if self._post is not None:
690 10
            return self._post
691 10
        if self._method not in self.POST_METHODS:
692 10
            self._post = MultiDictProxy(MultiDict())
693 10
            return self._post
694

695 10
        content_type = self.content_type
696 10
        if content_type not in (
697
            "",
698
            "application/x-www-form-urlencoded",
699
            "multipart/form-data",
700
        ):
701 10
            self._post = MultiDictProxy(MultiDict())
702 10
            return self._post
703

704 10
        out = MultiDict()  # type: MultiDict[Union[str, bytes, FileField]]
705

706 10
        if content_type == "multipart/form-data":
707 10
            multipart = await self.multipart()
708 10
            max_size = self._client_max_size
709

710 10
            field = await multipart.next()
711 10
            while field is not None:
712 10
                size = 0
713 10
                field_ct = field.headers.get(hdrs.CONTENT_TYPE)
714

715 10
                if isinstance(field, BodyPartReader):
716 10
                    assert field.name is not None
717

718
                    # Note that according to RFC 7578, the Content-Type header
719
                    # is optional, even for files, so we can't assume it's
720
                    # present.
721
                    # https://tools.ietf.org/html/rfc7578#section-4.4
722 10
                    if field.filename:
723
                        # store file in temp file
724 10
                        tmp = tempfile.TemporaryFile()
725 10
                        chunk = await field.read_chunk(size=2 ** 16)
726 10
                        while chunk:
727 10
                            chunk = field.decode(chunk)
728 10
                            tmp.write(chunk)
729 10
                            size += len(chunk)
730 10
                            if 0 < max_size < size:
731 10
                                tmp.close()
732 10
                                raise HTTPRequestEntityTooLarge(
733
                                    max_size=max_size, actual_size=size
734
                                )
735 10
                            chunk = await field.read_chunk(size=2 ** 16)
736 10
                        tmp.seek(0)
737

738 10
                        if field_ct is None:
739 10
                            field_ct = "application/octet-stream"
740

741 10
                        ff = FileField(
742
                            field.name,
743
                            field.filename,
744
                            cast(io.BufferedReader, tmp),
745
                            field_ct,
746
                            field.headers,
747
                        )
748 10
                        out.add(field.name, ff)
749
                    else:
750
                        # deal with ordinary data
751 10
                        value = await field.read(decode=True)
752 10
                        if field_ct is None or field_ct.startswith("text/"):
753 10
                            charset = field.get_charset(default="utf-8")
754 10
                            out.add(field.name, value.decode(charset))
755
                        else:
756 10
                            out.add(field.name, value)
757 10
                        size += len(value)
758 10
                        if 0 < max_size < size:
759 10
                            raise HTTPRequestEntityTooLarge(
760
                                max_size=max_size, actual_size=size
761
                            )
762
                else:
763 0
                    raise ValueError(
764
                        "To decode nested multipart you need " "to use custom reader",
765
                    )
766

767 10
                field = await multipart.next()
768
        else:
769 10
            data = await self.read()
770 10
            if data:
771 10
                charset = self.charset or "utf-8"
772 10
                bytes_query = data.rstrip()
773 10
                try:
774 10
                    query = bytes_query.decode(charset)
775 0
                except LookupError:
776 0
                    raise HTTPUnsupportedMediaType()
777 10
                out.extend(
778
                    parse_qsl(qs=query, keep_blank_values=True, encoding=charset)
779
                )
780

781 10
        self._post = MultiDictProxy(out)
782 10
        return self._post
783

784 10
    def get_extra_info(self, name: str, default: Any = None) -> Any:
785
        """Extra info from protocol transport"""
786 10
        protocol = self._protocol
787 10
        if protocol is None:
788 10
            return default
789

790 10
        transport = protocol.transport
791 10
        if transport is None:
792 10
            return default
793

794 10
        return transport.get_extra_info(name, default)
795

796 10
    def __repr__(self) -> str:
797 10
        ascii_encodable_path = self.path.encode("ascii", "backslashreplace").decode(
798
            "ascii"
799
        )
800 10
        return "<{} {} {} >".format(
801
            self.__class__.__name__, self._method, ascii_encodable_path
802
        )
803

804 10
    def __eq__(self, other: object) -> bool:
805 10
        return id(self) == id(other)
806

807 10
    def __bool__(self) -> bool:
808 10
        return True
809

810 10
    async def _prepare_hook(self, response: StreamResponse) -> None:
811 10
        return
812

813 10
    def _cancel(self, exc: BaseException) -> None:
814 10
        self._payload.set_exception(exc)
815 10
        for fut in self._disconnection_waiters:
816 0
            set_result(fut, None)
817

818 10
    def _finish(self) -> None:
819 10
        for fut in self._disconnection_waiters:
820 0
            fut.cancel()
821

822 10
        if self._post is None or self.content_type != "multipart/form-data":
823 10
            return
824

825
        # NOTE: Release file descriptors for the
826
        # NOTE: `tempfile.Temporaryfile`-created `_io.BufferedRandom`
827
        # NOTE: instances of files sent within multipart request body
828
        # NOTE: via HTTP POST request.
829 10
        for file_name, file_field_object in self._post.items():
830 10
            if not isinstance(file_field_object, FileField):
831 10
                continue
832

833 10
            file_field_object.file.close()
834

835 10
    async def wait_for_disconnection(self) -> None:
836 0
        loop = asyncio.get_event_loop()
837 0
        fut = loop.create_future()  # type: asyncio.Future[None]
838 0
        self._disconnection_waiters.add(fut)
839 0
        try:
840 0
            await fut
841
        finally:
842 0
            self._disconnection_waiters.remove(fut)
843

844

845 10
class Request(BaseRequest):
846

847 10
    __slots__ = ("_match_info",)
848

849 10
    def __init__(self, *args: Any, **kwargs: Any) -> None:
850 10
        super().__init__(*args, **kwargs)
851

852
        # matchdict, route_name, handler
853
        # or information about traversal lookup
854

855
        # initialized after route resolving
856 10
        self._match_info = None  # type: Optional[UrlMappingMatchInfo]
857

858 10
    def clone(
859
        self,
860
        *,
861
        method: Union[str, _SENTINEL] = sentinel,
862
        rel_url: Union[StrOrURL, _SENTINEL] = sentinel,
863
        headers: Union[LooseHeaders, _SENTINEL] = sentinel,
864
        scheme: Union[str, _SENTINEL] = sentinel,
865
        host: Union[str, _SENTINEL] = sentinel,
866
        remote: Union[str, _SENTINEL] = sentinel,
867
    ) -> "Request":
868 10
        ret = super().clone(
869
            method=method,
870
            rel_url=rel_url,
871
            headers=headers,
872
            scheme=scheme,
873
            host=host,
874
            remote=remote,
875
        )
876 10
        new_ret = cast(Request, ret)
877 10
        new_ret._match_info = self._match_info
878 10
        return new_ret
879

880 10
    @reify
881 10
    def match_info(self) -> "UrlMappingMatchInfo":
882
        """Result of route resolving."""
883 10
        match_info = self._match_info
884 10
        assert match_info is not None
885 10
        return match_info
886

887 10
    @property
888 10
    def app(self) -> "Application":
889
        """Application instance."""
890 10
        match_info = self._match_info
891 10
        assert match_info is not None
892 10
        return match_info.current_app
893

894 10
    @property
895 10
    def config_dict(self) -> ChainMapProxy:
896 10
        match_info = self._match_info
897 10
        assert match_info is not None
898 10
        lst = match_info.apps
899 10
        app = self.app
900 10
        idx = lst.index(app)
901 10
        sublist = list(reversed(lst[: idx + 1]))
902 10
        return ChainMapProxy(sublist)
903

904 10
    async def _prepare_hook(self, response: StreamResponse) -> None:
905 10
        match_info = self._match_info
906 10
        if match_info is None:
907 10
            return
908 10
        for app in match_info._apps:
909 10
            await app.on_response_prepare.send(self, response)

Read our documentation on viewing source code .

Loading