def __init__(self, ctx: context.Context, tunnel_conn: connection.Server, send_connect: bool): super().__init__(ctx, tunnel_connection=tunnel_conn, conn=ctx.server) assert self.tunnel_connection.address self.conn.via = server_spec.ServerSpec( "https" if self.tunnel_connection.tls else "http", self.tunnel_connection.address) self.buf = ReceiveBuffer() self.send_connect = send_connect
def __init__(self, context: Context, conn: Connection): super().__init__(context, conn) self.buf = ReceiveBuffer()
class Http1Connection(HttpConnection, metaclass=abc.ABCMeta): stream_id: Optional[StreamId] = None request: Optional[http.HTTPRequest] = None response: Optional[http.HTTPResponse] = None request_done: bool = False response_done: bool = False # this is a bit of a hack to make both mypy and PyCharm happy. state: Union[Callable[[events.Event], layer.CommandGenerator[None]], Callable] body_reader: TBodyReader buf: ReceiveBuffer ReceiveProtocolError: Type[Union[RequestProtocolError, ResponseProtocolError]] ReceiveData: Type[Union[RequestData, ResponseData]] ReceiveEndOfMessage: Type[Union[RequestEndOfMessage, ResponseEndOfMessage]] def __init__(self, context: Context, conn: Connection): super().__init__(context, conn) self.buf = ReceiveBuffer() @abc.abstractmethod def send(self, event: HttpEvent) -> layer.CommandGenerator[None]: yield from () # pragma: no cover @abc.abstractmethod def read_headers( self, event: events.ConnectionEvent) -> layer.CommandGenerator[None]: yield from () # pragma: no cover def _handle_event(self, event: events.Event) -> layer.CommandGenerator[None]: if isinstance(event, HttpEvent): yield from self.send(event) else: if isinstance( event, events.DataReceived) and self.state != self.passthrough: self.buf += event.data yield from self.state(event) @expect(events.Start) def start(self, _) -> layer.CommandGenerator[None]: self.state = self.read_headers yield from () state = start def read_body(self, event: events.Event) -> layer.CommandGenerator[None]: assert self.stream_id while True: try: if isinstance(event, events.DataReceived): h11_event = self.body_reader(self.buf) elif isinstance(event, events.ConnectionClosed): h11_event = self.body_reader.read_eof() else: raise AssertionError(f"Unexpected event: {event}") except h11.ProtocolError as e: yield commands.CloseConnection(self.conn) yield ReceiveHttp( self.ReceiveProtocolError(self.stream_id, f"HTTP/1 protocol error: {e}")) return if h11_event is None: return elif isinstance(h11_event, h11.Data): data: bytes = bytes(h11_event.data) if data: yield ReceiveHttp(self.ReceiveData(self.stream_id, data)) elif isinstance(h11_event, h11.EndOfMessage): assert self.request if h11_event.headers: raise NotImplementedError( f"HTTP trailers are not implemented yet.") if self.request.data.method.upper() != b"CONNECT": yield ReceiveHttp(self.ReceiveEndOfMessage(self.stream_id)) is_request = isinstance(self, Http1Server) yield from self.mark_done(request=is_request, response=not is_request) return def wait(self, event: events.Event) -> layer.CommandGenerator[None]: """ We wait for the current flow to be finished before parsing the next message, as we may want to upgrade to WebSocket or plain TCP before that. """ assert self.stream_id if isinstance(event, events.DataReceived): return elif isinstance(event, events.ConnectionClosed): # for practical purposes, we assume that a peer which sent at least a FIN # is not interested in any more data from us, see # see https://github.com/httpwg/http-core/issues/22 if event.connection.state is not ConnectionState.CLOSED: yield commands.CloseConnection(event.connection) yield ReceiveHttp( self.ReceiveProtocolError( self.stream_id, f"Client disconnected.", code=status_codes.CLIENT_CLOSED_REQUEST)) else: # pragma: no cover raise AssertionError(f"Unexpected event: {event}") def done(self, event: events.ConnectionEvent) -> layer.CommandGenerator[None]: yield from () # pragma: no cover def make_pipe(self) -> layer.CommandGenerator[None]: self.state = self.passthrough if self.buf: already_received = self.buf.maybe_extract_at_most(len(self.buf)) yield from self.state( events.DataReceived(self.conn, already_received)) def passthrough(self, event: events.Event) -> layer.CommandGenerator[None]: assert self.stream_id if isinstance(event, events.DataReceived): yield ReceiveHttp(self.ReceiveData(self.stream_id, event.data)) elif isinstance(event, events.ConnectionClosed): if isinstance(self, Http1Server): yield ReceiveHttp(RequestEndOfMessage(self.stream_id)) else: yield ReceiveHttp(ResponseEndOfMessage(self.stream_id)) def mark_done(self, *, request: bool = False, response: bool = False) -> layer.CommandGenerator[None]: if request: self.request_done = True if response: self.response_done = True if self.request_done and self.response_done: assert self.request assert self.response if should_make_pipe(self.request, self.response): yield from self.make_pipe() return connection_done = ( http1.expected_http_body_size(self.request, self.response) == -1 or http1.connection_close(self.request.http_version, self.request.headers) or http1.connection_close(self.response.http_version, self.response.headers) # If we proxy HTTP/2 to HTTP/1, we only use upstream connections for one request. # This simplifies our connection management quite a bit as we can rely on # the proxyserver's max-connection-per-server throttling. or (self.request.is_http2 and isinstance(self, Http1Client))) if connection_done: yield commands.CloseConnection(self.conn) self.state = self.done return self.request_done = self.response_done = False self.request = self.response = None if isinstance(self, Http1Server): self.stream_id += 2 else: self.stream_id = None self.state = self.read_headers if self.buf: yield from self.state(events.DataReceived(self.conn, b""))
class HttpUpstreamProxy(tunnel.TunnelLayer): buf: ReceiveBuffer send_connect: bool conn: connection.Server tunnel_connection: connection.Server def __init__(self, ctx: context.Context, tunnel_conn: connection.Server, send_connect: bool): super().__init__(ctx, tunnel_connection=tunnel_conn, conn=ctx.server) assert self.tunnel_connection.address self.conn.via = server_spec.ServerSpec( "https" if self.tunnel_connection.tls else "http", self.tunnel_connection.address) self.buf = ReceiveBuffer() self.send_connect = send_connect def start_handshake(self) -> layer.CommandGenerator[None]: if self.tunnel_connection.tls: # "Secure Web Proxy": We may have negotiated an ALPN when connecting to the upstream proxy. # The semantics are not really clear here, but we make sure that if we negotiated h2, # we act as an h2 client. self.conn.alpn = self.tunnel_connection.alpn if not self.send_connect: return (yield from super().start_handshake()) assert self.conn.address req = http.Request( host=self.conn.address[0], port=self.conn.address[1], method=b"CONNECT", scheme=b"", authority=f"{self.conn.address[0]}:{self.conn.address[1]}".encode( ), path=b"", http_version=b"HTTP/1.1", headers=http.Headers(), content=b"", trailers=None, timestamp_start=time.time(), timestamp_end=time.time(), ) raw = http1.assemble_request(req) yield commands.SendData(self.tunnel_connection, raw) def receive_handshake_data( self, data: bytes) -> layer.CommandGenerator[Tuple[bool, Optional[str]]]: if not self.send_connect: return (yield from super().receive_handshake_data(data)) self.buf += data response_head = self.buf.maybe_extract_lines() if response_head: response_head = [ bytes(x) for x in response_head ] # TODO: Make url.parse compatible with bytearrays try: response = http1.read_response_head(response_head) except ValueError as e: yield commands.Log( f"{human.format_address(self.tunnel_connection.address)}: {e}" ) return False, str(e) if 200 <= response.status_code < 300: if self.buf: yield from self.receive_data(bytes(self.buf)) del self.buf return True, None else: raw_resp = b"\n".join(response_head) yield commands.Log( f"{human.format_address(self.tunnel_connection.address)}: {raw_resp!r}", level="debug") return False, f"{response.status_code} {response.reason}" else: return False, None
def __init__(self, ctx: context.Context, tunnel_conn: connection.Server, send_connect: bool): super().__init__(ctx, tunnel_connection=tunnel_conn, conn=ctx.server) self.buf = ReceiveBuffer() self.send_connect = send_connect
class HttpUpstreamProxy(tunnel.TunnelLayer): buf: ReceiveBuffer send_connect: bool conn: connection.Server tunnel_connection: connection.Server def __init__(self, ctx: context.Context, tunnel_conn: connection.Server, send_connect: bool): super().__init__(ctx, tunnel_connection=tunnel_conn, conn=ctx.server) self.buf = ReceiveBuffer() self.send_connect = send_connect @classmethod def make(cls, ctx: context.Context, send_connect: bool) -> tunnel.LayerStack: spec = ctx.server.via assert spec assert spec.scheme in ("http", "https") http_proxy = connection.Server(spec.address) stack = tunnel.LayerStack() if spec.scheme == "https": http_proxy.alpn_offers = tls.HTTP1_ALPNS http_proxy.sni = spec.address[0] stack /= tls.ServerTLSLayer(ctx, http_proxy) stack /= cls(ctx, http_proxy, send_connect) return stack def start_handshake(self) -> layer.CommandGenerator[None]: if not self.send_connect: return (yield from super().start_handshake()) assert self.conn.address flow = http.HTTPFlow(self.context.client, self.tunnel_connection) flow.request = http.Request( host=self.conn.address[0], port=self.conn.address[1], method=b"CONNECT", scheme=b"", authority=f"{self.conn.address[0]}:{self.conn.address[1]}".encode( ), path=b"", http_version=b"HTTP/1.1", headers=http.Headers(), content=b"", trailers=None, timestamp_start=time.time(), timestamp_end=time.time(), ) yield HttpConnectUpstreamHook(flow) raw = http1.assemble_request(flow.request) yield commands.SendData(self.tunnel_connection, raw) def receive_handshake_data( self, data: bytes) -> layer.CommandGenerator[Tuple[bool, Optional[str]]]: if not self.send_connect: return (yield from super().receive_handshake_data(data)) self.buf += data response_head = self.buf.maybe_extract_lines() if response_head: response_head = [ bytes(x) for x in response_head ] # TODO: Make url.parse compatible with bytearrays try: response = http1.read_response_head(response_head) except ValueError as e: proxyaddr = human.format_address( self.tunnel_connection.address) yield commands.Log(f"{proxyaddr}: {e}") return False, f"Error connecting to {proxyaddr}: {e}" if 200 <= response.status_code < 300: if self.buf: yield from self.receive_data(bytes(self.buf)) del self.buf return True, None else: proxyaddr = human.format_address( self.tunnel_connection.address) raw_resp = b"\n".join(response_head) yield commands.Log(f"{proxyaddr}: {raw_resp!r}", level="debug") return False, f"Upstream proxy {proxyaddr} refused HTTP CONNECT request: {response.status_code} {response.reason}" else: return False, None