codekingpro/portable-devtools
115k
1import collections2import time3from collections.abc import Sequence4from enum import Enum5from logging import DEBUG6from logging import ERROR7from typing import Any8from typing import assert_never9from typing import ClassVar10 11import h2.config12import h2.connection13import h2.errors14import h2.events15import h2.exceptions16import h2.settings17import h2.stream18import h2.utilities19 20from ...commands import CloseConnection21from ...commands import Log22from ...commands import RequestWakeup23from ...commands import SendData24from ...context import Context25from ...events import ConnectionClosed26from ...events import DataReceived27from ...events import Event28from ...events import Start29from ...events import Wakeup30from ...layer import CommandGenerator31from ...utils import expect32from . import ErrorCode33from . import RequestData34from . import RequestEndOfMessage35from . import RequestHeaders36from . import RequestProtocolError37from . import RequestTrailers38from . import ResponseData39from . import ResponseEndOfMessage40from . import ResponseHeaders41from . import ResponseProtocolError42from . import ResponseTrailers43from ._base import format_error44from ._base import HttpConnection45from ._base import HttpEvent46from ._base import ReceiveHttp47from ._http_h2 import BufferedH2Connection48from ._http_h2 import H2ConnectionLogger49from mitmproxy import http50from mitmproxy import version51from mitmproxy.connection import Connection52from mitmproxy.net.http import status_codes53from mitmproxy.net.http import url54from mitmproxy.utils import human55 56 57class StreamState(Enum):58 EXPECTING_HEADERS = 159 HEADERS_RECEIVED = 260 61 62CATCH_HYPER_H2_ERRORS = (ValueError, IndexError)63 64 65class Http2Connection(HttpConnection):66 h2_conf: ClassVar[h2.config.H2Configuration]67 h2_conf_defaults: dict[str, Any] = dict(68 header_encoding=False,69 validate_outbound_headers=False,70 # validate_inbound_headers is controlled by the validate_inbound_headers option.71 normalize_inbound_headers=False, # changing this to True is required to pass h2spec72 normalize_outbound_headers=False,73 )74 h2_conn: BufferedH2Connection75 streams: dict[int, StreamState]76 """keep track of all active stream ids to send protocol errors on teardown"""77 78 ReceiveProtocolError: type[RequestProtocolError | ResponseProtocolError]79 ReceiveData: type[RequestData | ResponseData]80 ReceiveTrailers: type[RequestTrailers | ResponseTrailers]81 ReceiveEndOfMessage: type[RequestEndOfMessage | ResponseEndOfMessage]82 83 def __init__(self, context: Context, conn: Connection):84 super().__init__(context, conn)85 if self.debug:86 self.h2_conf.logger = H2ConnectionLogger(87 self.context.client.peername, self.__class__.__name__88 )89 self.h2_conf.validate_inbound_headers = (90 self.context.options.validate_inbound_headers91 )92 self.h2_conn = BufferedH2Connection(self.h2_conf)93 self.streams = {}94 95 def is_closed(self, stream_id: int) -> bool:96 """Check if a non-idle stream is closed"""97 stream = self.h2_conn.streams.get(stream_id, None)98 if (99 stream is not None100 and stream.state_machine.state is not h2.stream.StreamState.CLOSED101 and self.h2_conn.state_machine.state102 is not h2.connection.ConnectionState.CLOSED103 ):104 return False105 else:106 return True107 108 def is_open_for_us(self, stream_id: int) -> bool:109 """Check if we can write to a non-idle stream."""110 stream = self.h2_conn.streams.get(stream_id, None)111 if (112 stream is not None113 and stream.state_machine.state114 is not h2.stream.StreamState.HALF_CLOSED_LOCAL115 and stream.state_machine.state is not h2.stream.StreamState.CLOSED116 and self.h2_conn.state_machine.state117 is not h2.connection.ConnectionState.CLOSED118 ):119 return True120 else:121 return False122 123 def _handle_event(self, event: Event) -> CommandGenerator[None]:124 if isinstance(event, Start):125 self.h2_conn.initiate_connection()126 yield SendData(self.conn, self.h2_conn.data_to_send())127 128 elif isinstance(event, HttpEvent):129 if isinstance(event, (RequestData, ResponseData)):130 if self.is_open_for_us(event.stream_id):131 self.h2_conn.send_data(event.stream_id, event.data)132 elif isinstance(event, (RequestTrailers, ResponseTrailers)):133 if self.is_open_for_us(event.stream_id):134 trailers = [*event.trailers.fields]135 self.h2_conn.send_trailers(event.stream_id, trailers)136 elif isinstance(event, (RequestEndOfMessage, ResponseEndOfMessage)):137 if self.is_open_for_us(event.stream_id):138 self.h2_conn.end_stream(event.stream_id)139 elif isinstance(event, (RequestProtocolError, ResponseProtocolError)):140 if not self.is_closed(event.stream_id):141 stream: h2.stream.H2Stream = self.h2_conn.streams[event.stream_id]142 status = event.code.http_status_code()143 if (144 isinstance(event, ResponseProtocolError)145 and self.is_open_for_us(event.stream_id)146 and not stream.state_machine.headers_sent147 and status is not None148 ):149 self.h2_conn.send_headers(150 event.stream_id,151 [152 (b":status", b"%d" % status),153 (b"server", version.MITMPROXY.encode()),154 (b"content-type", b"text/html"),155 ],156 )157 self.h2_conn.send_data(158 event.stream_id,159 format_error(status, event.message),160 end_stream=True,161 )162 else:163 match event.code:164 case ErrorCode.CANCEL | ErrorCode.CLIENT_DISCONNECTED:165 error_code = h2.errors.ErrorCodes.CANCEL166 case ErrorCode.KILL:167 # XXX: Debateable whether this is the best error code.168 error_code = h2.errors.ErrorCodes.INTERNAL_ERROR169 case ErrorCode.HTTP_1_1_REQUIRED:170 error_code = h2.errors.ErrorCodes.HTTP_1_1_REQUIRED171 case ErrorCode.PASSTHROUGH_CLOSE:172 # FIXME: This probably shouldn't be a protocol error, but an EOM event.173 error_code = h2.errors.ErrorCodes.CANCEL174 case (175 ErrorCode.GENERIC_CLIENT_ERROR176 | ErrorCode.GENERIC_SERVER_ERROR177 | ErrorCode.REQUEST_TOO_LARGE178 | ErrorCode.RESPONSE_TOO_LARGE179 | ErrorCode.CONNECT_FAILED180 | ErrorCode.DESTINATION_UNKNOWN181 | ErrorCode.REQUEST_VALIDATION_FAILED182 | ErrorCode.RESPONSE_VALIDATION_FAILED183 ):184 error_code = h2.errors.ErrorCodes.INTERNAL_ERROR185 case other: # pragma: no cover186 assert_never(other)187 self.h2_conn.reset_stream(event.stream_id, error_code.value)188 else:189 raise AssertionError(f"Unexpected event: {event}")190 data_to_send = self.h2_conn.data_to_send()191 if data_to_send:192 yield SendData(self.conn, data_to_send)193 194 elif isinstance(event, DataReceived):195 try:196 try:197 events = self.h2_conn.receive_data(event.data)198 except CATCH_HYPER_H2_ERRORS as e: # pragma: no cover199 # this should never raise a ValueError, but we triggered one while fuzzing:200 # https://github.com/python-hyper/hyper-h2/issues/1231201 # this stays here as defense-in-depth.202 raise h2.exceptions.ProtocolError(203 f"uncaught hyper-h2 error: {e}"204 ) from e205 except h2.exceptions.ProtocolError as e:206 events = [e]207 208 for h2_event in events:209 if self.debug:210 yield Log(f"{self.debug}[h2] {h2_event}", DEBUG)211 if (yield from self.handle_h2_event(h2_event)):212 if self.debug:213 yield Log(f"{self.debug}[h2] done", DEBUG)214 return215 216 data_to_send = self.h2_conn.data_to_send()217 if data_to_send:218 yield SendData(self.conn, data_to_send)219 220 elif isinstance(event, ConnectionClosed):221 yield from self.close_connection("peer closed connection")222 else:223 raise AssertionError(f"Unexpected event: {event!r}")224 225 def handle_h2_event(self, event: h2.events.Event) -> CommandGenerator[bool]:226 """returns true if further processing should be stopped."""227 if isinstance(event, h2.events.DataReceived):228 state = self.streams.get(event.stream_id, None)229 if state is StreamState.HEADERS_RECEIVED:230 is_empty_eos_data_frame = event.stream_ended and not event.data231 if not is_empty_eos_data_frame:232 yield ReceiveHttp(self.ReceiveData(event.stream_id, event.data))233 elif state is StreamState.EXPECTING_HEADERS:234 yield from self.protocol_error(235 f"Received HTTP/2 data frame, expected headers."236 )237 return True238 self.h2_conn.acknowledge_received_data(239 event.flow_controlled_length, event.stream_id240 )241 elif isinstance(event, h2.events.TrailersReceived):242 trailers = http.Headers(event.headers)243 yield ReceiveHttp(self.ReceiveTrailers(event.stream_id, trailers))244 elif isinstance(event, h2.events.StreamEnded):245 state = self.streams.get(event.stream_id, None)246 if state is StreamState.HEADERS_RECEIVED:247 yield ReceiveHttp(self.ReceiveEndOfMessage(event.stream_id))248 elif state is StreamState.EXPECTING_HEADERS:249 raise AssertionError("unreachable")250 if self.is_closed(event.stream_id):251 self.streams.pop(event.stream_id, None)252 elif isinstance(event, h2.events.StreamReset):253 if event.stream_id in self.streams:254 try:255 err_str = h2.errors.ErrorCodes(event.error_code).name256 except ValueError:257 err_str = str(event.error_code)258 match event.error_code:259 case h2.errors.ErrorCodes.CANCEL:260 err_code = ErrorCode.CANCEL261 case h2.errors.ErrorCodes.HTTP_1_1_REQUIRED:262 err_code = ErrorCode.HTTP_1_1_REQUIRED263 case _:264 err_code = self.ReceiveProtocolError.code265 yield ReceiveHttp(266 self.ReceiveProtocolError(267 event.stream_id,268 f"stream reset by client ({err_str})",269 code=err_code,270 )271 )272 self.streams.pop(event.stream_id)273 else:274 pass # We don't track priority frames which could be followed by a stream reset here.275 elif isinstance(event, h2.exceptions.ProtocolError):276 yield from self.protocol_error(f"HTTP/2 protocol error: {event}")277 return True278 elif isinstance(event, h2.events.ConnectionTerminated):279 yield from self.close_connection(f"HTTP/2 connection closed: {event!r}")280 return True281 # The implementation above isn't really ideal, we should probably only terminate streams > last_stream_id?282 # We currently lack a mechanism to signal that connections are still active but cannot be reused.283 # for stream_id in self.streams:284 # if stream_id > event.last_stream_id:285 # yield ReceiveHttp(self.ReceiveProtocolError(stream_id, f"HTTP/2 connection closed: {event!r}"))286 # self.streams.pop(stream_id)287 elif isinstance(event, h2.events.RemoteSettingsChanged):288 pass289 elif isinstance(event, h2.events.SettingsAcknowledged):290 pass291 elif isinstance(event, h2.events.PriorityUpdated):292 pass293 elif isinstance(event, h2.events.PingReceived):294 pass295 elif isinstance(event, h2.events.PingAckReceived):296 pass297 elif isinstance(event, h2.events.PushedStreamReceived):298 yield Log(299 "Received HTTP/2 push promise, even though we signalled no support.",300 ERROR,301 )302 elif isinstance(event, h2.events.UnknownFrameReceived):303 # https://http2.github.io/http2-spec/#rfc.section.4.1304 # Implementations MUST ignore and discard any frame that has a type that is unknown.305 yield Log(f"Ignoring unknown HTTP/2 frame type: {event.frame.type}")306 elif isinstance(event, h2.events.AlternativeServiceAvailable):307 yield Log(308 "Received HTTP/2 Alt-Svc frame, which will not be forwarded.", DEBUG309 )310 else:311 raise AssertionError(f"Unexpected event: {event!r}")312 return False313 314 def protocol_error(315 self,316 message: str,317 error_code: int = h2.errors.ErrorCodes.PROTOCOL_ERROR,318 ) -> CommandGenerator[None]:319 yield Log(f"{human.format_address(self.conn.peername)}: {message}")320 self.h2_conn.close_connection(error_code, message.encode())321 yield SendData(self.conn, self.h2_conn.data_to_send())322 yield from self.close_connection(message)323 324 def close_connection(self, msg: str) -> CommandGenerator[None]:325 yield CloseConnection(self.conn)326 for stream_id in self.streams:327 yield ReceiveHttp(328 self.ReceiveProtocolError(329 stream_id, msg, self.ReceiveProtocolError.code330 )331 )332 self.streams.clear()333 self._handle_event = self.done # type: ignore334 335 @expect(DataReceived, HttpEvent, ConnectionClosed, Wakeup)336 def done(self, _) -> CommandGenerator[None]:337 yield from ()338 339 340def normalize_h1_headers(341 headers: list[tuple[bytes, bytes]], is_client: bool342) -> list[tuple[bytes, bytes]]:343 # HTTP/1 servers commonly send capitalized headers (Content-Length vs content-length),344 # which isn't valid HTTP/2. As such we normalize.345 # Make sure that this is not just an iterator but an iterable,346 # otherwise hyper-h2 will silently drop headers.347 return list(348 h2.utilities.normalize_outbound_headers(349 headers,350 h2.utilities.HeaderValidationFlags(is_client, False, not is_client, False),351 )352 )353 354 355def normalize_h2_headers(headers: list[tuple[bytes, bytes]]) -> CommandGenerator[None]:356 for i in range(len(headers)):357 if not headers[i][0].islower():358 yield Log(359 f"Lowercased {repr(headers[i][0]).lstrip('b')} header as uppercase is not allowed with HTTP/2."360 )361 headers[i] = (headers[i][0].lower(), headers[i][1])362 363 364def format_h2_request_headers(365 context: Context,366 event: RequestHeaders,367) -> CommandGenerator[list[tuple[bytes, bytes]]]:368 pseudo_headers = [369 (b":method", event.request.data.method),370 (b":scheme", event.request.data.scheme),371 (b":path", event.request.data.path),372 ]373 if event.request.authority:374 pseudo_headers.append((b":authority", event.request.data.authority))375 376 if event.request.is_http2 or event.request.is_http3:377 hdrs = list(event.request.headers.fields)378 if context.options.normalize_outbound_headers:379 yield from normalize_h2_headers(hdrs)380 else:381 headers = event.request.headers382 if not event.request.authority and "host" in headers:383 headers = headers.copy()384 pseudo_headers.append((b":authority", headers.pop(b"host")))385 hdrs = normalize_h1_headers(list(headers.fields), True)386 387 return pseudo_headers + hdrs388 389 390def format_h2_response_headers(391 context: Context,392 event: ResponseHeaders,393) -> CommandGenerator[list[tuple[bytes, bytes]]]:394 headers = [395 (b":status", b"%d" % event.response.status_code),396 *event.response.headers.fields,397 ]398 if event.response.is_http2 or event.response.is_http3:399 if context.options.normalize_outbound_headers:400 yield from normalize_h2_headers(headers)401 else:402 headers = normalize_h1_headers(headers, False)403 return headers404 405 406class Http2Server(Http2Connection):407 h2_conf = h2.config.H2Configuration(408 **Http2Connection.h2_conf_defaults,409 client_side=False,410 )411 412 ReceiveProtocolError = RequestProtocolError413 ReceiveData = RequestData414 ReceiveTrailers = RequestTrailers415 ReceiveEndOfMessage = RequestEndOfMessage416 417 def __init__(self, context: Context):418 super().__init__(context, context.client)419 420 def _handle_event(self, event: Event) -> CommandGenerator[None]:421 if isinstance(event, ResponseHeaders):422 if self.is_open_for_us(event.stream_id):423 self.h2_conn.send_headers(424 event.stream_id,425 headers=(426 yield from format_h2_response_headers(self.context, event)427 ),428 end_stream=event.end_stream,429 )430 yield SendData(self.conn, self.h2_conn.data_to_send())431 else:432 yield from super()._handle_event(event)433 434 def handle_h2_event(self, event: h2.events.Event) -> CommandGenerator[bool]:435 if isinstance(event, h2.events.RequestReceived):436 try:437 (438 host,439 port,440 method,441 scheme,442 authority,443 path,444 headers,445 ) = parse_h2_request_headers(event.headers)446 except ValueError as e:447 yield from self.protocol_error(f"Invalid HTTP/2 request headers: {e}")448 return True449 request = http.Request(450 host=host,451 port=port,452 method=method,453 scheme=scheme,454 authority=authority,455 path=path,456 http_version=b"HTTP/2.0",457 headers=headers,458 content=None,459 trailers=None,460 timestamp_start=time.time(),461 timestamp_end=None,462 )463 self.streams[event.stream_id] = StreamState.HEADERS_RECEIVED464 yield ReceiveHttp(465 RequestHeaders(466 event.stream_id, request, end_stream=bool(event.stream_ended)467 )468 )469 return False470 else:471 return (yield from super().handle_h2_event(event))472 473 474class Http2Client(Http2Connection):475 h2_conf = h2.config.H2Configuration(476 **Http2Connection.h2_conf_defaults,477 client_side=True,478 )479 480 ReceiveProtocolError = ResponseProtocolError481 ReceiveData = ResponseData482 ReceiveTrailers = ResponseTrailers483 ReceiveEndOfMessage = ResponseEndOfMessage484 485 our_stream_id: dict[int, int]486 their_stream_id: dict[int, int]487 stream_queue: collections.defaultdict[int, list[Event]]488 """Queue of streams that we haven't sent yet because we have reached MAX_CONCURRENT_STREAMS"""489 provisional_max_concurrency: int | None = 10490 """A provisional currency limit before we get the server's first settings frame."""491 last_activity: float492 """Timestamp of when we've last seen network activity on this connection."""493 494 def __init__(self, context: Context):495 super().__init__(context, context.server)496 # Disable HTTP/2 push for now to keep things simple.497 # don't send here, that is done as part of initiate_connection().498 self.h2_conn.local_settings.enable_push = 0499 # hyper-h2 pitfall: we need to acknowledge here, otherwise its sends out the old settings.500 self.h2_conn.local_settings.acknowledge()501 self.our_stream_id = {}502 self.their_stream_id = {}503 self.stream_queue = collections.defaultdict(list)504 505 def _handle_event(self, event: Event) -> CommandGenerator[None]:506 # We can't reuse stream ids from the client because they may arrived reordered here507 # and HTTP/2 forbids opening a stream on a lower id than what was previously sent (see test_stream_concurrency).508 # To mitigate this, we transparently map the outside's stream id to our stream id.509 if isinstance(event, HttpEvent):510 ours = self.our_stream_id.get(event.stream_id, None)511 if ours is None:512 no_free_streams = self.h2_conn.open_outbound_streams >= (513 self.provisional_max_concurrency514 or self.h2_conn.remote_settings.max_concurrent_streams515 )516 if no_free_streams:517 self.stream_queue[event.stream_id].append(event)518 return519 ours = self.h2_conn.get_next_available_stream_id()520 self.our_stream_id[event.stream_id] = ours521 self.their_stream_id[ours] = event.stream_id522 event.stream_id = ours523 524 for cmd in self._handle_event2(event):525 if isinstance(cmd, ReceiveHttp):526 cmd.event.stream_id = self.their_stream_id[cmd.event.stream_id]527 yield cmd528 529 can_resume_queue = self.stream_queue and self.h2_conn.open_outbound_streams < (530 self.provisional_max_concurrency531 or self.h2_conn.remote_settings.max_concurrent_streams532 )533 if can_resume_queue:534 # popitem would be LIFO, but we want FIFO.535 events = self.stream_queue.pop(next(iter(self.stream_queue)))536 for event in events:537 yield from self._handle_event(event)538 539 def _handle_event2(self, event: Event) -> CommandGenerator[None]:540 if isinstance(event, Wakeup):541 send_ping_now = (542 # add one second to avoid unnecessary roundtrip, we don't need to be super correct here.543 time.time() - self.last_activity + 1544 > self.context.options.http2_ping_keepalive545 )546 if send_ping_now:547 # PING frames MUST contain 8 octets of opaque data in the payload.548 # A sender can include any value it chooses and use those octets in any fashion.549 self.last_activity = time.time()550 self.h2_conn.ping(b"0" * 8)551 data = self.h2_conn.data_to_send()552 if data is not None:553 yield Log(554 f"Send HTTP/2 keep-alive PING to {human.format_address(self.conn.peername)}",555 DEBUG,556 )557 yield SendData(self.conn, data)558 time_until_next_ping = self.context.options.http2_ping_keepalive - (559 time.time() - self.last_activity560 )561 yield RequestWakeup(time_until_next_ping)562 return563 564 self.last_activity = time.time()565 if isinstance(event, Start):566 if self.context.options.http2_ping_keepalive > 0:567 yield RequestWakeup(self.context.options.http2_ping_keepalive)568 yield from super()._handle_event(event)569 elif isinstance(event, RequestHeaders):570 self.h2_conn.send_headers(571 event.stream_id,572 headers=(yield from format_h2_request_headers(self.context, event)),573 end_stream=event.end_stream,574 )575 self.streams[event.stream_id] = StreamState.EXPECTING_HEADERS576 yield SendData(self.conn, self.h2_conn.data_to_send())577 else:578 yield from super()._handle_event(event)579 580 def handle_h2_event(self, event: h2.events.Event) -> CommandGenerator[bool]:581 if isinstance(event, h2.events.ResponseReceived):582 if (583 self.streams.get(event.stream_id, None)584 is not StreamState.EXPECTING_HEADERS585 ):586 yield from self.protocol_error(f"Received unexpected HTTP/2 response.")587 return True588 589 try:590 status_code, headers = parse_h2_response_headers(event.headers)591 except ValueError as e:592 yield from self.protocol_error(f"Invalid HTTP/2 response headers: {e}")593 return True594 595 response = http.Response(596 http_version=b"HTTP/2.0",597 status_code=status_code,598 reason=b"",599 headers=headers,600 content=None,601 trailers=None,602 timestamp_start=time.time(),603 timestamp_end=None,604 )605 self.streams[event.stream_id] = StreamState.HEADERS_RECEIVED606 yield ReceiveHttp(607 ResponseHeaders(event.stream_id, response, bool(event.stream_ended))608 )609 return False610 elif isinstance(event, h2.events.InformationalResponseReceived):611 # We violate the spec here ("A proxy MUST forward 1xx responses", RFC 7231),612 # but that's probably fine:613 # - 100 Continue is sent by mitmproxy to clients (irrespective of what the server does).614 # - 101 Switching Protocols is not allowed for HTTP/2.615 # - 102 Processing is WebDAV only and also ignorable.616 # - 103 Early Hints is not mission-critical.617 headers = http.Headers(event.headers)618 status: str | int = "<unknown status>"619 try:620 status = int(headers[":status"])621 reason = status_codes.RESPONSES.get(status, "")622 except (KeyError, ValueError):623 reason = ""624 yield Log(f"Swallowing HTTP/2 informational response: {status} {reason}")625 return False626 elif isinstance(event, h2.events.RequestReceived):627 yield from self.protocol_error(628 f"HTTP/2 protocol error: received request from server"629 )630 return True631 elif isinstance(event, h2.events.RemoteSettingsChanged):632 # We have received at least one settings from now,633 # which means we can rely on the max concurrency in remote_settings634 self.provisional_max_concurrency = None635 return (yield from super().handle_h2_event(event))636 else:637 return (yield from super().handle_h2_event(event))638 639 640def split_pseudo_headers(641 h2_headers: Sequence[tuple[bytes, bytes]],642) -> tuple[dict[bytes, bytes], http.Headers]:643 pseudo_headers: dict[bytes, bytes] = {}644 i = 0645 for header, value in h2_headers:646 if header.startswith(b":"):647 if header in pseudo_headers:648 raise ValueError(f"Duplicate HTTP/2 pseudo header: {header!r}")649 pseudo_headers[header] = value650 i += 1651 else:652 # Pseudo-headers must be at the start, we are done here.653 break654 655 headers = http.Headers(h2_headers[i:])656 657 return pseudo_headers, headers658 659 660def parse_h2_request_headers(661 h2_headers: Sequence[tuple[bytes, bytes]],662) -> tuple[str, int, bytes, bytes, bytes, bytes, http.Headers]:663 """Split HTTP/2 pseudo-headers from the actual headers and parse them."""664 pseudo_headers, headers = split_pseudo_headers(h2_headers)665 666 try:667 method: bytes = pseudo_headers.pop(b":method")668 scheme: bytes = pseudo_headers.pop(669 b":scheme"670 ) # this raises for HTTP/2 CONNECT requests671 path: bytes = pseudo_headers.pop(b":path")672 authority: bytes = pseudo_headers.pop(b":authority", b"")673 except KeyError as e:674 raise ValueError(f"Required pseudo header is missing: {e}")675 676 if pseudo_headers:677 raise ValueError(f"Unknown pseudo headers: {pseudo_headers}")678 679 if authority:680 host, port = url.parse_authority(authority, check=True)681 if port is None:682 port = 80 if scheme == b"http" else 443683 else:684 host = ""685 port = 0686 687 return host, port, method, scheme, authority, path, headers688 689 690def parse_h2_response_headers(691 h2_headers: Sequence[tuple[bytes, bytes]],692) -> tuple[int, http.Headers]:693 """Split HTTP/2 pseudo-headers from the actual headers and parse them."""694 pseudo_headers, headers = split_pseudo_headers(h2_headers)695 696 try:697 status_code: int = int(pseudo_headers.pop(b":status"))698 except KeyError as e:699 raise ValueError(f"Required pseudo header is missing: {e}")700 701 if pseudo_headers:702 raise ValueError(f"Unknown pseudo headers: {pseudo_headers}")703 704 return status_code, headers705 706 707__all__ = [708 "format_h2_request_headers",709 "format_h2_response_headers",710 "parse_h2_request_headers",711 "parse_h2_response_headers",712 "Http2Client",713 "Http2Server",714]715 