Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/werkzeug/sansio/request.py: 73%

202 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-09 06:08 +0000

1from __future__ import annotations 

2 

3import typing as t 

4import warnings 

5from datetime import datetime 

6from urllib.parse import parse_qsl 

7 

8from ..datastructures import Accept 

9from ..datastructures import Authorization 

10from ..datastructures import CharsetAccept 

11from ..datastructures import ETags 

12from ..datastructures import Headers 

13from ..datastructures import HeaderSet 

14from ..datastructures import IfRange 

15from ..datastructures import ImmutableList 

16from ..datastructures import ImmutableMultiDict 

17from ..datastructures import LanguageAccept 

18from ..datastructures import MIMEAccept 

19from ..datastructures import MultiDict 

20from ..datastructures import Range 

21from ..datastructures import RequestCacheControl 

22from ..http import parse_accept_header 

23from ..http import parse_cache_control_header 

24from ..http import parse_date 

25from ..http import parse_etags 

26from ..http import parse_if_range_header 

27from ..http import parse_list_header 

28from ..http import parse_options_header 

29from ..http import parse_range_header 

30from ..http import parse_set_header 

31from ..user_agent import UserAgent 

32from ..utils import cached_property 

33from ..utils import header_property 

34from .http import parse_cookie 

35from .utils import get_content_length 

36from .utils import get_current_url 

37from .utils import get_host 

38 

39 

40class Request: 

41 """Represents the non-IO parts of a HTTP request, including the 

42 method, URL info, and headers. 

43 

44 This class is not meant for general use. It should only be used when 

45 implementing WSGI, ASGI, or another HTTP application spec. Werkzeug 

46 provides a WSGI implementation at :cls:`werkzeug.wrappers.Request`. 

47 

48 :param method: The method the request was made with, such as 

49 ``GET``. 

50 :param scheme: The URL scheme of the protocol the request used, such 

51 as ``https`` or ``wss``. 

52 :param server: The address of the server. ``(host, port)``, 

53 ``(path, None)`` for unix sockets, or ``None`` if not known. 

54 :param root_path: The prefix that the application is mounted under. 

55 This is prepended to generated URLs, but is not part of route 

56 matching. 

57 :param path: The path part of the URL after ``root_path``. 

58 :param query_string: The part of the URL after the "?". 

59 :param headers: The headers received with the request. 

60 :param remote_addr: The address of the client sending the request. 

61 

62 .. versionadded:: 2.0 

63 """ 

64 

65 _charset: str 

66 

67 @property 

68 def charset(self) -> str: 

69 """The charset used to decode body, form, and cookie data. Defaults to UTF-8. 

70 

71 .. deprecated:: 2.3 

72 Will be removed in Werkzeug 3.0. Request data must always be UTF-8. 

73 """ 

74 warnings.warn( 

75 "The 'charset' attribute is deprecated and will not be used in Werkzeug" 

76 " 2.4. Interpreting bytes as text in body, form, and cookie data will" 

77 " always use UTF-8.", 

78 DeprecationWarning, 

79 stacklevel=2, 

80 ) 

81 return self._charset 

82 

83 @charset.setter 

84 def charset(self, value: str) -> None: 

85 warnings.warn( 

86 "The 'charset' attribute is deprecated and will not be used in Werkzeug" 

87 " 2.4. Interpreting bytes as text in body, form, and cookie data will" 

88 " always use UTF-8.", 

89 DeprecationWarning, 

90 stacklevel=2, 

91 ) 

92 self._charset = value 

93 

94 _encoding_errors: str 

95 

96 @property 

97 def encoding_errors(self) -> str: 

98 """How errors when decoding bytes are handled. Defaults to "replace". 

99 

100 .. deprecated:: 2.3 

101 Will be removed in Werkzeug 3.0. 

102 """ 

103 warnings.warn( 

104 "The 'encoding_errors' attribute is deprecated and will not be used in" 

105 " Werkzeug 3.0.", 

106 DeprecationWarning, 

107 stacklevel=2, 

108 ) 

109 return self._encoding_errors 

110 

111 @encoding_errors.setter 

112 def encoding_errors(self, value: str) -> None: 

113 warnings.warn( 

114 "The 'encoding_errors' attribute is deprecated and will not be used in" 

115 " Werkzeug 3.0.", 

116 DeprecationWarning, 

117 stacklevel=2, 

118 ) 

119 self._encoding_errors = value 

120 

121 _url_charset: str 

122 

123 @property 

124 def url_charset(self) -> str: 

125 """The charset to use when decoding percent-encoded bytes in :attr:`args`. 

126 Defaults to the value of :attr:`charset`, which defaults to UTF-8. 

127 

128 .. deprecated:: 2.3 

129 Will be removed in Werkzeug 3.0. Percent-encoded bytes must always be UTF-8. 

130 

131 .. versionadded:: 0.6 

132 """ 

133 warnings.warn( 

134 "The 'url_charset' attribute is deprecated and will not be used in" 

135 " Werkzeug 3.0. Percent-encoded bytes must always be UTF-8.", 

136 DeprecationWarning, 

137 stacklevel=2, 

138 ) 

139 return self._url_charset 

140 

141 @url_charset.setter 

142 def url_charset(self, value: str) -> None: 

143 warnings.warn( 

144 "The 'url_charset' attribute is deprecated and will not be used in" 

145 " Werkzeug 3.0. Percent-encoded bytes must always be UTF-8.", 

146 DeprecationWarning, 

147 stacklevel=2, 

148 ) 

149 self._url_charset = value 

150 

151 #: the class to use for `args` and `form`. The default is an 

152 #: :class:`~werkzeug.datastructures.ImmutableMultiDict` which supports 

153 #: multiple values per key. alternatively it makes sense to use an 

154 #: :class:`~werkzeug.datastructures.ImmutableOrderedMultiDict` which 

155 #: preserves order or a :class:`~werkzeug.datastructures.ImmutableDict` 

156 #: which is the fastest but only remembers the last key. It is also 

157 #: possible to use mutable structures, but this is not recommended. 

158 #: 

159 #: .. versionadded:: 0.6 

160 parameter_storage_class: type[MultiDict] = ImmutableMultiDict 

161 

162 #: The type to be used for dict values from the incoming WSGI 

163 #: environment. (For example for :attr:`cookies`.) By default an 

164 #: :class:`~werkzeug.datastructures.ImmutableMultiDict` is used. 

165 #: 

166 #: .. versionchanged:: 1.0.0 

167 #: Changed to ``ImmutableMultiDict`` to support multiple values. 

168 #: 

169 #: .. versionadded:: 0.6 

170 dict_storage_class: type[MultiDict] = ImmutableMultiDict 

171 

172 #: the type to be used for list values from the incoming WSGI environment. 

173 #: By default an :class:`~werkzeug.datastructures.ImmutableList` is used 

174 #: (for example for :attr:`access_list`). 

175 #: 

176 #: .. versionadded:: 0.6 

177 list_storage_class: type[t.List] = ImmutableList 

178 

179 user_agent_class: type[UserAgent] = UserAgent 

180 """The class used and returned by the :attr:`user_agent` property to 

181 parse the header. Defaults to 

182 :class:`~werkzeug.user_agent.UserAgent`, which does no parsing. An 

183 extension can provide a subclass that uses a parser to provide other 

184 data. 

185 

186 .. versionadded:: 2.0 

187 """ 

188 

189 #: Valid host names when handling requests. By default all hosts are 

190 #: trusted, which means that whatever the client says the host is 

191 #: will be accepted. 

192 #: 

193 #: Because ``Host`` and ``X-Forwarded-Host`` headers can be set to 

194 #: any value by a malicious client, it is recommended to either set 

195 #: this property or implement similar validation in the proxy (if 

196 #: the application is being run behind one). 

197 #: 

198 #: .. versionadded:: 0.9 

199 trusted_hosts: list[str] | None = None 

200 

201 def __init__( 

202 self, 

203 method: str, 

204 scheme: str, 

205 server: tuple[str, int | None] | None, 

206 root_path: str, 

207 path: str, 

208 query_string: bytes, 

209 headers: Headers, 

210 remote_addr: str | None, 

211 ) -> None: 

212 if not isinstance(type(self).charset, property): 

213 warnings.warn( 

214 "The 'charset' attribute is deprecated and will not be used in Werkzeug" 

215 " 2.4. Interpreting bytes as text in body, form, and cookie data will" 

216 " always use UTF-8.", 

217 DeprecationWarning, 

218 stacklevel=2, 

219 ) 

220 self._charset = self.charset 

221 else: 

222 self._charset = "utf-8" 

223 

224 if not isinstance(type(self).encoding_errors, property): 

225 warnings.warn( 

226 "The 'encoding_errors' attribute is deprecated and will not be used in" 

227 " Werkzeug 3.0.", 

228 DeprecationWarning, 

229 stacklevel=2, 

230 ) 

231 self._encoding_errors = self.encoding_errors 

232 else: 

233 self._encoding_errors = "replace" 

234 

235 if not isinstance(type(self).url_charset, property): 

236 warnings.warn( 

237 "The 'url_charset' attribute is deprecated and will not be used in" 

238 " Werkzeug 3.0. Percent-encoded bytes must always be UTF-8.", 

239 DeprecationWarning, 

240 stacklevel=2, 

241 ) 

242 self._url_charset = self.url_charset 

243 else: 

244 self._url_charset = self._charset 

245 

246 #: The method the request was made with, such as ``GET``. 

247 self.method = method.upper() 

248 #: The URL scheme of the protocol the request used, such as 

249 #: ``https`` or ``wss``. 

250 self.scheme = scheme 

251 #: The address of the server. ``(host, port)``, ``(path, None)`` 

252 #: for unix sockets, or ``None`` if not known. 

253 self.server = server 

254 #: The prefix that the application is mounted under, without a 

255 #: trailing slash. :attr:`path` comes after this. 

256 self.root_path = root_path.rstrip("/") 

257 #: The path part of the URL after :attr:`root_path`. This is the 

258 #: path used for routing within the application. 

259 self.path = "/" + path.lstrip("/") 

260 #: The part of the URL after the "?". This is the raw value, use 

261 #: :attr:`args` for the parsed values. 

262 self.query_string = query_string 

263 #: The headers received with the request. 

264 self.headers = headers 

265 #: The address of the client sending the request. 

266 self.remote_addr = remote_addr 

267 

268 def __repr__(self) -> str: 

269 try: 

270 url = self.url 

271 except Exception as e: 

272 url = f"(invalid URL: {e})" 

273 

274 return f"<{type(self).__name__} {url!r} [{self.method}]>" 

275 

276 @cached_property 

277 def args(self) -> MultiDict[str, str]: 

278 """The parsed URL parameters (the part in the URL after the question 

279 mark). 

280 

281 By default an 

282 :class:`~werkzeug.datastructures.ImmutableMultiDict` 

283 is returned from this function. This can be changed by setting 

284 :attr:`parameter_storage_class` to a different type. This might 

285 be necessary if the order of the form data is important. 

286 

287 .. versionchanged:: 2.3 

288 Invalid bytes remain percent encoded. 

289 """ 

290 return self.parameter_storage_class( 

291 parse_qsl( 

292 self.query_string.decode(), 

293 keep_blank_values=True, 

294 encoding=self._url_charset, 

295 errors="werkzeug.url_quote", 

296 ) 

297 ) 

298 

299 @cached_property 

300 def access_route(self) -> list[str]: 

301 """If a forwarded header exists this is a list of all ip addresses 

302 from the client ip to the last proxy server. 

303 """ 

304 if "X-Forwarded-For" in self.headers: 

305 return self.list_storage_class( 

306 parse_list_header(self.headers["X-Forwarded-For"]) 

307 ) 

308 elif self.remote_addr is not None: 

309 return self.list_storage_class([self.remote_addr]) 

310 return self.list_storage_class() 

311 

312 @cached_property 

313 def full_path(self) -> str: 

314 """Requested path, including the query string.""" 

315 return f"{self.path}?{self.query_string.decode()}" 

316 

317 @property 

318 def is_secure(self) -> bool: 

319 """``True`` if the request was made with a secure protocol 

320 (HTTPS or WSS). 

321 """ 

322 return self.scheme in {"https", "wss"} 

323 

324 @cached_property 

325 def url(self) -> str: 

326 """The full request URL with the scheme, host, root path, path, 

327 and query string.""" 

328 return get_current_url( 

329 self.scheme, self.host, self.root_path, self.path, self.query_string 

330 ) 

331 

332 @cached_property 

333 def base_url(self) -> str: 

334 """Like :attr:`url` but without the query string.""" 

335 return get_current_url(self.scheme, self.host, self.root_path, self.path) 

336 

337 @cached_property 

338 def root_url(self) -> str: 

339 """The request URL scheme, host, and root path. This is the root 

340 that the application is accessed from. 

341 """ 

342 return get_current_url(self.scheme, self.host, self.root_path) 

343 

344 @cached_property 

345 def host_url(self) -> str: 

346 """The request URL scheme and host only.""" 

347 return get_current_url(self.scheme, self.host) 

348 

349 @cached_property 

350 def host(self) -> str: 

351 """The host name the request was made to, including the port if 

352 it's non-standard. Validated with :attr:`trusted_hosts`. 

353 """ 

354 return get_host( 

355 self.scheme, self.headers.get("host"), self.server, self.trusted_hosts 

356 ) 

357 

358 @cached_property 

359 def cookies(self) -> ImmutableMultiDict[str, str]: 

360 """A :class:`dict` with the contents of all cookies transmitted with 

361 the request.""" 

362 wsgi_combined_cookie = ";".join(self.headers.getlist("Cookie")) 

363 charset = self._charset if self._charset != "utf-8" else None 

364 errors = self._encoding_errors if self._encoding_errors != "replace" else None 

365 return parse_cookie( # type: ignore 

366 wsgi_combined_cookie, 

367 charset=charset, 

368 errors=errors, 

369 cls=self.dict_storage_class, 

370 ) 

371 

372 # Common Descriptors 

373 

374 content_type = header_property[str]( 

375 "Content-Type", 

376 doc="""The Content-Type entity-header field indicates the media 

377 type of the entity-body sent to the recipient or, in the case of 

378 the HEAD method, the media type that would have been sent had 

379 the request been a GET.""", 

380 read_only=True, 

381 ) 

382 

383 @cached_property 

384 def content_length(self) -> int | None: 

385 """The Content-Length entity-header field indicates the size of the 

386 entity-body in bytes or, in the case of the HEAD method, the size of 

387 the entity-body that would have been sent had the request been a 

388 GET. 

389 """ 

390 return get_content_length( 

391 http_content_length=self.headers.get("Content-Length"), 

392 http_transfer_encoding=self.headers.get("Transfer-Encoding"), 

393 ) 

394 

395 content_encoding = header_property[str]( 

396 "Content-Encoding", 

397 doc="""The Content-Encoding entity-header field is used as a 

398 modifier to the media-type. When present, its value indicates 

399 what additional content codings have been applied to the 

400 entity-body, and thus what decoding mechanisms must be applied 

401 in order to obtain the media-type referenced by the Content-Type 

402 header field. 

403 

404 .. versionadded:: 0.9""", 

405 read_only=True, 

406 ) 

407 content_md5 = header_property[str]( 

408 "Content-MD5", 

409 doc="""The Content-MD5 entity-header field, as defined in 

410 RFC 1864, is an MD5 digest of the entity-body for the purpose of 

411 providing an end-to-end message integrity check (MIC) of the 

412 entity-body. (Note: a MIC is good for detecting accidental 

413 modification of the entity-body in transit, but is not proof 

414 against malicious attacks.) 

415 

416 .. versionadded:: 0.9""", 

417 read_only=True, 

418 ) 

419 referrer = header_property[str]( 

420 "Referer", 

421 doc="""The Referer[sic] request-header field allows the client 

422 to specify, for the server's benefit, the address (URI) of the 

423 resource from which the Request-URI was obtained (the 

424 "referrer", although the header field is misspelled).""", 

425 read_only=True, 

426 ) 

427 date = header_property( 

428 "Date", 

429 None, 

430 parse_date, 

431 doc="""The Date general-header field represents the date and 

432 time at which the message was originated, having the same 

433 semantics as orig-date in RFC 822. 

434 

435 .. versionchanged:: 2.0 

436 The datetime object is timezone-aware. 

437 """, 

438 read_only=True, 

439 ) 

440 max_forwards = header_property( 

441 "Max-Forwards", 

442 None, 

443 int, 

444 doc="""The Max-Forwards request-header field provides a 

445 mechanism with the TRACE and OPTIONS methods to limit the number 

446 of proxies or gateways that can forward the request to the next 

447 inbound server.""", 

448 read_only=True, 

449 ) 

450 

451 def _parse_content_type(self) -> None: 

452 if not hasattr(self, "_parsed_content_type"): 

453 self._parsed_content_type = parse_options_header( 

454 self.headers.get("Content-Type", "") 

455 ) 

456 

457 @property 

458 def mimetype(self) -> str: 

459 """Like :attr:`content_type`, but without parameters (eg, without 

460 charset, type etc.) and always lowercase. For example if the content 

461 type is ``text/HTML; charset=utf-8`` the mimetype would be 

462 ``'text/html'``. 

463 """ 

464 self._parse_content_type() 

465 return self._parsed_content_type[0].lower() 

466 

467 @property 

468 def mimetype_params(self) -> dict[str, str]: 

469 """The mimetype parameters as dict. For example if the content 

470 type is ``text/html; charset=utf-8`` the params would be 

471 ``{'charset': 'utf-8'}``. 

472 """ 

473 self._parse_content_type() 

474 return self._parsed_content_type[1] 

475 

476 @cached_property 

477 def pragma(self) -> HeaderSet: 

478 """The Pragma general-header field is used to include 

479 implementation-specific directives that might apply to any recipient 

480 along the request/response chain. All pragma directives specify 

481 optional behavior from the viewpoint of the protocol; however, some 

482 systems MAY require that behavior be consistent with the directives. 

483 """ 

484 return parse_set_header(self.headers.get("Pragma", "")) 

485 

486 # Accept 

487 

488 @cached_property 

489 def accept_mimetypes(self) -> MIMEAccept: 

490 """List of mimetypes this client supports as 

491 :class:`~werkzeug.datastructures.MIMEAccept` object. 

492 """ 

493 return parse_accept_header(self.headers.get("Accept"), MIMEAccept) 

494 

495 @cached_property 

496 def accept_charsets(self) -> CharsetAccept: 

497 """List of charsets this client supports as 

498 :class:`~werkzeug.datastructures.CharsetAccept` object. 

499 """ 

500 return parse_accept_header(self.headers.get("Accept-Charset"), CharsetAccept) 

501 

502 @cached_property 

503 def accept_encodings(self) -> Accept: 

504 """List of encodings this client accepts. Encodings in a HTTP term 

505 are compression encodings such as gzip. For charsets have a look at 

506 :attr:`accept_charset`. 

507 """ 

508 return parse_accept_header(self.headers.get("Accept-Encoding")) 

509 

510 @cached_property 

511 def accept_languages(self) -> LanguageAccept: 

512 """List of languages this client accepts as 

513 :class:`~werkzeug.datastructures.LanguageAccept` object. 

514 

515 .. versionchanged 0.5 

516 In previous versions this was a regular 

517 :class:`~werkzeug.datastructures.Accept` object. 

518 """ 

519 return parse_accept_header(self.headers.get("Accept-Language"), LanguageAccept) 

520 

521 # ETag 

522 

523 @cached_property 

524 def cache_control(self) -> RequestCacheControl: 

525 """A :class:`~werkzeug.datastructures.RequestCacheControl` object 

526 for the incoming cache control headers. 

527 """ 

528 cache_control = self.headers.get("Cache-Control") 

529 return parse_cache_control_header(cache_control, None, RequestCacheControl) 

530 

531 @cached_property 

532 def if_match(self) -> ETags: 

533 """An object containing all the etags in the `If-Match` header. 

534 

535 :rtype: :class:`~werkzeug.datastructures.ETags` 

536 """ 

537 return parse_etags(self.headers.get("If-Match")) 

538 

539 @cached_property 

540 def if_none_match(self) -> ETags: 

541 """An object containing all the etags in the `If-None-Match` header. 

542 

543 :rtype: :class:`~werkzeug.datastructures.ETags` 

544 """ 

545 return parse_etags(self.headers.get("If-None-Match")) 

546 

547 @cached_property 

548 def if_modified_since(self) -> datetime | None: 

549 """The parsed `If-Modified-Since` header as a datetime object. 

550 

551 .. versionchanged:: 2.0 

552 The datetime object is timezone-aware. 

553 """ 

554 return parse_date(self.headers.get("If-Modified-Since")) 

555 

556 @cached_property 

557 def if_unmodified_since(self) -> datetime | None: 

558 """The parsed `If-Unmodified-Since` header as a datetime object. 

559 

560 .. versionchanged:: 2.0 

561 The datetime object is timezone-aware. 

562 """ 

563 return parse_date(self.headers.get("If-Unmodified-Since")) 

564 

565 @cached_property 

566 def if_range(self) -> IfRange: 

567 """The parsed ``If-Range`` header. 

568 

569 .. versionchanged:: 2.0 

570 ``IfRange.date`` is timezone-aware. 

571 

572 .. versionadded:: 0.7 

573 """ 

574 return parse_if_range_header(self.headers.get("If-Range")) 

575 

576 @cached_property 

577 def range(self) -> Range | None: 

578 """The parsed `Range` header. 

579 

580 .. versionadded:: 0.7 

581 

582 :rtype: :class:`~werkzeug.datastructures.Range` 

583 """ 

584 return parse_range_header(self.headers.get("Range")) 

585 

586 # User Agent 

587 

588 @cached_property 

589 def user_agent(self) -> UserAgent: 

590 """The user agent. Use ``user_agent.string`` to get the header 

591 value. Set :attr:`user_agent_class` to a subclass of 

592 :class:`~werkzeug.user_agent.UserAgent` to provide parsing for 

593 the other properties or other extended data. 

594 

595 .. versionchanged:: 2.1 

596 The built-in parser was removed. Set ``user_agent_class`` to a ``UserAgent`` 

597 subclass to parse data from the string. 

598 """ 

599 return self.user_agent_class(self.headers.get("User-Agent", "")) 

600 

601 # Authorization 

602 

603 @cached_property 

604 def authorization(self) -> Authorization | None: 

605 """The ``Authorization`` header parsed into an :class:`.Authorization` object. 

606 ``None`` if the header is not present. 

607 

608 .. versionchanged:: 2.3 

609 :class:`Authorization` is no longer a ``dict``. The ``token`` attribute 

610 was added for auth schemes that use a token instead of parameters. 

611 """ 

612 return Authorization.from_header(self.headers.get("Authorization")) 

613 

614 # CORS 

615 

616 origin = header_property[str]( 

617 "Origin", 

618 doc=( 

619 "The host that the request originated from. Set" 

620 " :attr:`~CORSResponseMixin.access_control_allow_origin` on" 

621 " the response to indicate which origins are allowed." 

622 ), 

623 read_only=True, 

624 ) 

625 

626 access_control_request_headers = header_property( 

627 "Access-Control-Request-Headers", 

628 load_func=parse_set_header, 

629 doc=( 

630 "Sent with a preflight request to indicate which headers" 

631 " will be sent with the cross origin request. Set" 

632 " :attr:`~CORSResponseMixin.access_control_allow_headers`" 

633 " on the response to indicate which headers are allowed." 

634 ), 

635 read_only=True, 

636 ) 

637 

638 access_control_request_method = header_property[str]( 

639 "Access-Control-Request-Method", 

640 doc=( 

641 "Sent with a preflight request to indicate which method" 

642 " will be used for the cross origin request. Set" 

643 " :attr:`~CORSResponseMixin.access_control_allow_methods`" 

644 " on the response to indicate which methods are allowed." 

645 ), 

646 read_only=True, 

647 ) 

648 

649 @property 

650 def is_json(self) -> bool: 

651 """Check if the mimetype indicates JSON data, either 

652 :mimetype:`application/json` or :mimetype:`application/*+json`. 

653 """ 

654 mt = self.mimetype 

655 return ( 

656 mt == "application/json" 

657 or mt.startswith("application/") 

658 and mt.endswith("+json") 

659 )