Example #1
0
    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
Example #2
0
 def __init__(self, context: Context, conn: Connection):
     super().__init__(context, conn)
     self.buf = ReceiveBuffer()
Example #3
0
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""))
Example #4
0
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
Example #5
0
 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
Example #6
0
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