Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/tornado/httputil.py: 30%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

509 statements  

1# 

2# Copyright 2009 Facebook 

3# 

4# Licensed under the Apache License, Version 2.0 (the "License"); you may 

5# not use this file except in compliance with the License. You may obtain 

6# a copy of the License at 

7# 

8# http://www.apache.org/licenses/LICENSE-2.0 

9# 

10# Unless required by applicable law or agreed to in writing, software 

11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 

12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 

13# License for the specific language governing permissions and limitations 

14# under the License. 

15 

16"""HTTP utility code shared by clients and servers. 

17 

18This module also defines the `HTTPServerRequest` class which is exposed 

19via `tornado.web.RequestHandler.request`. 

20""" 

21 

22import calendar 

23import collections.abc 

24import copy 

25import dataclasses 

26import datetime 

27import email.utils 

28from functools import lru_cache 

29from http.client import responses 

30import http.cookies 

31import re 

32from ssl import SSLError 

33import time 

34import unicodedata 

35from urllib.parse import urlencode, urlparse, urlunparse, parse_qsl 

36 

37from tornado.escape import native_str, parse_qs_bytes, utf8, to_unicode 

38from tornado.util import ObjectDict, unicode_type 

39 

40# responses is unused in this file, but we re-export it to other files. 

41# Reference it so pyflakes doesn't complain. 

42responses 

43 

44import typing 

45from typing import ( 

46 Tuple, 

47 Iterable, 

48 List, 

49 Mapping, 

50 Iterator, 

51 Dict, 

52 Union, 

53 Optional, 

54 Awaitable, 

55 Generator, 

56 AnyStr, 

57) 

58 

59if typing.TYPE_CHECKING: 

60 from typing import Deque # noqa: F401 

61 from asyncio import Future # noqa: F401 

62 import unittest # noqa: F401 

63 

64 # This can be done unconditionally in the base class of HTTPHeaders 

65 # after we drop support for Python 3.8. 

66 StrMutableMapping = collections.abc.MutableMapping[str, str] 

67else: 

68 StrMutableMapping = collections.abc.MutableMapping 

69 

70# To be used with str.strip() and related methods. 

71HTTP_WHITESPACE = " \t" 

72 

73# Roughly the inverse of RequestHandler._VALID_HEADER_CHARS, but permits 

74# chars greater than \xFF (which may appear after decoding utf8). 

75_FORBIDDEN_HEADER_CHARS_RE = re.compile(r"[\x00-\x08\x0A-\x1F\x7F]") 

76 

77 

78class _ABNF: 

79 """Class that holds a subset of ABNF rules from RFC 9110 and friends. 

80 

81 Class attributes are re.Pattern objects, with the same name as in the RFC 

82 (with hyphens changed to underscores). Currently contains only the subset 

83 we use (which is why this class is not public). Unfortunately the fields 

84 cannot be alphabetized as they are in the RFCs because of dependencies. 

85 """ 

86 

87 # RFC 3986 (URI) 

88 # The URI hostname ABNF is both complex (including detailed vaildation of IPv4 and IPv6 

89 # literals) and not strict enough (a lot of punctuation is allowed by the ABNF even though 

90 # it is not allowed by DNS). We simplify it by allowing square brackets and colons in any 

91 # position, not only for their use in IPv6 literals. 

92 uri_unreserved = re.compile(r"[A-Za-z0-9\-._~]") 

93 uri_sub_delims = re.compile(r"[!$&'()*+,;=]") 

94 uri_pct_encoded = re.compile(r"%[0-9A-Fa-f]{2}") 

95 uri_host = re.compile( 

96 rf"(?:[\[\]:]|{uri_unreserved.pattern}|{uri_sub_delims.pattern}|{uri_pct_encoded.pattern})*" 

97 ) 

98 uri_port = re.compile(r"[0-9]*") 

99 

100 # RFC 5234 (ABNF) 

101 VCHAR = re.compile(r"[\x21-\x7E]") 

102 

103 # RFC 9110 (HTTP Semantics) 

104 obs_text = re.compile(r"[\x80-\xFF]") 

105 field_vchar = re.compile(rf"(?:{VCHAR.pattern}|{obs_text.pattern})") 

106 # Not exactly from the RFC to simplify and combine field-content and field-value. 

107 field_value = re.compile( 

108 rf"|" 

109 rf"{field_vchar.pattern}|" 

110 rf"{field_vchar.pattern}(?:{field_vchar.pattern}| |\t)*{field_vchar.pattern}" 

111 ) 

112 tchar = re.compile(r"[!#$%&'*+\-.^_`|~0-9A-Za-z]") 

113 token = re.compile(rf"{tchar.pattern}+") 

114 field_name = token 

115 method = token 

116 host = re.compile(rf"(?:{uri_host.pattern})(?::{uri_port.pattern})?") 

117 

118 # RFC 9112 (HTTP/1.1) 

119 HTTP_version = re.compile(r"HTTP/[0-9]\.[0-9]") 

120 reason_phrase = re.compile(rf"(?:[\t ]|{VCHAR.pattern}|{obs_text.pattern})+") 

121 # request_target delegates to the URI RFC 3986, which is complex and may be 

122 # too restrictive (for example, the WHATWG version of the URL spec allows non-ASCII 

123 # characters). Instead, we allow everything but control chars and whitespace. 

124 request_target = re.compile(rf"{field_vchar.pattern}+") 

125 request_line = re.compile( 

126 rf"({method.pattern}) ({request_target.pattern}) ({HTTP_version.pattern})" 

127 ) 

128 status_code = re.compile(r"[0-9]{3}") 

129 status_line = re.compile( 

130 rf"({HTTP_version.pattern}) ({status_code.pattern}) ({reason_phrase.pattern})?" 

131 ) 

132 

133 

134@lru_cache(1000) 

135def _normalize_header(name: str) -> str: 

136 """Map a header name to Http-Header-Case. 

137 

138 >>> _normalize_header("coNtent-TYPE") 

139 'Content-Type' 

140 """ 

141 return "-".join([w.capitalize() for w in name.split("-")]) 

142 

143 

144class HTTPHeaders(StrMutableMapping): 

145 """A dictionary that maintains ``Http-Header-Case`` for all keys. 

146 

147 Supports multiple values per key via a pair of new methods, 

148 `add()` and `get_list()`. The regular dictionary interface 

149 returns a single value per key, with multiple values joined by a 

150 comma. 

151 

152 >>> h = HTTPHeaders({"content-type": "text/html"}) 

153 >>> list(h.keys()) 

154 ['Content-Type'] 

155 >>> h["Content-Type"] 

156 'text/html' 

157 

158 >>> h.add("Set-Cookie", "A=B") 

159 >>> h.add("Set-Cookie", "C=D") 

160 >>> h["set-cookie"] 

161 'A=B,C=D' 

162 >>> h.get_list("set-cookie") 

163 ['A=B', 'C=D'] 

164 

165 >>> for (k,v) in sorted(h.get_all()): 

166 ... print('%s: %s' % (k,v)) 

167 ... 

168 Content-Type: text/html 

169 Set-Cookie: A=B 

170 Set-Cookie: C=D 

171 """ 

172 

173 @typing.overload 

174 def __init__(self, __arg: Mapping[str, List[str]]) -> None: 

175 pass 

176 

177 @typing.overload # noqa: F811 

178 def __init__(self, __arg: Mapping[str, str]) -> None: 

179 pass 

180 

181 @typing.overload # noqa: F811 

182 def __init__(self, *args: Tuple[str, str]) -> None: 

183 pass 

184 

185 @typing.overload # noqa: F811 

186 def __init__(self, **kwargs: str) -> None: 

187 pass 

188 

189 def __init__(self, *args: typing.Any, **kwargs: str) -> None: # noqa: F811 

190 # Formally, HTTP headers are a mapping from a field name to a "combined field value", 

191 # which may be constructed from multiple field lines by joining them with commas. 

192 # In practice, however, some headers (notably Set-Cookie) do not follow this convention, 

193 # so we maintain a mapping from field name to a list of field lines in self._as_list. 

194 # self._combined_cache is a cache of the combined field values derived from self._as_list 

195 # on demand (and cleared whenever the list is modified). 

196 self._as_list: dict[str, list[str]] = {} 

197 self._combined_cache: dict[str, str] = {} 

198 self._last_key = None # type: Optional[str] 

199 if len(args) == 1 and len(kwargs) == 0 and isinstance(args[0], HTTPHeaders): 

200 # Copy constructor 

201 for k, v in args[0].get_all(): 

202 self.add(k, v) 

203 else: 

204 # Dict-style initialization 

205 self.update(*args, **kwargs) 

206 

207 # new public methods 

208 

209 def add(self, name: str, value: str, *, _chars_are_bytes: bool = True) -> None: 

210 """Adds a new value for the given key.""" 

211 if not _ABNF.field_name.fullmatch(name): 

212 raise HTTPInputError("Invalid header name %r" % name) 

213 if _chars_are_bytes: 

214 if not _ABNF.field_value.fullmatch(to_unicode(value)): 

215 # TODO: the fact we still support bytes here (contrary to type annotations) 

216 # and still test for it should probably be changed. 

217 raise HTTPInputError("Invalid header value %r" % value) 

218 else: 

219 if _FORBIDDEN_HEADER_CHARS_RE.search(value): 

220 raise HTTPInputError("Invalid header value %r" % value) 

221 norm_name = _normalize_header(name) 

222 self._last_key = norm_name 

223 if norm_name in self: 

224 self._combined_cache.pop(norm_name, None) 

225 self._as_list[norm_name].append(value) 

226 else: 

227 self[norm_name] = value 

228 

229 def get_list(self, name: str) -> List[str]: 

230 """Returns all values for the given header as a list.""" 

231 norm_name = _normalize_header(name) 

232 return self._as_list.get(norm_name, []) 

233 

234 def get_all(self) -> Iterable[Tuple[str, str]]: 

235 """Returns an iterable of all (name, value) pairs. 

236 

237 If a header has multiple values, multiple pairs will be 

238 returned with the same name. 

239 """ 

240 for name, values in self._as_list.items(): 

241 for value in values: 

242 yield (name, value) 

243 

244 def parse_line(self, line: str, *, _chars_are_bytes: bool = True) -> None: 

245 r"""Updates the dictionary with a single header line. 

246 

247 >>> h = HTTPHeaders() 

248 >>> h.parse_line("Content-Type: text/html") 

249 >>> h.get('content-type') 

250 'text/html' 

251 >>> h.parse_line("Content-Length: 42\r\n") 

252 >>> h.get('content-type') 

253 'text/html' 

254 

255 .. versionchanged:: 6.5 

256 Now supports lines with or without the trailing CRLF, making it possible 

257 to pass lines from AsyncHTTPClient's header_callback directly to this method. 

258 

259 .. deprecated:: 6.5 

260 In Tornado 7.0, certain deprecated features of HTTP will become errors. 

261 Specifically, line folding and the use of LF (with CR) as a line separator 

262 will be removed. 

263 """ 

264 if m := re.search(r"\r?\n$", line): 

265 # RFC 9112 section 2.2: a recipient MAY recognize a single LF as a line 

266 # terminator and ignore any preceding CR. 

267 # TODO(7.0): Remove this support for LF-only line endings. 

268 line = line[: m.start()] 

269 if not line: 

270 # Empty line, or the final CRLF of a header block. 

271 return 

272 if line[0] in HTTP_WHITESPACE: 

273 # continuation of a multi-line header 

274 # TODO(7.0): Remove support for line folding. 

275 if self._last_key is None: 

276 raise HTTPInputError("first header line cannot start with whitespace") 

277 new_part = " " + line.strip(HTTP_WHITESPACE) 

278 if _chars_are_bytes: 

279 if not _ABNF.field_value.fullmatch(new_part[1:]): 

280 raise HTTPInputError("Invalid header continuation %r" % new_part) 

281 else: 

282 if _FORBIDDEN_HEADER_CHARS_RE.search(new_part): 

283 raise HTTPInputError("Invalid header value %r" % new_part) 

284 self._as_list[self._last_key][-1] += new_part 

285 self._combined_cache.pop(self._last_key, None) 

286 else: 

287 try: 

288 name, value = line.split(":", 1) 

289 except ValueError: 

290 raise HTTPInputError("no colon in header line") 

291 self.add( 

292 name, value.strip(HTTP_WHITESPACE), _chars_are_bytes=_chars_are_bytes 

293 ) 

294 

295 @classmethod 

296 def parse(cls, headers: str, *, _chars_are_bytes: bool = True) -> "HTTPHeaders": 

297 """Returns a dictionary from HTTP header text. 

298 

299 >>> h = HTTPHeaders.parse("Content-Type: text/html\\r\\nContent-Length: 42\\r\\n") 

300 >>> sorted(h.items()) 

301 [('Content-Length', '42'), ('Content-Type', 'text/html')] 

302 

303 .. versionchanged:: 5.1 

304 

305 Raises `HTTPInputError` on malformed headers instead of a 

306 mix of `KeyError`, and `ValueError`. 

307 

308 """ 

309 # _chars_are_bytes is a hack. This method is used in two places, HTTP headers (in which 

310 # non-ascii characters are to be interpreted as latin-1) and multipart/form-data (in which 

311 # they are to be interpreted as utf-8). For historical reasons, this method handled this by 

312 # expecting both callers to decode the headers to strings before parsing them. This wasn't a 

313 # problem until we started doing stricter validation of the characters allowed in HTTP 

314 # headers (using ABNF rules defined in terms of byte values), which inadvertently started 

315 # disallowing non-latin1 characters in multipart/form-data filenames. 

316 # 

317 # This method should have accepted bytes and a desired encoding, but this change is being 

318 # introduced in a patch release that shouldn't change the API. Instead, the _chars_are_bytes 

319 # flag decides whether to use HTTP-style ABNF validation (treating the string as bytes 

320 # smuggled through the latin1 encoding) or to accept any non-control unicode characters 

321 # as required by multipart/form-data. This method will change to accept bytes in a future 

322 # release. 

323 h = cls() 

324 

325 start = 0 

326 while True: 

327 lf = headers.find("\n", start) 

328 if lf == -1: 

329 h.parse_line(headers[start:], _chars_are_bytes=_chars_are_bytes) 

330 break 

331 line = headers[start : lf + 1] 

332 start = lf + 1 

333 h.parse_line(line, _chars_are_bytes=_chars_are_bytes) 

334 return h 

335 

336 # MutableMapping abstract method implementations. 

337 

338 def __setitem__(self, name: str, value: str) -> None: 

339 norm_name = _normalize_header(name) 

340 self._combined_cache[norm_name] = value 

341 self._as_list[norm_name] = [value] 

342 

343 def __contains__(self, name: object) -> bool: 

344 # This is an important optimization to avoid the expensive concatenation 

345 # in __getitem__ when it's not needed. 

346 if not isinstance(name, str): 

347 return False 

348 norm_name = _normalize_header(name) 

349 return norm_name in self._as_list 

350 

351 def __getitem__(self, name: str) -> str: 

352 header = _normalize_header(name) 

353 if header not in self._combined_cache: 

354 self._combined_cache[header] = ",".join(self._as_list[header]) 

355 return self._combined_cache[header] 

356 

357 def __delitem__(self, name: str) -> None: 

358 norm_name = _normalize_header(name) 

359 del self._combined_cache[norm_name] 

360 del self._as_list[norm_name] 

361 

362 def __len__(self) -> int: 

363 return len(self._as_list) 

364 

365 def __iter__(self) -> Iterator[typing.Any]: 

366 return iter(self._as_list) 

367 

368 def copy(self) -> "HTTPHeaders": 

369 # defined in dict but not in MutableMapping. 

370 return HTTPHeaders(self) 

371 

372 # Use our overridden copy method for the copy.copy module. 

373 # This makes shallow copies one level deeper, but preserves 

374 # the appearance that HTTPHeaders is a single container. 

375 __copy__ = copy 

376 

377 def __str__(self) -> str: 

378 lines = [] 

379 for name, value in self.get_all(): 

380 lines.append(f"{name}: {value}\n") 

381 return "".join(lines) 

382 

383 __unicode__ = __str__ 

384 

385 

386class HTTPServerRequest: 

387 """A single HTTP request. 

388 

389 All attributes are type `str` unless otherwise noted. 

390 

391 .. attribute:: method 

392 

393 HTTP request method, e.g. "GET" or "POST" 

394 

395 .. attribute:: uri 

396 

397 The requested uri. 

398 

399 .. attribute:: path 

400 

401 The path portion of `uri` 

402 

403 .. attribute:: query 

404 

405 The query portion of `uri` 

406 

407 .. attribute:: version 

408 

409 HTTP version specified in request, e.g. "HTTP/1.1" 

410 

411 .. attribute:: headers 

412 

413 `.HTTPHeaders` dictionary-like object for request headers. Acts like 

414 a case-insensitive dictionary with additional methods for repeated 

415 headers. 

416 

417 .. attribute:: body 

418 

419 Request body, if present, as a byte string. 

420 

421 .. attribute:: remote_ip 

422 

423 Client's IP address as a string. If ``HTTPServer.xheaders`` is set, 

424 will pass along the real IP address provided by a load balancer 

425 in the ``X-Real-Ip`` or ``X-Forwarded-For`` header. 

426 

427 .. versionchanged:: 3.1 

428 The list format of ``X-Forwarded-For`` is now supported. 

429 

430 .. attribute:: protocol 

431 

432 The protocol used, either "http" or "https". If ``HTTPServer.xheaders`` 

433 is set, will pass along the protocol used by a load balancer if 

434 reported via an ``X-Scheme`` header. 

435 

436 .. attribute:: host 

437 

438 The requested hostname, usually taken from the ``Host`` header. 

439 

440 .. attribute:: arguments 

441 

442 GET/POST arguments are available in the arguments property, which 

443 maps arguments names to lists of values (to support multiple values 

444 for individual names). Names are of type `str`, while arguments 

445 are byte strings. Note that this is different from 

446 `.RequestHandler.get_argument`, which returns argument values as 

447 unicode strings. 

448 

449 .. attribute:: query_arguments 

450 

451 Same format as ``arguments``, but contains only arguments extracted 

452 from the query string. 

453 

454 .. versionadded:: 3.2 

455 

456 .. attribute:: body_arguments 

457 

458 Same format as ``arguments``, but contains only arguments extracted 

459 from the request body. 

460 

461 .. versionadded:: 3.2 

462 

463 .. attribute:: files 

464 

465 File uploads are available in the files property, which maps file 

466 names to lists of `.HTTPFile`. 

467 

468 .. attribute:: connection 

469 

470 An HTTP request is attached to a single HTTP connection, which can 

471 be accessed through the "connection" attribute. Since connections 

472 are typically kept open in HTTP/1.1, multiple requests can be handled 

473 sequentially on a single connection. 

474 

475 .. versionchanged:: 4.0 

476 Moved from ``tornado.httpserver.HTTPRequest``. 

477 

478 .. deprecated:: 6.5.2 

479 The ``host`` argument to the ``HTTPServerRequest`` constructor is deprecated. Use 

480 ``headers["Host"]`` instead. This argument was mistakenly removed in Tornado 6.5.0 and 

481 temporarily restored in 6.5.2. 

482 """ 

483 

484 path = None # type: str 

485 query = None # type: str 

486 

487 # HACK: Used for stream_request_body 

488 _body_future = None # type: Future[None] 

489 

490 def __init__( 

491 self, 

492 method: Optional[str] = None, 

493 uri: Optional[str] = None, 

494 version: str = "HTTP/1.0", 

495 headers: Optional[HTTPHeaders] = None, 

496 body: Optional[bytes] = None, 

497 host: Optional[str] = None, 

498 files: Optional[Dict[str, List["HTTPFile"]]] = None, 

499 connection: Optional["HTTPConnection"] = None, 

500 start_line: Optional["RequestStartLine"] = None, 

501 server_connection: Optional[object] = None, 

502 ) -> None: 

503 if start_line is not None: 

504 method, uri, version = start_line 

505 self.method = method 

506 self.uri = uri 

507 self.version = version 

508 self.headers = headers or HTTPHeaders() 

509 self.body = body or b"" 

510 

511 # set remote IP and protocol 

512 context = getattr(connection, "context", None) 

513 self.remote_ip = getattr(context, "remote_ip", None) 

514 self.protocol = getattr(context, "protocol", "http") 

515 

516 try: 

517 self.host = host or self.headers["Host"] 

518 except KeyError: 

519 if version == "HTTP/1.0": 

520 # HTTP/1.0 does not require the Host header. 

521 self.host = "127.0.0.1" 

522 else: 

523 raise HTTPInputError("Missing Host header") 

524 if not _ABNF.host.fullmatch(self.host): 

525 raise HTTPInputError("Invalid Host header: %r" % self.host) 

526 if "," in self.host: 

527 # https://www.rfc-editor.org/rfc/rfc9112.html#name-request-target 

528 # Server MUST respond with 400 Bad Request if multiple 

529 # Host headers are present. 

530 # 

531 # We test for the presence of a comma instead of the number of 

532 # headers received because a proxy may have converted 

533 # multiple headers into a single comma-separated value 

534 # (per RFC 9110 section 5.3). 

535 # 

536 # This is technically a departure from the RFC since the ABNF 

537 # does not forbid commas in the host header. However, since 

538 # commas are not allowed in DNS names, it is appropriate to 

539 # disallow them. (The same argument could be made for other special 

540 # characters, but commas are the most problematic since they could 

541 # be used to exploit differences between proxies when multiple headers 

542 # are supplied). 

543 raise HTTPInputError("Multiple host headers not allowed: %r" % self.host) 

544 self.host_name = split_host_and_port(self.host.lower())[0] 

545 self.files = files or {} 

546 self.connection = connection 

547 self.server_connection = server_connection 

548 self._start_time = time.time() 

549 self._finish_time = None 

550 

551 if uri is not None: 

552 self.path, sep, self.query = uri.partition("?") 

553 try: 

554 self.arguments = parse_qs_bytes( 

555 self.query, 

556 keep_blank_values=True, 

557 # The query string is bounded by max_header_size, but parsing 

558 # is expensive enough per field to be worth limiting. Use the 

559 # same limit as a urlencoded body. This reads the global 

560 # config because HTTPServerRequest has no access to the 

561 # per-connection configuration; see set_parse_body_config. 

562 max_num_fields=_DEFAULT_PARSE_BODY_CONFIG.urlencoded.max_arguments, 

563 ) 

564 except Exception as e: 

565 raise HTTPInputError("Invalid query string: %s" % e) from e 

566 self.query_arguments = copy.deepcopy(self.arguments) 

567 self.body_arguments = {} # type: Dict[str, List[bytes]] 

568 

569 @property 

570 def cookies(self) -> Dict[str, http.cookies.Morsel]: 

571 """A dictionary of ``http.cookies.Morsel`` objects.""" 

572 if not hasattr(self, "_cookies"): 

573 self._cookies = ( 

574 http.cookies.SimpleCookie() 

575 ) # type: http.cookies.SimpleCookie 

576 if "Cookie" in self.headers: 

577 try: 

578 parsed = parse_cookie(self.headers["Cookie"]) 

579 except Exception: 

580 pass 

581 else: 

582 for k, v in parsed.items(): 

583 try: 

584 self._cookies[k] = v 

585 except Exception: 

586 # SimpleCookie imposes some restrictions on keys; 

587 # parse_cookie does not. Discard any cookies 

588 # with disallowed keys. 

589 pass 

590 return self._cookies 

591 

592 def full_url(self) -> str: 

593 """Reconstructs the full URL for this request.""" 

594 return self.protocol + "://" + self.host + self.uri # type: ignore[operator] 

595 

596 def request_time(self) -> float: 

597 """Returns the amount of time it took for this request to execute.""" 

598 if self._finish_time is None: 

599 return time.time() - self._start_time 

600 else: 

601 return self._finish_time - self._start_time 

602 

603 def get_ssl_certificate( 

604 self, binary_form: bool = False 

605 ) -> Union[None, Dict, bytes]: 

606 """Returns the client's SSL certificate, if any. 

607 

608 To use client certificates, the HTTPServer's 

609 `ssl.SSLContext.verify_mode` field must be set, e.g.:: 

610 

611 ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) 

612 ssl_ctx.load_cert_chain("foo.crt", "foo.key") 

613 ssl_ctx.load_verify_locations("cacerts.pem") 

614 ssl_ctx.verify_mode = ssl.CERT_REQUIRED 

615 server = HTTPServer(app, ssl_options=ssl_ctx) 

616 

617 By default, the return value is a dictionary (or None, if no 

618 client certificate is present). If ``binary_form`` is true, a 

619 DER-encoded form of the certificate is returned instead. See 

620 SSLSocket.getpeercert() in the standard library for more 

621 details. 

622 http://docs.python.org/library/ssl.html#sslsocket-objects 

623 """ 

624 try: 

625 if self.connection is None: 

626 return None 

627 # TODO: add a method to HTTPConnection for this so it can work with HTTP/2 

628 return self.connection.stream.socket.getpeercert( # type: ignore 

629 binary_form=binary_form 

630 ) 

631 except SSLError: 

632 return None 

633 

634 def _parse_body(self) -> None: 

635 parse_body_arguments( 

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

637 self.body, 

638 self.body_arguments, 

639 self.files, 

640 self.headers, 

641 ) 

642 

643 for k, v in self.body_arguments.items(): 

644 self.arguments.setdefault(k, []).extend(v) 

645 

646 def __repr__(self) -> str: 

647 attrs = ("protocol", "host", "method", "uri", "version", "remote_ip") 

648 args = ", ".join([f"{n}={getattr(self, n)!r}" for n in attrs]) 

649 return f"{self.__class__.__name__}({args})" 

650 

651 

652class HTTPInputError(Exception): 

653 """Exception class for malformed HTTP requests or responses 

654 from remote sources. 

655 

656 .. versionadded:: 4.0 

657 """ 

658 

659 pass 

660 

661 

662class HTTPOutputError(Exception): 

663 """Exception class for errors in HTTP output. 

664 

665 .. versionadded:: 4.0 

666 """ 

667 

668 pass 

669 

670 

671class HTTPServerConnectionDelegate: 

672 """Implement this interface to handle requests from `.HTTPServer`. 

673 

674 .. versionadded:: 4.0 

675 """ 

676 

677 def start_request( 

678 self, server_conn: object, request_conn: "HTTPConnection" 

679 ) -> "HTTPMessageDelegate": 

680 """This method is called by the server when a new request has started. 

681 

682 :arg server_conn: is an opaque object representing the long-lived 

683 (e.g. tcp-level) connection. 

684 :arg request_conn: is a `.HTTPConnection` object for a single 

685 request/response exchange. 

686 

687 This method should return a `.HTTPMessageDelegate`. 

688 """ 

689 raise NotImplementedError() 

690 

691 def on_close(self, server_conn: object) -> None: 

692 """This method is called when a connection has been closed. 

693 

694 :arg server_conn: is a server connection that has previously been 

695 passed to ``start_request``. 

696 """ 

697 pass 

698 

699 

700class HTTPMessageDelegate: 

701 """Implement this interface to handle an HTTP request or response. 

702 

703 .. versionadded:: 4.0 

704 """ 

705 

706 # TODO: genericize this class to avoid exposing the Union. 

707 def headers_received( 

708 self, 

709 start_line: Union["RequestStartLine", "ResponseStartLine"], 

710 headers: HTTPHeaders, 

711 ) -> Optional[Awaitable[None]]: 

712 """Called when the HTTP headers have been received and parsed. 

713 

714 :arg start_line: a `.RequestStartLine` or `.ResponseStartLine` 

715 depending on whether this is a client or server message. 

716 :arg headers: a `.HTTPHeaders` instance. 

717 

718 Some `.HTTPConnection` methods can only be called during 

719 ``headers_received``. 

720 

721 May return a `.Future`; if it does the body will not be read 

722 until it is done. 

723 """ 

724 pass 

725 

726 def data_received(self, chunk: bytes) -> Optional[Awaitable[None]]: 

727 """Called when a chunk of data has been received. 

728 

729 May return a `.Future` for flow control. 

730 """ 

731 pass 

732 

733 def finish(self) -> None: 

734 """Called after the last chunk of data has been received.""" 

735 pass 

736 

737 def on_connection_close(self) -> None: 

738 """Called if the connection is closed without finishing the request. 

739 

740 If ``headers_received`` is called, either ``finish`` or 

741 ``on_connection_close`` will be called, but not both. 

742 """ 

743 pass 

744 

745 

746class HTTPConnection: 

747 """Applications use this interface to write their responses. 

748 

749 .. versionadded:: 4.0 

750 """ 

751 

752 def write_headers( 

753 self, 

754 start_line: Union["RequestStartLine", "ResponseStartLine"], 

755 headers: HTTPHeaders, 

756 chunk: Optional[bytes] = None, 

757 ) -> "Future[None]": 

758 """Write an HTTP header block. 

759 

760 :arg start_line: a `.RequestStartLine` or `.ResponseStartLine`. 

761 :arg headers: a `.HTTPHeaders` instance. 

762 :arg chunk: the first (optional) chunk of data. This is an optimization 

763 so that small responses can be written in the same call as their 

764 headers. 

765 

766 The ``version`` field of ``start_line`` is ignored. 

767 

768 Returns a future for flow control. 

769 

770 .. versionchanged:: 6.0 

771 

772 The ``callback`` argument was removed. 

773 """ 

774 raise NotImplementedError() 

775 

776 def write(self, chunk: bytes) -> "Future[None]": 

777 """Writes a chunk of body data. 

778 

779 Returns a future for flow control. 

780 

781 .. versionchanged:: 6.0 

782 

783 The ``callback`` argument was removed. 

784 """ 

785 raise NotImplementedError() 

786 

787 def finish(self) -> None: 

788 """Indicates that the last body data has been written.""" 

789 raise NotImplementedError() 

790 

791 

792def url_concat( 

793 url: str, 

794 args: Union[ 

795 None, Dict[str, str], List[Tuple[str, str]], Tuple[Tuple[str, str], ...] 

796 ], 

797) -> str: 

798 """Concatenate url and arguments regardless of whether 

799 url has existing query parameters. 

800 

801 ``args`` may be either a dictionary or a list of key-value pairs 

802 (the latter allows for multiple values with the same key. 

803 

804 >>> url_concat("http://example.com/foo", dict(c="d")) 

805 'http://example.com/foo?c=d' 

806 >>> url_concat("http://example.com/foo?a=b", dict(c="d")) 

807 'http://example.com/foo?a=b&c=d' 

808 >>> url_concat("http://example.com/foo?a=b", [("c", "d"), ("c", "d2")]) 

809 'http://example.com/foo?a=b&c=d&c=d2' 

810 """ 

811 if args is None: 

812 return url 

813 parsed_url = urlparse(url) 

814 if isinstance(args, dict): 

815 parsed_query = parse_qsl(parsed_url.query, keep_blank_values=True) 

816 parsed_query.extend(args.items()) 

817 elif isinstance(args, list) or isinstance(args, tuple): 

818 parsed_query = parse_qsl(parsed_url.query, keep_blank_values=True) 

819 parsed_query.extend(args) 

820 else: 

821 err = "'args' parameter should be dict, list or tuple. Not {0}".format( 

822 type(args) 

823 ) 

824 raise TypeError(err) 

825 final_query = urlencode(parsed_query) 

826 url = urlunparse( 

827 ( 

828 parsed_url[0], 

829 parsed_url[1], 

830 parsed_url[2], 

831 parsed_url[3], 

832 final_query, 

833 parsed_url[5], 

834 ) 

835 ) 

836 return url 

837 

838 

839class HTTPFile(ObjectDict): 

840 """Represents a file uploaded via a form. 

841 

842 For backwards compatibility, its instance attributes are also 

843 accessible as dictionary keys. 

844 

845 * ``filename`` 

846 * ``body`` 

847 * ``content_type`` 

848 """ 

849 

850 filename: str 

851 body: bytes 

852 content_type: str 

853 

854 

855def _parse_request_range( 

856 range_header: str, 

857) -> Optional[Tuple[Optional[int], Optional[int]]]: 

858 """Parses a Range header. 

859 

860 Returns either ``None`` or tuple ``(start, end)``. 

861 Note that while the HTTP headers use inclusive byte positions, 

862 this method returns indexes suitable for use in slices. 

863 

864 >>> start, end = _parse_request_range("bytes=1-2") 

865 >>> start, end 

866 (1, 3) 

867 >>> [0, 1, 2, 3, 4][start:end] 

868 [1, 2] 

869 >>> _parse_request_range("bytes=6-") 

870 (6, None) 

871 >>> _parse_request_range("bytes=-6") 

872 (-6, None) 

873 >>> _parse_request_range("bytes=-0") 

874 (None, 0) 

875 >>> _parse_request_range("bytes=") 

876 (None, None) 

877 >>> _parse_request_range("foo=42") 

878 >>> _parse_request_range("bytes=1-2,6-10") 

879 

880 Note: only supports one range (ex, ``bytes=1-2,6-10`` is not allowed). 

881 

882 See [0] for the details of the range header. 

883 

884 [0]: http://greenbytes.de/tech/webdav/draft-ietf-httpbis-p5-range-latest.html#byte.ranges 

885 """ 

886 unit, _, value = range_header.partition("=") 

887 unit, value = unit.strip(), value.strip() 

888 if unit != "bytes": 

889 return None 

890 start_b, _, end_b = value.partition("-") 

891 try: 

892 start = _int_or_none(start_b) 

893 end = _int_or_none(end_b) 

894 except ValueError: 

895 return None 

896 if end is not None: 

897 if start is None: 

898 if end != 0: 

899 start = -end 

900 end = None 

901 else: 

902 end += 1 

903 return (start, end) 

904 

905 

906def _get_content_range(start: Optional[int], end: Optional[int], total: int) -> str: 

907 """Returns a suitable Content-Range header: 

908 

909 >>> print(_get_content_range(None, 1, 4)) 

910 bytes 0-0/4 

911 >>> print(_get_content_range(1, 3, 4)) 

912 bytes 1-2/4 

913 >>> print(_get_content_range(None, None, 4)) 

914 bytes 0-3/4 

915 """ 

916 start = start or 0 

917 end = (end or total) - 1 

918 return f"bytes {start}-{end}/{total}" 

919 

920 

921def _int_or_none(val: str) -> Optional[int]: 

922 val = val.strip() 

923 if val == "": 

924 return None 

925 return int(val) 

926 

927 

928@dataclasses.dataclass 

929class ParseMultipartConfig: 

930 """This class configures the parsing of ``multipart/form-data`` request bodies. 

931 

932 Its primary purpose is to place limits on the size and complexity of request messages 

933 to avoid potential denial-of-service attacks. 

934 

935 .. versionadded:: 6.5.5 

936 """ 

937 

938 enabled: bool = True 

939 """Set this to false to disable the parsing of ``multipart/form-data`` requests entirely. 

940 

941 This may be desirable for applications that do not need to handle this format, since 

942 multipart request have a history of DoS vulnerabilities in Tornado. Multipart requests 

943 are used primarily for ``<input type="file">`` in HTML forms, or in APIs that mimic this 

944 format. File uploads that use the HTTP ``PUT`` method generally do not use the multipart 

945 format. 

946 """ 

947 

948 max_parts: int = 100 

949 """The maximum number of parts accepted in a multipart request. 

950 

951 Each ``<input>`` element in an HTML form corresponds to at least one "part". 

952 """ 

953 

954 max_part_header_size: int = 10 * 1024 

955 """The maximum size of the headers for each part of a multipart request. 

956 

957 The header for a part contains the name of the form field and optionally the filename 

958 and content type of the uploaded file. 

959 """ 

960 

961 

962@dataclasses.dataclass 

963class ParseUrlEncodedConfig: 

964 """This class configures the parsing of ``application/x-www-form-urlencoded`` request bodies. 

965 

966 Its primary purpose is to place limits on the size and complexity of request messages 

967 to avoid potential denial-of-service attacks. 

968 

969 .. versionadded:: 6.5.8 

970 """ 

971 

972 max_arguments: int = 1000 

973 """The maximum number of arguments accepted in a urlencoded request. 

974 

975 Each ``<input>`` element in an HTML form corresponds to at least one argument. 

976 """ 

977 

978 

979@dataclasses.dataclass 

980class ParseBodyConfig: 

981 """This class configures the parsing of request bodies. 

982 

983 .. versionadded:: 6.5.5 

984 """ 

985 

986 multipart: ParseMultipartConfig = dataclasses.field( 

987 default_factory=ParseMultipartConfig 

988 ) 

989 urlencoded: ParseUrlEncodedConfig = dataclasses.field( 

990 default_factory=ParseUrlEncodedConfig 

991 ) 

992 """Configuration for ``multipart/form-data`` request bodies.""" 

993 

994 

995_DEFAULT_PARSE_BODY_CONFIG = ParseBodyConfig() 

996 

997 

998def set_parse_body_config(config: ParseBodyConfig) -> None: 

999 r"""Sets the **global** default configuration for parsing request bodies. 

1000 

1001 This global setting is provided as a stopgap for applications that need to raise the limits 

1002 introduced in Tornado 6.5.5, or who wish to disable the parsing of multipart/form-data bodies 

1003 entirely. Non-global configuration for this functionality will be introduced in a future 

1004 release. 

1005 

1006 >>> content_type = "multipart/form-data; boundary=foo" 

1007 >>> multipart_body = b"--foo--\r\n" 

1008 >>> parse_body_arguments(content_type, multipart_body, {}, {}) 

1009 >>> multipart_config = ParseMultipartConfig(enabled=False) 

1010 >>> config = ParseBodyConfig(multipart=multipart_config) 

1011 >>> set_parse_body_config(config) 

1012 >>> parse_body_arguments(content_type, multipart_body, {}, {}) 

1013 Traceback (most recent call last): 

1014 ... 

1015 tornado.httputil.HTTPInputError: ...: multipart/form-data parsing is disabled 

1016 >>> set_parse_body_config(ParseBodyConfig()) # reset to defaults 

1017 

1018 .. versionadded:: 6.5.5 

1019 """ 

1020 global _DEFAULT_PARSE_BODY_CONFIG 

1021 _DEFAULT_PARSE_BODY_CONFIG = config 

1022 

1023 

1024def parse_body_arguments( 

1025 content_type: str, 

1026 body: bytes, 

1027 arguments: Dict[str, List[bytes]], 

1028 files: Dict[str, List[HTTPFile]], 

1029 headers: Optional[HTTPHeaders] = None, 

1030 *, 

1031 config: Optional[ParseBodyConfig] = None, 

1032) -> None: 

1033 """Parses a form request body. 

1034 

1035 Supports ``application/x-www-form-urlencoded`` and 

1036 ``multipart/form-data``. The ``content_type`` parameter should be 

1037 a string and ``body`` should be a byte string. The ``arguments`` 

1038 and ``files`` parameters are dictionaries that will be updated 

1039 with the parsed contents. 

1040 """ 

1041 if config is None: 

1042 config = _DEFAULT_PARSE_BODY_CONFIG 

1043 if content_type.startswith("application/x-www-form-urlencoded"): 

1044 if headers and "Content-Encoding" in headers: 

1045 raise HTTPInputError( 

1046 "Unsupported Content-Encoding: %s" % headers["Content-Encoding"] 

1047 ) 

1048 try: 

1049 # real charset decoding will happen in RequestHandler.decode_argument() 

1050 uri_arguments = parse_qs_bytes( 

1051 body, 

1052 keep_blank_values=True, 

1053 max_num_fields=config.urlencoded.max_arguments, 

1054 ) 

1055 except Exception as e: 

1056 raise HTTPInputError("Invalid x-www-form-urlencoded body: %s" % e) from e 

1057 for name, values in uri_arguments.items(): 

1058 if values: 

1059 arguments.setdefault(name, []).extend(values) 

1060 elif content_type.startswith("multipart/form-data"): 

1061 if headers and "Content-Encoding" in headers: 

1062 raise HTTPInputError( 

1063 "Unsupported Content-Encoding: %s" % headers["Content-Encoding"] 

1064 ) 

1065 try: 

1066 fields = content_type.split(";") 

1067 if fields[0].strip() != "multipart/form-data": 

1068 # This catches "Content-Type: multipart/form-dataxyz" 

1069 raise HTTPInputError("Invalid content type") 

1070 for field in fields: 

1071 k, sep, v = field.strip().partition("=") 

1072 if k == "boundary" and v: 

1073 parse_multipart_form_data( 

1074 utf8(v), body, arguments, files, config=config.multipart 

1075 ) 

1076 break 

1077 else: 

1078 raise HTTPInputError("multipart boundary not found") 

1079 except Exception as e: 

1080 raise HTTPInputError("Invalid multipart/form-data: %s" % e) from e 

1081 

1082 

1083def parse_multipart_form_data( 

1084 boundary: bytes, 

1085 data: bytes, 

1086 arguments: Dict[str, List[bytes]], 

1087 files: Dict[str, List[HTTPFile]], 

1088 *, 

1089 config: Optional[ParseMultipartConfig] = None, 

1090) -> None: 

1091 """Parses a ``multipart/form-data`` body. 

1092 

1093 The ``boundary`` and ``data`` parameters are both byte strings. 

1094 The dictionaries given in the arguments and files parameters 

1095 will be updated with the contents of the body. 

1096 

1097 .. versionchanged:: 5.1 

1098 

1099 Now recognizes non-ASCII filenames in RFC 2231/5987 

1100 (``filename*=``) format. 

1101 """ 

1102 if config is None: 

1103 config = _DEFAULT_PARSE_BODY_CONFIG.multipart 

1104 if not config.enabled: 

1105 raise HTTPInputError("multipart/form-data parsing is disabled") 

1106 # The standard allows for the boundary to be quoted in the header, 

1107 # although it's rare (it happens at least for google app engine 

1108 # xmpp). I think we're also supposed to handle backslash-escapes 

1109 # here but I'll save that until we see a client that uses them 

1110 # in the wild. 

1111 if boundary.startswith(b'"') and boundary.endswith(b'"'): 

1112 boundary = boundary[1:-1] 

1113 final_boundary_index = data.rfind(b"--" + boundary + b"--") 

1114 if final_boundary_index == -1: 

1115 raise HTTPInputError("Invalid multipart/form-data: no final boundary found") 

1116 parts = data[:final_boundary_index].split( 

1117 b"--" + boundary + b"\r\n", config.max_parts + 1 

1118 ) 

1119 if len(parts) > config.max_parts: 

1120 raise HTTPInputError("multipart/form-data has too many parts") 

1121 for part in parts: 

1122 if not part: 

1123 continue 

1124 eoh = part.find(b"\r\n\r\n") 

1125 if eoh == -1: 

1126 raise HTTPInputError("multipart/form-data missing headers") 

1127 if eoh > config.max_part_header_size: 

1128 raise HTTPInputError("multipart/form-data part header too large") 

1129 headers = HTTPHeaders.parse(part[:eoh].decode("utf-8"), _chars_are_bytes=False) 

1130 disp_header = headers.get("Content-Disposition", "") 

1131 disposition, disp_params = _parse_header(disp_header) 

1132 if disposition != "form-data" or not part.endswith(b"\r\n"): 

1133 raise HTTPInputError("Invalid multipart/form-data") 

1134 value = part[eoh + 4 : -2] 

1135 if not disp_params.get("name"): 

1136 raise HTTPInputError("multipart/form-data missing name") 

1137 name = disp_params["name"] 

1138 if disp_params.get("filename"): 

1139 ctype = headers.get("Content-Type", "application/unknown") 

1140 files.setdefault(name, []).append( 

1141 HTTPFile( 

1142 filename=disp_params["filename"], body=value, content_type=ctype 

1143 ) 

1144 ) 

1145 else: 

1146 arguments.setdefault(name, []).append(value) 

1147 

1148 

1149def format_timestamp( 

1150 ts: Union[int, float, tuple, time.struct_time, datetime.datetime], 

1151) -> str: 

1152 """Formats a timestamp in the format used by HTTP. 

1153 

1154 The argument may be a numeric timestamp as returned by `time.time`, 

1155 a time tuple as returned by `time.gmtime`, or a `datetime.datetime` 

1156 object. Naive `datetime.datetime` objects are assumed to represent 

1157 UTC; aware objects are converted to UTC before formatting. 

1158 

1159 >>> format_timestamp(1359312200) 

1160 'Sun, 27 Jan 2013 18:43:20 GMT' 

1161 """ 

1162 if isinstance(ts, (int, float)): 

1163 time_num = ts 

1164 elif isinstance(ts, (tuple, time.struct_time)): 

1165 time_num = calendar.timegm(ts) 

1166 elif isinstance(ts, datetime.datetime): 

1167 time_num = calendar.timegm(ts.utctimetuple()) 

1168 else: 

1169 raise TypeError("unknown timestamp type: %r" % ts) 

1170 return email.utils.formatdate(time_num, usegmt=True) 

1171 

1172 

1173class RequestStartLine(typing.NamedTuple): 

1174 method: str 

1175 path: str 

1176 version: str 

1177 

1178 

1179def parse_request_start_line(line: str) -> RequestStartLine: 

1180 """Returns a (method, path, version) tuple for an HTTP 1.x request line. 

1181 

1182 The response is a `typing.NamedTuple`. 

1183 

1184 >>> parse_request_start_line("GET /foo HTTP/1.1") 

1185 RequestStartLine(method='GET', path='/foo', version='HTTP/1.1') 

1186 """ 

1187 match = _ABNF.request_line.fullmatch(line) 

1188 if not match: 

1189 # https://tools.ietf.org/html/rfc7230#section-3.1.1 

1190 # invalid request-line SHOULD respond with a 400 (Bad Request) 

1191 raise HTTPInputError("Malformed HTTP request line") 

1192 r = RequestStartLine(match.group(1), match.group(2), match.group(3)) 

1193 if not r.version.startswith("HTTP/1"): 

1194 # HTTP/2 and above doesn't use parse_request_start_line. 

1195 # This could be folded into the regex but we don't want to deviate 

1196 # from the ABNF in the RFCs. 

1197 raise HTTPInputError("Unexpected HTTP version %r" % r.version) 

1198 return r 

1199 

1200 

1201class ResponseStartLine(typing.NamedTuple): 

1202 version: str 

1203 code: int 

1204 reason: str 

1205 

1206 

1207def parse_response_start_line(line: str) -> ResponseStartLine: 

1208 """Returns a (version, code, reason) tuple for an HTTP 1.x response line. 

1209 

1210 The response is a `typing.NamedTuple`. 

1211 

1212 >>> parse_response_start_line("HTTP/1.1 200 OK") 

1213 ResponseStartLine(version='HTTP/1.1', code=200, reason='OK') 

1214 """ 

1215 match = _ABNF.status_line.fullmatch(line) 

1216 if not match: 

1217 raise HTTPInputError("Error parsing response start line") 

1218 r = ResponseStartLine(match.group(1), int(match.group(2)), match.group(3)) 

1219 if not r.version.startswith("HTTP/1"): 

1220 # HTTP/2 and above doesn't use parse_response_start_line. 

1221 raise HTTPInputError("Unexpected HTTP version %r" % r.version) 

1222 return r 

1223 

1224 

1225# _parseparam and _parse_header are copied and modified from python2.7's cgi.py 

1226# The original 2.7 version of this code did not correctly support some 

1227# combinations of semicolons and double quotes. 

1228# It has also been modified to support valueless parameters as seen in 

1229# websocket extension negotiations, and to support non-ascii values in 

1230# RFC 2231/5987 format. 

1231# 

1232# _parseparam has been further modified with the logic from 

1233# https://github.com/python/cpython/pull/136072/files 

1234# to avoid quadratic behavior when parsing semicolons in quoted strings. 

1235# 

1236# TODO: See if we can switch to email.message.Message for this functionality. 

1237# This is the suggested replacement for the cgi.py module now that cgi has 

1238# been removed from recent versions of Python. We need to verify that 

1239# the email module is consistent with our existing behavior (and all relevant 

1240# RFCs for multipart/form-data) before making this change. 

1241 

1242 

1243def _parseparam(s: str) -> Generator[str, None, None]: 

1244 start = 0 

1245 while s.find(";", start) == start: 

1246 start += 1 

1247 end = s.find(";", start) 

1248 ind, diff = start, 0 

1249 while end > 0: 

1250 diff += s.count('"', ind, end) - s.count('\\"', ind, end) 

1251 if diff % 2 == 0: 

1252 break 

1253 end, ind = ind, s.find(";", end + 1) 

1254 if end < 0: 

1255 end = len(s) 

1256 f = s[start:end] 

1257 yield f.strip() 

1258 start = end 

1259 

1260 

1261def _parse_header(line: str) -> Tuple[str, Dict[str, str]]: 

1262 r"""Parse a Content-type like header. 

1263 

1264 Return the main content-type and a dictionary of options. 

1265 

1266 >>> d = "form-data; foo=\"b\\\\a\\\"r\"; file*=utf-8''T%C3%A4st" 

1267 >>> ct, d = _parse_header(d) 

1268 >>> ct 

1269 'form-data' 

1270 >>> d['file'] == r'T\u00e4st'.encode('ascii').decode('unicode_escape') 

1271 True 

1272 >>> d['foo'] 

1273 'b\\a"r' 

1274 """ 

1275 parts = _parseparam(";" + line) 

1276 key = next(parts) 

1277 # decode_params treats first argument special, but we already stripped key 

1278 params = [("Dummy", "value")] 

1279 for p in parts: 

1280 i = p.find("=") 

1281 if i >= 0: 

1282 name = p[:i].strip().lower() 

1283 value = p[i + 1 :].strip() 

1284 params.append((name, native_str(value))) 

1285 decoded_params = email.utils.decode_params(params) 

1286 decoded_params.pop(0) # get rid of the dummy again 

1287 pdict = {} 

1288 for name, decoded_value in decoded_params: 

1289 value = email.utils.collapse_rfc2231_value(decoded_value) 

1290 if len(value) >= 2 and value[0] == '"' and value[-1] == '"': 

1291 value = value[1:-1] 

1292 pdict[name] = value 

1293 return key, pdict 

1294 

1295 

1296def _encode_header(key: str, pdict: Dict[str, str]) -> str: 

1297 """Inverse of _parse_header. 

1298 

1299 >>> _encode_header('permessage-deflate', 

1300 ... {'client_max_window_bits': 15, 'client_no_context_takeover': None}) 

1301 'permessage-deflate; client_max_window_bits=15; client_no_context_takeover' 

1302 """ 

1303 if not pdict: 

1304 return key 

1305 out = [key] 

1306 # Sort the parameters just to make it easy to test. 

1307 for k, v in sorted(pdict.items()): 

1308 if v is None: 

1309 out.append(k) 

1310 else: 

1311 # TODO: quote if necessary. 

1312 out.append(f"{k}={v}") 

1313 return "; ".join(out) 

1314 

1315 

1316def encode_username_password( 

1317 username: Union[str, bytes], password: Union[str, bytes] 

1318) -> bytes: 

1319 """Encodes a username/password pair in the format used by HTTP auth. 

1320 

1321 The return value is a byte string in the form ``username:password``. 

1322 

1323 .. versionadded:: 5.1 

1324 """ 

1325 if isinstance(username, unicode_type): 

1326 username = unicodedata.normalize("NFC", username) 

1327 if isinstance(password, unicode_type): 

1328 password = unicodedata.normalize("NFC", password) 

1329 return utf8(username) + b":" + utf8(password) 

1330 

1331 

1332def doctests(): 

1333 # type: () -> unittest.TestSuite 

1334 import doctest 

1335 

1336 return doctest.DocTestSuite(optionflags=doctest.ELLIPSIS) 

1337 

1338 

1339_netloc_re = re.compile(r"^(.+):(\d+)$") 

1340 

1341 

1342def split_host_and_port(netloc: str) -> Tuple[str, Optional[int]]: 

1343 """Returns ``(host, port)`` tuple from ``netloc``. 

1344 

1345 Returned ``port`` will be ``None`` if not present. 

1346 

1347 .. versionadded:: 4.1 

1348 """ 

1349 match = _netloc_re.match(netloc) 

1350 if match: 

1351 host = match.group(1) 

1352 port = int(match.group(2)) # type: Optional[int] 

1353 else: 

1354 host = netloc 

1355 port = None 

1356 return (host, port) 

1357 

1358 

1359def qs_to_qsl(qs: Dict[str, List[AnyStr]]) -> Iterable[Tuple[str, AnyStr]]: 

1360 """Generator converting a result of ``parse_qs`` back to name-value pairs. 

1361 

1362 .. versionadded:: 5.0 

1363 """ 

1364 for k, vs in qs.items(): 

1365 for v in vs: 

1366 yield (k, v) 

1367 

1368 

1369_unquote_sub = re.compile(r"\\(?:([0-3][0-7][0-7])|(.))").sub 

1370 

1371 

1372def _unquote_replace(m: re.Match) -> str: 

1373 if m[1]: 

1374 return chr(int(m[1], 8)) 

1375 else: 

1376 return m[2] 

1377 

1378 

1379def _unquote_cookie(s: str) -> str: 

1380 """Handle double quotes and escaping in cookie values. 

1381 

1382 This method is copied verbatim from the Python 3.13 standard 

1383 library (http.cookies._unquote) so we don't have to depend on 

1384 non-public interfaces. 

1385 """ 

1386 # If there aren't any doublequotes, 

1387 # then there can't be any special characters. See RFC 2109. 

1388 if s is None or len(s) < 2: 

1389 return s 

1390 if s[0] != '"' or s[-1] != '"': 

1391 return s 

1392 

1393 # We have to assume that we must decode this string. 

1394 # Down to work. 

1395 

1396 # Remove the "s 

1397 s = s[1:-1] 

1398 

1399 # Check for special sequences. Examples: 

1400 # \012 --> \n 

1401 # \" --> " 

1402 # 

1403 return _unquote_sub(_unquote_replace, s) 

1404 

1405 

1406def parse_cookie(cookie: str) -> Dict[str, str]: 

1407 """Parse a ``Cookie`` HTTP header into a dict of name/value pairs. 

1408 

1409 This function attempts to mimic browser cookie parsing behavior; 

1410 it specifically does not follow any of the cookie-related RFCs 

1411 (because browsers don't either). 

1412 

1413 The algorithm used is identical to that used by Django version 1.9.10. 

1414 

1415 .. versionadded:: 4.4.2 

1416 """ 

1417 cookiedict = {} 

1418 for chunk in cookie.split(";"): 

1419 if "=" in chunk: 

1420 key, val = chunk.split("=", 1) 

1421 else: 

1422 # Assume an empty name per 

1423 # https://bugzilla.mozilla.org/show_bug.cgi?id=169091 

1424 key, val = "", chunk 

1425 key, val = key.strip(), val.strip() 

1426 if key or val: 

1427 # unquote using Python's algorithm. 

1428 cookiedict[key] = _unquote_cookie(val) 

1429 return cookiedict