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

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

504 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 

22from __future__ import annotations 

23 

24import calendar 

25import collections.abc 

26import copy 

27import dataclasses 

28import datetime 

29import email.utils 

30import http.cookies 

31import re 

32import time 

33import unicodedata 

34from functools import lru_cache 

35from http.client import responses 

36from ssl import SSLError 

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

38import warnings 

39 

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

41from tornado.util import ObjectDict, unicode_type 

42 

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

44# Reference it so pyflakes doesn't complain. 

45responses 

46 

47import typing 

48from collections.abc import Awaitable, Generator, Iterable, Iterator, Mapping 

49from typing import ( 

50 AnyStr, 

51) 

52 

53if typing.TYPE_CHECKING: 

54 # These are relatively heavy imports and aren't needed in this file 

55 # unless we're type-checking. 

56 import unittest 

57 from asyncio import Future 

58 

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

60HTTP_WHITESPACE = " \t" 

61 

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

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

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

65 

66 

67class _ABNF: 

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

69 

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

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

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

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

74 """ 

75 

76 # RFC 3986 (URI) 

77 # The URI hostname ABNF is both complex (including detailed validation of IPv4 and IPv6 

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

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

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

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

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

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

84 uri_host = re.compile( 

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

86 ) 

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

88 

89 # RFC 5234 (ABNF) 

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

91 

92 # RFC 9110 (HTTP Semantics) 

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

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

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

96 field_value = re.compile( 

97 rf"|" 

98 rf"{field_vchar.pattern}|" 

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

100 ) 

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

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

103 field_name = token 

104 method = token 

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

106 

107 # RFC 9112 (HTTP/1.1) 

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

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

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

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

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

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

114 request_line = re.compile( 

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

116 ) 

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

118 status_line = re.compile( 

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

120 ) 

121 

122 

123@lru_cache(1000) 

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

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

126 

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

128 'Content-Type' 

129 """ 

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

131 

132 

133class HTTPHeaders(collections.abc.MutableMapping[str, str]): 

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

135 

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

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

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

139 comma. 

140 

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

142 >>> list(h.keys()) 

143 ['Content-Type'] 

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

145 'text/html' 

146 

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

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

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

150 'A=B,C=D' 

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

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

153 

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

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

156 ... 

157 Content-Type: text/html 

158 Set-Cookie: A=B 

159 Set-Cookie: C=D 

160 """ 

161 

162 @typing.overload 

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

164 pass 

165 

166 @typing.overload 

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

168 pass 

169 

170 @typing.overload 

171 def __init__(self, *args: tuple[str, str]) -> None: 

172 pass 

173 

174 @typing.overload 

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

176 pass 

177 

178 def __init__(self, *args: typing.Any, **kwargs: str) -> None: 

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

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

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

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

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

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

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

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

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

188 # Copy constructor 

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

190 self.add(k, v) 

191 else: 

192 # Dict-style initialization 

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

194 

195 # new public methods 

196 

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

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

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

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

201 if _chars_are_bytes: 

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

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

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

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

206 else: 

207 if _FORBIDDEN_HEADER_CHARS_RE.search(value): 

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

209 norm_name = _normalize_header(name) 

210 if norm_name in self: 

211 self._combined_cache.pop(norm_name, None) 

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

213 else: 

214 self[norm_name] = value 

215 

216 def get_list(self, name: str) -> list[str]: 

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

218 norm_name = _normalize_header(name) 

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

220 

221 def get_all(self) -> Iterable[tuple[str, str]]: 

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

223 

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

225 returned with the same name. 

226 """ 

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

228 for value in values: 

229 yield (name, value) 

230 

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

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

233 

234 >>> h = HTTPHeaders() 

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

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

237 'text/html' 

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

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

240 'text/html' 

241 

242 .. versionchanged:: 6.5 

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

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

245 

246 .. deprecated:: 6.5 

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

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

249 will be removed. 

250 

251 .. versionchanged:: 6.6 

252 Support for line folding was removed due to security/DoS concerns. This removal 

253 was ahead of the original removal schedule announced for Tornado 7.0. 

254 Support for LF without CR remains until Tornado 7.0. 

255 

256 .. versionchanged:: 6.6.1 

257 Parsing of header lines terminated by LF without CR will be removed in Tornado 6.7 

258 instead of 7.0 as previously documented. 

259 """ 

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

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

262 # terminator and ignore any preceding CR. 

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

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

265 if not line: 

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

267 return 

268 if line[0] in HTTP_WHITESPACE: 

269 raise HTTPInputError( 

270 "header line cannot start with whitespace, folding not supported" 

271 ) 

272 try: 

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

274 except ValueError: 

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

276 self.add(name, value.strip(HTTP_WHITESPACE), _chars_are_bytes=_chars_are_bytes) 

277 

278 @classmethod 

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

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

281 

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

283 >>> sorted(h.items()) 

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

285 

286 .. versionchanged:: 5.1 

287 

288 Raises `HTTPInputError` on malformed headers instead of a 

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

290 

291 """ 

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

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

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

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

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

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

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

299 # 

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

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

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

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

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

305 # release. 

306 h = cls() 

307 

308 start = 0 

309 while True: 

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

311 if lf == -1: 

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

313 break 

314 line = headers[start : lf + 1] 

315 start = lf + 1 

316 h.parse_line(line, _chars_are_bytes=_chars_are_bytes) 

317 return h 

318 

319 # MutableMapping abstract method implementations. 

320 

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

322 norm_name = _normalize_header(name) 

323 self._combined_cache[norm_name] = value 

324 self._as_list[norm_name] = [value] 

325 

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

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

328 # in __getitem__ when it's not needed. 

329 if not isinstance(name, str): 

330 return False 

331 norm_name = _normalize_header(name) 

332 return norm_name in self._as_list 

333 

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

335 header = _normalize_header(name) 

336 if header not in self._combined_cache: 

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

338 return self._combined_cache[header] 

339 

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

341 norm_name = _normalize_header(name) 

342 # _combined_cache is populated lazily by __getitem__ and invalidated 

343 # by add(), so the name may be absent from it even when the header is 

344 # present. Only _as_list determines whether the key exists. 

345 self._combined_cache.pop(norm_name, None) 

346 del self._as_list[norm_name] 

347 

348 def __len__(self) -> int: 

349 return len(self._as_list) 

350 

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

352 return iter(self._as_list) 

353 

354 def copy(self) -> HTTPHeaders: 

355 # defined in dict but not in MutableMapping. 

356 return HTTPHeaders(self) 

357 

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

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

360 # the appearance that HTTPHeaders is a single container. 

361 __copy__ = copy 

362 

363 def __str__(self) -> str: 

364 lines = [] 

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

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

367 return "".join(lines) 

368 

369 __unicode__ = __str__ 

370 

371 

372class HTTPServerRequest: 

373 """A single HTTP request. 

374 

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

376 

377 .. attribute:: method 

378 

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

380 

381 .. attribute:: uri 

382 

383 The requested uri. 

384 

385 .. attribute:: path 

386 

387 The path portion of `uri` 

388 

389 .. attribute:: query 

390 

391 The query portion of `uri` 

392 

393 .. attribute:: version 

394 

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

396 

397 .. attribute:: headers 

398 

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

400 a case-insensitive dictionary with additional methods for repeated 

401 headers. 

402 

403 .. attribute:: body 

404 

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

406 

407 .. attribute:: remote_ip 

408 

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

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

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

412 

413 .. versionchanged:: 3.1 

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

415 

416 .. attribute:: protocol 

417 

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

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

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

421 

422 .. attribute:: host 

423 

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

425 

426 .. attribute:: arguments 

427 

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

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

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

431 are byte strings. Note that this is different from 

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

433 unicode strings. 

434 

435 .. attribute:: query_arguments 

436 

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

438 from the query string. 

439 

440 .. versionadded:: 3.2 

441 

442 .. attribute:: body_arguments 

443 

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

445 from the request body. 

446 

447 .. versionadded:: 3.2 

448 

449 .. attribute:: files 

450 

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

452 names to lists of `.HTTPFile`. 

453 

454 .. attribute:: connection 

455 

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

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

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

459 sequentially on a single connection. 

460 

461 .. versionchanged:: 4.0 

462 Moved from ``tornado.httpserver.HTTPRequest``. 

463 

464 .. deprecated:: 6.5.2 

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

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

467 temporarily restored in 6.5.2. It will be removed in Tornado 6.7. 

468 

469 .. deprecated:: 6.6 

470 Creating an ``HTTPServerRequest`` without a ``start_line`` argument is deprecated. 

471 This argument will require a non-None value in Tornado 6.7. The ``method``, ``uri``, 

472 and ``version`` arguments are deprecated and will be removed in Tornado 6.7, along 

473 with the previously-deprecated ``host`` argument. At this time all remaining arguments 

474 will become keyword-only. 

475 """ 

476 

477 path: str 

478 query: str 

479 

480 # HACK: Used for stream_request_body 

481 _body_future: Future[None] 

482 

483 def __init__( 

484 self, 

485 method: str | None = None, 

486 uri: str | None = None, 

487 version: str = "HTTP/1.0", 

488 headers: HTTPHeaders | None = None, 

489 body: bytes | None = None, 

490 host: str | None = None, 

491 files: dict[str, list[HTTPFile]] | None = None, 

492 connection: HTTPConnection | None = None, 

493 start_line: RequestStartLine | None = None, 

494 server_connection: object | None = None, 

495 ) -> None: 

496 if method is not None or uri is not None: 

497 warnings.warn( 

498 "The method, uri, and version arguments to HTTPServerRequest are deprecated and " 

499 "will be removed in Tornado 6.7. Use the start_line argument instead.", 

500 DeprecationWarning, 

501 stacklevel=2, 

502 ) 

503 if start_line is None: 

504 warnings.warn( 

505 "The start_line argument to HTTPServerRequest will be required in Tornado 6.7.", 

506 DeprecationWarning, 

507 stacklevel=2, 

508 ) 

509 start_line = RequestStartLine(method or "GET", uri or "/", version) 

510 del method, uri, version 

511 self.method, self.uri, self.version = start_line 

512 

513 self.headers = headers or HTTPHeaders() 

514 if host is not None: 

515 warnings.warn( 

516 "The host argument to HTTPServerRequest is deprecated and will be removed " 

517 "in Tornado 6.7. Use headers['Host'] instead.", 

518 DeprecationWarning, 

519 stacklevel=2, 

520 ) 

521 self.headers["Host"] = host 

522 del host 

523 

524 self.body = body or b"" 

525 

526 # set remote IP and protocol 

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

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

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

530 

531 try: 

532 self.host = self.headers["Host"] 

533 except KeyError: 

534 if self.version == "HTTP/1.0": 

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

536 self.host = "127.0.0.1" 

537 else: 

538 raise HTTPInputError("Missing Host header") 

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

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

541 if "," in self.host: 

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

543 # Server MUST respond with 400 Bad Request if multiple 

544 # Host headers are present. 

545 # 

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

547 # headers received because a proxy may have converted 

548 # multiple headers into a single comma-separated value 

549 # (per RFC 9110 section 5.3). 

550 # 

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

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

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

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

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

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

557 # are supplied). 

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

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

560 self.files = files or {} 

561 self.connection = connection 

562 self.server_connection = server_connection 

563 self._start_time = time.time() 

564 self._finish_time = None 

565 

566 if self.uri is not None: 

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

568 try: 

569 self.arguments = parse_qs_bytes( 

570 self.query, 

571 keep_blank_values=True, 

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

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

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

575 # config because HTTPServerRequest has no access to the 

576 # per-connection configuration; see set_parse_body_config. 

577 max_num_fields=_DEFAULT_PARSE_BODY_CONFIG.urlencoded.max_arguments, 

578 ) 

579 except Exception as e: 

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

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

582 self.body_arguments: dict[str, list[bytes]] = {} 

583 

584 @property 

585 def cookies(self) -> dict[str, http.cookies.Morsel]: 

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

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

588 self._cookies: http.cookies.SimpleCookie = http.cookies.SimpleCookie() 

589 if "Cookie" in self.headers: 

590 try: 

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

592 except Exception: 

593 pass 

594 else: 

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

596 try: 

597 self._cookies[k] = v 

598 except Exception: 

599 # SimpleCookie imposes some restrictions on keys; 

600 # parse_cookie does not. Discard any cookies 

601 # with disallowed keys. 

602 pass 

603 return self._cookies 

604 

605 def full_url(self) -> str: 

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

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

608 

609 def request_time(self) -> float: 

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

611 if self._finish_time is None: 

612 return time.time() - self._start_time 

613 else: 

614 return self._finish_time - self._start_time 

615 

616 def get_ssl_certificate(self, binary_form: bool = False) -> None | dict | bytes: 

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

618 

619 To use client certificates, the HTTPServer's 

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

621 

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

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

624 ssl_ctx.load_verify_locations("cacerts.pem") 

625 ssl_ctx.verify_mode = ssl.CERT_REQUIRED 

626 server = HTTPServer(app, ssl_options=ssl_ctx) 

627 

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

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

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

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

632 details. 

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

634 """ 

635 try: 

636 if self.connection is None: 

637 return None 

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

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

640 binary_form=binary_form 

641 ) 

642 except SSLError: 

643 return None 

644 

645 def _parse_body(self) -> None: 

646 parse_body_arguments( 

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

648 self.body, 

649 self.body_arguments, 

650 self.files, 

651 self.headers, 

652 ) 

653 

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

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

656 

657 def __repr__(self) -> str: 

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

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

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

661 

662 

663class HTTPInputError(Exception): 

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

665 from remote sources. 

666 

667 .. versionadded:: 4.0 

668 """ 

669 

670 pass 

671 

672 

673class HTTPOutputError(Exception): 

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

675 

676 .. versionadded:: 4.0 

677 """ 

678 

679 pass 

680 

681 

682class HTTPServerConnectionDelegate: 

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

684 

685 .. versionadded:: 4.0 

686 """ 

687 

688 def start_request( 

689 self, server_conn: object, request_conn: HTTPConnection 

690 ) -> HTTPMessageDelegate: 

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

692 

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

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

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

696 request/response exchange. 

697 

698 This method should return a `.HTTPMessageDelegate`. 

699 """ 

700 raise NotImplementedError() 

701 

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

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

704 

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

706 passed to ``start_request``. 

707 """ 

708 pass 

709 

710 

711class HTTPMessageDelegate: 

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

713 

714 .. versionadded:: 4.0 

715 """ 

716 

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

718 def headers_received( 

719 self, 

720 start_line: RequestStartLine | ResponseStartLine, 

721 headers: HTTPHeaders, 

722 ) -> Awaitable[None] | None: 

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

724 

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

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

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

728 

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

730 ``headers_received``. 

731 

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

733 until it is done. 

734 """ 

735 pass 

736 

737 def data_received(self, chunk: bytes) -> Awaitable[None] | None: 

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

739 

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

741 """ 

742 pass 

743 

744 def finish(self) -> None: 

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

746 pass 

747 

748 def on_connection_close(self) -> None: 

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

750 

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

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

753 """ 

754 pass 

755 

756 

757class HTTPConnection: 

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

759 

760 .. versionadded:: 4.0 

761 """ 

762 

763 def write_headers( 

764 self, 

765 start_line: RequestStartLine | ResponseStartLine, 

766 headers: HTTPHeaders, 

767 chunk: bytes | None = None, 

768 ) -> Future[None]: 

769 """Write an HTTP header block. 

770 

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

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

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

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

775 headers. 

776 

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

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 write(self, chunk: bytes) -> Future[None]: 

788 """Writes a chunk of body data. 

789 

790 Returns a future for flow control. 

791 

792 .. versionchanged:: 6.0 

793 

794 The ``callback`` argument was removed. 

795 """ 

796 raise NotImplementedError() 

797 

798 def finish(self) -> None: 

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

800 raise NotImplementedError() 

801 

802 

803def url_concat( 

804 url: str, 

805 args: None | dict[str, str] | list[tuple[str, str]] | tuple[tuple[str, str], ...], 

806) -> str: 

807 """Concatenate url and arguments regardless of whether 

808 url has existing query parameters. 

809 

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

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

812 

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

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

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

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

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

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

819 """ 

820 if args is None: 

821 return url 

822 parsed_url = urlparse(url) 

823 if isinstance(args, dict): 

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

825 parsed_query.extend(args.items()) 

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

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

828 parsed_query.extend(args) 

829 else: 

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

831 type(args) 

832 ) 

833 raise TypeError(err) 

834 final_query = urlencode(parsed_query) 

835 url = urlunparse( 

836 ( 

837 parsed_url[0], 

838 parsed_url[1], 

839 parsed_url[2], 

840 parsed_url[3], 

841 final_query, 

842 parsed_url[5], 

843 ) 

844 ) 

845 return url 

846 

847 

848class HTTPFile(ObjectDict): 

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

850 

851 For backwards compatibility, its instance attributes are also 

852 accessible as dictionary keys. 

853 

854 * ``filename`` 

855 * ``body`` 

856 * ``content_type`` 

857 """ 

858 

859 filename: str 

860 body: bytes 

861 content_type: str 

862 

863 

864def _parse_request_range( 

865 range_header: str, 

866) -> tuple[int | None, int | None] | None: 

867 """Parses a Range header. 

868 

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

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

871 this method returns indexes suitable for use in slices. 

872 

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

874 >>> start, end 

875 (1, 3) 

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

877 [1, 2] 

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

879 (6, None) 

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

881 (-6, None) 

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

883 (None, 0) 

884 >>> _parse_request_range("bytes=") 

885 (None, None) 

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

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

888 

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

890 

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

892 

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

894 """ 

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

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

897 if unit != "bytes": 

898 return None 

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

900 try: 

901 start = _int_or_none(start_b) 

902 end = _int_or_none(end_b) 

903 except ValueError: 

904 return None 

905 if end is not None: 

906 if start is None: 

907 if end != 0: 

908 start = -end 

909 end = None 

910 else: 

911 end += 1 

912 return (start, end) 

913 

914 

915def _get_content_range(start: int | None, end: int | None, total: int) -> str: 

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

917 

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

919 bytes 0-0/4 

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

921 bytes 1-2/4 

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

923 bytes 0-3/4 

924 """ 

925 start = start or 0 

926 end = (end or total) - 1 

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

928 

929 

930def _int_or_none(val: str) -> int | None: 

931 val = val.strip() 

932 if val == "": 

933 return None 

934 return int(val) 

935 

936 

937@dataclasses.dataclass 

938class ParseMultipartConfig: 

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

940 

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

942 to avoid potential denial-of-service attacks. 

943 

944 .. versionadded:: 6.5.5 

945 """ 

946 

947 enabled: bool = True 

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

949 

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

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

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

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

954 format. 

955 """ 

956 

957 max_parts: int = 100 

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

959 

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

961 """ 

962 

963 max_part_header_size: int = 10 * 1024 

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

965 

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

967 and content type of the uploaded file. 

968 """ 

969 

970 

971@dataclasses.dataclass 

972class ParseUrlEncodedConfig: 

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

974 

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

976 to avoid potential denial-of-service attacks. 

977 

978 .. versionadded:: 6.5.8 

979 """ 

980 

981 max_arguments: int = 1000 

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

983 

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

985 """ 

986 

987 

988@dataclasses.dataclass 

989class ParseBodyConfig: 

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

991 

992 .. versionadded:: 6.5.5 

993 """ 

994 

995 multipart: ParseMultipartConfig = dataclasses.field( 

996 default_factory=ParseMultipartConfig 

997 ) 

998 urlencoded: ParseUrlEncodedConfig = dataclasses.field( 

999 default_factory=ParseUrlEncodedConfig 

1000 ) 

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

1002 

1003 

1004_DEFAULT_PARSE_BODY_CONFIG = ParseBodyConfig() 

1005 

1006 

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

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

1009 

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

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

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

1013 release. 

1014 

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

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

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

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

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

1020 >>> set_parse_body_config(config) 

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

1022 Traceback (most recent call last): 

1023 ... 

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

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

1026 

1027 .. versionadded:: 6.5.5 

1028 """ 

1029 global _DEFAULT_PARSE_BODY_CONFIG 

1030 _DEFAULT_PARSE_BODY_CONFIG = config 

1031 

1032 

1033def parse_body_arguments( 

1034 content_type: str, 

1035 body: bytes, 

1036 arguments: dict[str, list[bytes]], 

1037 files: dict[str, list[HTTPFile]], 

1038 headers: HTTPHeaders | None = None, 

1039 *, 

1040 config: ParseBodyConfig | None = None, 

1041) -> None: 

1042 """Parses a form request body. 

1043 

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

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

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

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

1048 with the parsed contents. 

1049 """ 

1050 if config is None: 

1051 config = _DEFAULT_PARSE_BODY_CONFIG 

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

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

1054 raise HTTPInputError( 

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

1056 ) 

1057 try: 

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

1059 uri_arguments = parse_qs_bytes( 

1060 body, 

1061 keep_blank_values=True, 

1062 max_num_fields=config.urlencoded.max_arguments, 

1063 ) 

1064 except Exception as e: 

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

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

1067 if values: 

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

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

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

1071 raise HTTPInputError( 

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

1073 ) 

1074 try: 

1075 fields = content_type.split(";") 

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

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

1078 raise HTTPInputError("Invalid content type") 

1079 for field in fields: 

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

1081 if k == "boundary" and v: 

1082 parse_multipart_form_data( 

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

1084 ) 

1085 break 

1086 else: 

1087 raise HTTPInputError("multipart boundary not found") 

1088 except Exception as e: 

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

1090 

1091 

1092def parse_multipart_form_data( 

1093 boundary: bytes, 

1094 data: bytes, 

1095 arguments: dict[str, list[bytes]], 

1096 files: dict[str, list[HTTPFile]], 

1097 *, 

1098 config: ParseMultipartConfig | None = None, 

1099) -> None: 

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

1101 

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

1103 The dictionaries given in the arguments and files parameters 

1104 will be updated with the contents of the body. 

1105 

1106 .. versionchanged:: 5.1 

1107 

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

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

1110 """ 

1111 if config is None: 

1112 config = _DEFAULT_PARSE_BODY_CONFIG.multipart 

1113 if not config.enabled: 

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

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

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

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

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

1119 # in the wild. 

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

1121 boundary = boundary[1:-1] 

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

1123 if final_boundary_index == -1: 

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

1125 parts = data[:final_boundary_index].split( 

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

1127 ) 

1128 if len(parts) > config.max_parts: 

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

1130 for part in parts: 

1131 if not part: 

1132 continue 

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

1134 if eoh == -1: 

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

1136 if eoh > config.max_part_header_size: 

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

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

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

1140 disposition, disp_params = _parse_header(disp_header) 

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

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

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

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

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

1146 name = disp_params["name"] 

1147 if disp_params.get("filename"): 

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

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

1150 HTTPFile( 

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

1152 ) 

1153 ) 

1154 else: 

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

1156 

1157 

1158def format_timestamp( 

1159 ts: int | float | tuple | time.struct_time | datetime.datetime, 

1160) -> str: 

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

1162 

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

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

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

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

1167 

1168 >>> format_timestamp(1359312200) 

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

1170 """ 

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

1172 time_num = ts 

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

1174 time_num = calendar.timegm(ts) 

1175 elif isinstance(ts, datetime.datetime): 

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

1177 else: 

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

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

1180 

1181 

1182class RequestStartLine(typing.NamedTuple): 

1183 method: str 

1184 path: str 

1185 version: str 

1186 

1187 

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

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

1190 

1191 The response is a `typing.NamedTuple`. 

1192 

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

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

1195 """ 

1196 match = _ABNF.request_line.fullmatch(line) 

1197 if not match: 

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

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

1200 raise HTTPInputError("Malformed HTTP request line") 

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

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

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

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

1205 # from the ABNF in the RFCs. 

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

1207 return r 

1208 

1209 

1210class ResponseStartLine(typing.NamedTuple): 

1211 version: str 

1212 code: int 

1213 reason: str 

1214 

1215 

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

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

1218 

1219 The response is a `typing.NamedTuple`. 

1220 

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

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

1223 """ 

1224 match = _ABNF.status_line.fullmatch(line) 

1225 if not match: 

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

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

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

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

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

1231 return r 

1232 

1233 

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

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

1236# combinations of semicolons and double quotes. 

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

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

1239# RFC 2231/5987 format. 

1240# 

1241# _parseparam has been further modified with the logic from 

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

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

1244# 

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

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

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

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

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

1250 

1251 

1252def _parseparam(s: str) -> Generator[str]: 

1253 start = 0 

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

1255 start += 1 

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

1257 ind, diff = start, 0 

1258 while end > 0: 

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

1260 if diff % 2 == 0: 

1261 break 

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

1263 if end < 0: 

1264 end = len(s) 

1265 f = s[start:end] 

1266 yield f.strip() 

1267 start = end 

1268 

1269 

1270def _parse_header(line: str) -> tuple[str, dict[str, str]]: 

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

1272 

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

1274 

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

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

1277 >>> ct 

1278 'form-data' 

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

1280 True 

1281 >>> d['foo'] 

1282 'b\\a"r' 

1283 """ 

1284 parts = _parseparam(";" + line) 

1285 key = next(parts) 

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

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

1288 for p in parts: 

1289 i = p.find("=") 

1290 if i >= 0: 

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

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

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

1294 decoded_params = email.utils.decode_params(params) 

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

1296 pdict = {} 

1297 for name, decoded_value in decoded_params: 

1298 value = email.utils.collapse_rfc2231_value(decoded_value) 

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

1300 value = value[1:-1] 

1301 pdict[name] = value 

1302 return key, pdict 

1303 

1304 

1305def _encode_header(key: str, pdict: dict[str, str]) -> str: 

1306 """Inverse of _parse_header. 

1307 

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

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

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

1311 """ 

1312 if not pdict: 

1313 return key 

1314 out = [key] 

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

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

1317 if v is None: 

1318 out.append(k) 

1319 else: 

1320 # TODO: quote if necessary. 

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

1322 return "; ".join(out) 

1323 

1324 

1325def encode_username_password(username: str | bytes, password: str | bytes) -> bytes: 

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

1327 

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

1329 

1330 .. versionadded:: 5.1 

1331 """ 

1332 if isinstance(username, unicode_type): 

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

1334 if isinstance(password, unicode_type): 

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

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

1337 

1338 

1339def doctests() -> unittest.TestSuite: 

1340 import doctest 

1341 

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

1343 

1344 

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

1346 

1347 

1348def split_host_and_port(netloc: str) -> tuple[str, int | None]: 

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

1350 

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

1352 

1353 .. versionadded:: 4.1 

1354 """ 

1355 match = _netloc_re.match(netloc) 

1356 if match: 

1357 host = match.group(1) 

1358 port: int | None = int(match.group(2)) 

1359 else: 

1360 host = netloc 

1361 port = None 

1362 return (host, port) 

1363 

1364 

1365def qs_to_qsl(qs: dict[str, list[AnyStr]]) -> Iterable[tuple[str, AnyStr]]: 

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

1367 

1368 .. versionadded:: 5.0 

1369 """ 

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

1371 for v in vs: 

1372 yield (k, v) 

1373 

1374 

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

1376 

1377 

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

1379 if m[1]: 

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

1381 else: 

1382 return m[2] 

1383 

1384 

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

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

1387 

1388 This method is copied verbatim from the Python 3.13 standard 

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

1390 non-public interfaces. 

1391 """ 

1392 # If there aren't any doublequotes, 

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

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

1395 return s 

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

1397 return s 

1398 

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

1400 # Down to work. 

1401 

1402 # Remove the "s 

1403 s = s[1:-1] 

1404 

1405 # Check for special sequences. Examples: 

1406 # \012 --> \n 

1407 # \" --> " 

1408 # 

1409 return _unquote_sub(_unquote_replace, s) 

1410 

1411 

1412def parse_cookie(cookie: str) -> dict[str, str]: 

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

1414 

1415 This function attempts to mimic browser cookie parsing behavior; 

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

1417 (because browsers don't either). 

1418 

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

1420 

1421 .. versionadded:: 4.4.2 

1422 """ 

1423 cookiedict = {} 

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

1425 if "=" in chunk: 

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

1427 else: 

1428 # Assume an empty name per 

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

1430 key, val = "", chunk 

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

1432 if key or val: 

1433 # unquote using Python's algorithm. 

1434 cookiedict[key] = _unquote_cookie(val) 

1435 return cookiedict