Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/websockets/http11.py: 88%

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

158 statements  

1from __future__ import annotations 

2 

3import dataclasses 

4import os 

5import re 

6import sys 

7import warnings 

8from collections.abc import Generator 

9from typing import Callable 

10 

11from .datastructures import Headers 

12from .exceptions import ( 

13 HeaderLineTooLong, 

14 RequestLineTooLong, 

15 SecurityError, 

16 StatusLineTooLong, 

17 TooManyHeaders, 

18) 

19from .version import version as websockets_version 

20 

21 

22__all__ = [ 

23 "SERVER", 

24 "USER_AGENT", 

25 "Request", 

26 "Response", 

27] 

28 

29 

30PYTHON_VERSION = "{}.{}".format(*sys.version_info) 

31 

32# User-Agent header for HTTP requests. 

33USER_AGENT = os.environ.get( 

34 "WEBSOCKETS_USER_AGENT", 

35 f"Python/{PYTHON_VERSION} websockets/{websockets_version}", 

36) 

37 

38# Server header for HTTP responses. 

39SERVER = os.environ.get( 

40 "WEBSOCKETS_SERVER", 

41 f"Python/{PYTHON_VERSION} websockets/{websockets_version}", 

42) 

43 

44# Maximum total size of headers is around 128 * 8 KiB = 1 MiB. 

45MAX_NUM_HEADERS = int(os.environ.get("WEBSOCKETS_MAX_NUM_HEADERS", "128")) 

46 

47# Limit request line and header lines. 8KiB is the most common default 

48# configuration of popular HTTP servers. 

49MAX_LINE_LENGTH = int(os.environ.get("WEBSOCKETS_MAX_LINE_LENGTH", "8192")) 

50 

51# Support for HTTP response bodies is intended to read an error message 

52# returned by a server. It isn't designed to perform large file transfers. 

53MAX_BODY_SIZE = int(os.environ.get("WEBSOCKETS_MAX_BODY_SIZE", "1_048_576")) # 1 MiB 

54 

55 

56def d(value: bytes | bytearray) -> str: 

57 """ 

58 Decode a bytestring for interpolating into an error message. 

59 

60 """ 

61 return value.decode(errors="backslashreplace") 

62 

63 

64# See https://datatracker.ietf.org/doc/html/rfc7230#appendix-B. 

65 

66# Regex for validating header names. 

67 

68_token_re = re.compile(rb"[-!#$%&\'*+.^_`|~0-9a-zA-Z]+") 

69 

70# Regex for validating header values. 

71 

72# We don't attempt to support obsolete line folding. 

73 

74# Include HTAB (\x09), SP (\x20), VCHAR (\x21-\x7e), obs-text (\x80-\xff). 

75 

76# The ABNF is complicated because it attempts to express that optional 

77# whitespace is ignored. We strip whitespace and don't revalidate that. 

78 

79# See also https://www.rfc-editor.org/errata_search.php?rfc=7230&eid=4189 

80 

81_value_re = re.compile(rb"[\x09\x20-\x7e\x80-\xff]*") 

82 

83# Regexes for validating status codes and numeric header values. 

84# Set a lower limit than default_max_str_digits; 1 EB is plenty. 

85 

86# Content-Length = 1*DIGIT 

87# See https://datatracker.ietf.org/doc/html/rfc9110#section-8.6 

88 

89_digits_re = re.compile(rb"[0-9]{1,18}") 

90 

91# chunk-size = 1*HEXDIG 

92# See https://datatracker.ietf.org/doc/html/rfc9112#section-7.1 

93 

94_hexdigits_re = re.compile(rb"[0-9a-fA-F]{1,15}") 

95 

96 

97@dataclasses.dataclass 

98class Request: 

99 """ 

100 WebSocket handshake request. 

101 

102 ``method`` and ``path`` must contain only ASCII characters. ``headers`` 

103 should contain only ASCII characters; however, non-ASCII header values are 

104 tolerated and encoded as ISO-8859-1. 

105 

106 Attributes: 

107 path: Request path, including optional query. 

108 headers: Request headers. 

109 method: Request method; WebSocket handshake requests use GET. 

110 protocol: Request protocol; WebSocket handshake requests use HTTP/1.1. 

111 """ 

112 

113 path: str 

114 headers: Headers 

115 # method and protocol have a default value, so they're declared after path 

116 # and headers which don't. 

117 method: str = "GET" 

118 protocol: str = "HTTP/1.1" 

119 # body isn't useful is the context of this library. 

120 

121 _exception: Exception | None = None 

122 

123 @property 

124 def exception(self) -> Exception | None: # pragma: no cover 

125 warnings.warn( # deprecated in 10.3 - 2022-04-17 

126 "Request.exception is deprecated; use ServerProtocol.handshake_exc instead", 

127 DeprecationWarning, 

128 ) 

129 return self._exception 

130 

131 @classmethod 

132 def parse( 

133 cls, 

134 read_line: Callable[ 

135 [int, type[Exception]], Generator[None, None, bytes | bytearray] 

136 ], 

137 ) -> Generator[None, None, Request]: 

138 """ 

139 Parse a WebSocket handshake request. 

140 

141 This is a generator-based coroutine. 

142 

143 The request method and path must contain only ASCII characters. The 

144 request path isn't URL-decoded or validated in any way. Request headers 

145 should contain only ASCII characters; however, non-ASCII header values 

146 are tolerated and decoded with ISO-8859-1. 

147 

148 :meth:`parse` doesn't read the request body because WebSocket handshake 

149 requests don't have one. If the request contains a body, it may be read 

150 from the data stream after :meth:`parse` returns. 

151 

152 Args: 

153 read_line: Generator-based coroutine that reads a LF-terminated 

154 line or raises an exception if there isn't enough data 

155 

156 Raises: 

157 EOFError: If the connection is closed without a full HTTP request. 

158 RequestLineTooLong: If the request line is too long. 

159 HeaderLineTooLong: If a header line is too long. 

160 TooManyHeaders: If there are too many headers. 

161 UnicodeDecodeError: If the request method or path isn't ASCII. 

162 ValueError: If the request isn't well formatted. 

163 NotImplementedError: If the request uses Transfer-Encoding. 

164 

165 """ 

166 # https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.1 

167 

168 # Parsing is simple because a fixed value is expected for the version 

169 # and because path isn't checked. Since WebSocket libraries generally 

170 # implement HTTP/1.1 strictly, there's little need for lenient parsing. 

171 

172 try: 

173 request_line = yield from parse_line(read_line, RequestLineTooLong) 

174 except EOFError as exc: 

175 raise EOFError("connection closed while reading HTTP request line") from exc 

176 

177 try: 

178 raw_method, raw_path, raw_protocol = request_line.split(b" ", 2) 

179 except ValueError: # not enough values to unpack (expected 3, got 1-2) 

180 raise ValueError(f"invalid HTTP request line: {d(request_line)}") from None 

181 if raw_protocol not in [b"HTTP/1.1", b"HTTP/1.0"]: 

182 raise ValueError( 

183 f"unsupported protocol; expected HTTP/1.1 or HTTP/1.0: " 

184 f"{d(request_line)}" 

185 ) 

186 method = raw_method.decode("ascii") 

187 protocol = raw_protocol.decode("ascii") 

188 

189 # RFC 9110 defers the definition of URIs to RFC 3986, which allows only 

190 # a subset of ASCII. Non-ASCII IRIs must be UTF-8 then percent-encoded. 

191 path = raw_path.decode("ascii") 

192 

193 headers = yield from parse_headers(read_line) 

194 

195 # https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.3 

196 

197 if "Transfer-Encoding" in headers: 

198 raise NotImplementedError("transfer codings aren't supported") 

199 

200 # Some devices send a Content-Length header with a value of 0. 

201 # Reject any other value. For technical correctness, accept 

202 # multiple Content-Length headers, all with a value of 0. 

203 if not set(headers.get_all("Content-Length")) <= {"0"}: 

204 raise ValueError("unsupported request body") 

205 

206 return cls(path, headers, method, protocol) 

207 

208 def serialize(self) -> bytes: 

209 """ 

210 Serialize a WebSocket handshake request. 

211 

212 """ 

213 # Methods are hardcoded and always ASCII. Non-ASCII paths are converted 

214 # from URI to IRI and percent-encoded. Enforce ASCII as a safety net. 

215 request_line = f"{self.method} {self.path} {self.protocol}\r\n" 

216 request = request_line.encode("ascii") 

217 request += self.headers.serialize() 

218 return request 

219 

220 

221@dataclasses.dataclass 

222class Response: 

223 """ 

224 WebSocket handshake response. 

225 

226 ``reason_phrase`` and ``headers`` should contain only ASCII characters; 

227 however, non-ASCII reason phrases and header values are tolerated and 

228 encoded as ISO-8859-1. 

229 

230 Attributes: 

231 status_code: Response code. 

232 reason_phrase: Response reason. 

233 headers: Response headers. 

234 body: Response body. 

235 

236 """ 

237 

238 status_code: int 

239 reason_phrase: str 

240 headers: Headers 

241 body: bytes | bytearray = b"" 

242 

243 _exception: Exception | None = None 

244 

245 @property 

246 def exception(self) -> Exception | None: # pragma: no cover 

247 warnings.warn( # deprecated in 10.3 - 2022-04-17 

248 "Response.exception is deprecated; " 

249 "use ClientProtocol.handshake_exc instead", 

250 DeprecationWarning, 

251 ) 

252 return self._exception 

253 

254 @classmethod 

255 def parse( 

256 cls, 

257 read_line: Callable[ 

258 [int, type[Exception]], Generator[None, None, bytes | bytearray] 

259 ], 

260 read_exact: Callable[[int], Generator[None, None, bytes | bytearray]], 

261 read_to_eof: Callable[ 

262 [int, type[Exception]], Generator[None, None, bytes | bytearray] 

263 ], 

264 proxy: bool = False, 

265 ) -> Generator[None, None, Response]: 

266 """ 

267 Parse a WebSocket handshake response. 

268 

269 This is a generator-based coroutine. 

270 

271 The reason phrase and headers should contain only ASCII characters; 

272 however, non-ASCII reason phrases and header values are tolerated and 

273 decoded as ISO-8859-1. 

274 

275 Args: 

276 read_line: Generator-based coroutine that reads a LF-terminated 

277 line or raises an exception if there isn't enough data. 

278 read_exact: Generator-based coroutine that reads the requested 

279 bytes or raises an exception if there isn't enough data. 

280 read_to_eof: Generator-based coroutine that reads until the end 

281 of the stream. 

282 

283 Raises: 

284 EOFError: If the connection is closed without a full HTTP response. 

285 StatusLineTooLong: If the status line is too long. 

286 HeaderLineTooLong: If a header line is too long. 

287 TooManyHeaders: If there are too many headers. 

288 SecurityError: If the response body exceeds a security limit. 

289 LookupError: If the response isn't well formatted. 

290 ValueError: If the response isn't well formatted. 

291 NotImplementedError: If the response uses Transfer-Encoding, and 

292 it isn't "chunked". 

293 

294 """ 

295 # https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2 

296 

297 try: 

298 status_line = yield from parse_line(read_line, StatusLineTooLong) 

299 except EOFError as exc: 

300 raise EOFError("connection closed while reading HTTP status line") from exc 

301 

302 try: 

303 protocol, raw_status_code, raw_reason = status_line.split(b" ", 2) 

304 except ValueError: # not enough values to unpack (expected 3, got 1-2) 

305 raise ValueError(f"invalid HTTP status line: {d(status_line)}") from None 

306 if proxy: # some proxies still use HTTP/1.0 

307 if protocol not in [b"HTTP/1.1", b"HTTP/1.0"]: 

308 raise ValueError( 

309 f"unsupported protocol; expected HTTP/1.1 or HTTP/1.0: " 

310 f"{d(status_line)}" 

311 ) 

312 else: 

313 if protocol != b"HTTP/1.1": 

314 raise ValueError( 

315 f"unsupported protocol; expected HTTP/1.1: {d(status_line)}" 

316 ) 

317 if not _digits_re.fullmatch(raw_status_code): 

318 raise ValueError( 

319 f"invalid status code; expected integer; got {d(raw_status_code)}" 

320 ) 

321 status_code = int(raw_status_code) 

322 if not 100 <= status_code < 600: 

323 raise ValueError( 

324 f"invalid status code; expected 100–599; got {d(raw_status_code)}" 

325 ) 

326 if not _value_re.fullmatch(raw_reason): 

327 raise ValueError(f"invalid HTTP reason phrase: {d(raw_reason)}") 

328 

329 # RFC 2616 implies ISO-8859-1. It's easy to reverse and cannot crash. 

330 # Non-ASCII never worked reliably and the reason isn't useful anyway. 

331 reason = raw_reason.decode("iso-8859-1") 

332 

333 headers = yield from parse_headers(read_line) 

334 

335 body: bytes | bytearray 

336 if proxy: 

337 body = b"" 

338 else: 

339 body = yield from read_body( 

340 status_code, headers, read_line, read_exact, read_to_eof 

341 ) 

342 

343 return cls(status_code, reason, headers, body) 

344 

345 def serialize(self) -> bytes: 

346 """ 

347 Serialize a WebSocket handshake response. 

348 

349 """ 

350 # Encode the reason phrase as ISO-8859-1 to round-trip cleanly. 

351 status_line = f"HTTP/1.1 {self.status_code} {self.reason_phrase}\r\n" 

352 response = status_line.encode("iso-8859-1") 

353 response += self.headers.serialize() 

354 response += self.body 

355 return response 

356 

357 

358def parse_line( 

359 read_line: Callable[ 

360 [int, type[Exception]], Generator[None, None, bytes | bytearray] 

361 ], 

362 too_long_exc_type: type[Exception] = SecurityError, 

363) -> Generator[None, None, bytes | bytearray]: 

364 """ 

365 Parse a single line. 

366 

367 CRLF is stripped from the return value. 

368 

369 Args: 

370 read_line: Generator-based coroutine that reads a LF-terminated line 

371 or raises an exception if there isn't enough data. 

372 too_long_exc_type: exception to raise if the line is too long; 

373 defaults to :exc:`SecurityError`. 

374 

375 Raises: 

376 EOFError: If the connection is closed without a CRLF. 

377 SecurityError: If the response exceeds a security limit. 

378 

379 """ 

380 line = yield from read_line(MAX_LINE_LENGTH, too_long_exc_type) 

381 # Not mandatory but safe - https://datatracker.ietf.org/doc/html/rfc7230#section-3.5 

382 if not line.endswith(b"\r\n"): 

383 raise EOFError("line without CRLF") 

384 return line[:-2] 

385 

386 

387def parse_headers( 

388 read_line: Callable[ 

389 [int, type[Exception]], Generator[None, None, bytes | bytearray] 

390 ], 

391) -> Generator[None, None, Headers]: 

392 """ 

393 Parse HTTP headers. 

394 

395 Headers should contain only ASCII characters; however, non-ASCII values are 

396 tolerated and decoded as ISO-8859-1. 

397 

398 Args: 

399 read_line: Generator-based coroutine that reads a LF-terminated line 

400 or raises an exception if there isn't enough data. 

401 

402 Raises: 

403 EOFError: If the connection is closed without complete headers. 

404 HeaderLineTooLong: If a header line is too long. 

405 TooManyHeaders: If there are too many headers. 

406 ValueError: If the request isn't well formatted. 

407 

408 """ 

409 # https://datatracker.ietf.org/doc/html/rfc7230#section-3.2 

410 

411 # We don't attempt to support obsolete line folding. 

412 

413 headers = Headers() 

414 for _ in range(MAX_NUM_HEADERS + 1): 

415 try: 

416 line = yield from parse_line(read_line, HeaderLineTooLong) 

417 except EOFError as exc: 

418 raise EOFError("connection closed while reading HTTP headers") from exc 

419 if line == b"": 

420 break 

421 

422 try: 

423 raw_name, raw_value = line.split(b":", 1) 

424 except ValueError: # not enough values to unpack (expected 2, got 1) 

425 raise ValueError(f"invalid HTTP header line: {d(line)}") from None 

426 if not _token_re.fullmatch(raw_name): 

427 raise ValueError(f"invalid HTTP header name: {d(raw_name)}") 

428 raw_value = raw_value.strip(b" \t") 

429 if not _value_re.fullmatch(raw_value): 

430 raise ValueError(f"invalid HTTP header value: {d(raw_value)}") 

431 

432 name = raw_name.decode("ascii") # guaranteed to be ASCII at this point 

433 # Headers should be ASCII. Section 5.5 of RFC 9110 says: "Historically, 

434 # HTTP allowed field content with text in the ISO-8859-1 charset." and 

435 # "A recipient SHOULD treat other allowed octets in field content (i.e., 

436 # obs-text) as opaque data." ISO-8859-1 is an opaque representation of 

437 # arbitrary binary data in a str object and it is easy to reverse. 

438 value = raw_value.decode("iso-8859-1") 

439 

440 # Since we just validated raw_value, we don't need to revalidate it. 

441 headers.set_insecure(name, value) 

442 

443 else: 

444 raise TooManyHeaders(f"expected no more than {MAX_NUM_HEADERS} headers") 

445 

446 return headers 

447 

448 

449def read_body( 

450 status_code: int, 

451 headers: Headers, 

452 read_line: Callable[ 

453 [int, type[Exception]], Generator[None, None, bytes | bytearray] 

454 ], 

455 read_exact: Callable[[int], Generator[None, None, bytes | bytearray]], 

456 read_to_eof: Callable[ 

457 [int, type[Exception]], Generator[None, None, bytes | bytearray] 

458 ], 

459) -> Generator[None, None, bytes | bytearray]: 

460 # https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.3 

461 

462 # Since websockets only does GET requests (no HEAD, no CONNECT), all 

463 # responses except 1xx, 204, and 304 include a message body. 

464 if 100 <= status_code < 200 or status_code == 204 or status_code == 304: 

465 return b"" 

466 

467 # MultipleValuesError is sufficiently unlikely that we don't attempt to 

468 # handle it when accessing headers. Instead we document that its parent 

469 # class, LookupError, may be raised. 

470 # Conversions from str to int are protected by sys.set_int_max_str_digits. 

471 

472 elif (coding := headers.get("Transfer-Encoding")) is not None: 

473 if coding != "chunked": 

474 raise NotImplementedError(f"transfer coding {coding} isn't supported") 

475 

476 body = b"" 

477 while True: 

478 chunk_size_line = yield from parse_line(read_line, SecurityError) 

479 raw_chunk_size = chunk_size_line.split(b";", 1)[0] 

480 if not _hexdigits_re.fullmatch(raw_chunk_size): 

481 raise SecurityError(f"invalid chunk size: {d(raw_chunk_size)}") 

482 chunk_size = int(raw_chunk_size, 16) 

483 if chunk_size == 0: 

484 break 

485 if len(body) + chunk_size > MAX_BODY_SIZE: 

486 raise SecurityError( 

487 f"chunk too large: {chunk_size} bytes after {len(body)} bytes" 

488 ) 

489 body += yield from read_exact(chunk_size) 

490 if (yield from read_exact(2)) != b"\r\n": 

491 raise ValueError("chunk without CRLF") 

492 # Read the trailer. 

493 yield from parse_headers(read_line) 

494 return body 

495 

496 elif (raw_content_length := headers.get("Content-Length")) is not None: 

497 if not _digits_re.fullmatch(raw_content_length.encode("iso-8859-1")): 

498 raise SecurityError(f"invalid content length: {raw_content_length}") 

499 content_length = int(raw_content_length) 

500 if content_length > MAX_BODY_SIZE: 

501 raise SecurityError(f"body too large: {content_length} bytes") 

502 return (yield from read_exact(content_length)) 

503 

504 else: 

505 return (yield from read_to_eof(MAX_BODY_SIZE, SecurityError))