async def test_http1_request(event_loop: asyncio.AbstractEventLoop) -> None: server = TCPServer( sanity_framework, event_loop, Config(), WorkerContext(), MemoryReader(), MemoryWriter() # type: ignore # noqa: E501 ) task = event_loop.create_task(server.run()) client = h11.Connection(h11.CLIENT) await server.reader.send( # type: ignore client.send( h11.Request( method="POST", target="/", headers=[ (b"host", b"hypercorn"), (b"connection", b"close"), (b"content-length", b"%d" % len(SANITY_BODY)), ], ))) await server.reader.send(client.send(h11.Data(data=SANITY_BODY)) ) # type: ignore await server.reader.send(client.send(h11.EndOfMessage())) # type: ignore events = [] while True: event = client.next_event() if event == h11.NEED_DATA: data = await server.writer.receive() # type: ignore client.receive_data(data) elif isinstance(event, h11.ConnectionClosed): break else: events.append(event) assert events == [ h11.Response( status_code=200, headers=[ (b"content-length", b"15"), (b"date", b"Thu, 01 Jan 1970 01:23:20 GMT"), (b"server", b"hypercorn-h11"), (b"connection", b"close"), ], http_version=b"1.1", reason=b"", ), h11.Data(data=b"Hello & Goodbye"), h11.EndOfMessage(headers=[]), # type: ignore ] server.reader.close() # type: ignore await task
async def test_client_sends_chunked( event_loop: asyncio.AbstractEventLoop, ) -> None: connection = MockConnection(event_loop) chunked_headers = [('transfer-encoding', 'chunked'), ('expect', '100-continue')] await connection.send( h11.Request(method='POST', target='/echo', headers=BASIC_HEADERS + chunked_headers), ) await connection.transport.updated.wait() informational_response = connection.get_events()[0] assert isinstance(informational_response, h11.InformationalResponse) assert informational_response.status_code == 100 connection.transport.clear() for chunk in [b'chunked ', b'data']: await connection.send( h11.Data(data=chunk, chunk_start=True, chunk_end=True)) await connection.send(h11.EndOfMessage()) await connection.transport.closed.wait() response, *data, end = connection.get_events() assert isinstance(response, h11.Response) assert response.status_code == 200 assert all(isinstance(datum, h11.Data) for datum in data) data = json.loads(b''.join(datum.data for datum in data).decode()) assert data['request_body'] == 'chunked data' # type: ignore assert isinstance(end, h11.EndOfMessage)
def _send_fatal_error(self, exc): status_code = getattr(exc, 'error_status_hint', 500) self._logger.debug('sending error response, status %d', status_code) try: self.send_event( h11.Response( status_code=status_code, reason=turq.util.http.default_reason(status_code).encode(), headers=[ (b'Date', turq.util.http.date().encode()), (b'Content-Type', b'text/plain'), (b'Connection', b'close'), ], )) self.send_event(h11.Data(data=('Error: %s\r\n' % exc).encode())) self.send_event(h11.EndOfMessage()) except Exception as e: self._logger.debug('cannot send error response: %s', e) # A crude way to avoid the TCP reset problem (RFC 7230 Section 6.6). try: self._socket.shutdown(socket.SHUT_WR) while self._socket.recv(1024): self._logger.debug('discarding data from client') except OSError: # The client may have already closed the connection pass
async def handle_client_http(self, stream, event, conn): # TODO: right now we handle a single request then close the connection # hence HTTP 1.1 keep-alive is not supported req = HTTPRequest.from_h11_req(event) rep = await self.http.handle_request(req) if self.config.debug: server_header = f"guardata/{guardata_version} {h11.PRODUCT_ID}" else: server_header = "guardata" rep.headers.append(("server", server_header)) # Tell no support for keep-alive (h11 will know what to do from there) rep.headers.append(("connection", "close")) try: response_data = bytearray( conn.send( h11.Response( status_code=rep.status_code, headers=rep.headers, reason=rep.reason ) ) ) if rep.data: response_data += conn.send(h11.Data(data=rep.data)) response_data += conn.send(h11.EndOfMessage()) await stream.send_all(response_data) except trio.BrokenResourceError: # Peer is already gone, nothing to do pass
def end_of_body(self): """ This method marks the end of the body. It should be called once all the body data has been sent (if there is any), and may potentially emit more bytes. """ return self._conn.send(h11.EndOfMessage())
def handle(self): with closing(self.request) as s: c = h11.Connection(h11.SERVER) request = None while True: event = c.next_event() if event is h11.NEED_DATA: # Use a small read buffer to make things more challenging # and exercise more paths :-) c.receive_data(s.recv(10)) continue if type(event) is h11.Request: request = event if type(event) is h11.EndOfMessage: break info = json.dumps({ "method": request.method.decode("ascii"), "target": request.target.decode("ascii"), "headers": { name.decode("ascii"): value.decode("ascii") for (name, value) in request.headers }, }) s.sendall(c.send(h11.Response(status_code=200, headers=[]))) s.sendall(c.send(h11.Data(data=info.encode("ascii")))) s.sendall(c.send(h11.EndOfMessage()))
async def send(self, wrapper): headers = wrapper.server.create_headers() + self.headers await wrapper.send( h11.Response(status_code=self.status_code, headers=headers)) if self.body: await wrapper.send(h11.Data(data=self.body)) await wrapper.send(h11.EndOfMessage())
async def test_client_sends_chunked( event_loop: asyncio.AbstractEventLoop, ) -> None: connection = MockConnection(event_loop) chunked_headers = [("transfer-encoding", "chunked"), ("expect", "100-continue")] await connection.send( h11.Request(method="POST", target="/echo", headers=BASIC_HEADERS + chunked_headers)) await connection.transport.updated.wait() informational_response = connection.get_events()[0] assert isinstance(informational_response, h11.InformationalResponse) assert informational_response.status_code == 100 connection.transport.clear() for chunk in [b"chunked ", b"data"]: await connection.send( h11.Data(data=chunk, chunk_start=True, chunk_end=True)) await connection.send(h11.EndOfMessage()) await connection.transport.closed.wait() response, *data, end = connection.get_events() assert isinstance(response, h11.Response) assert response.status_code == 200 assert all(isinstance(datum, h11.Data) for datum in data) data = json.loads(b"".join(datum.data for datum in data).decode()) assert data["request_body"] == "chunked data" # type: ignore assert isinstance(end, h11.EndOfMessage)
def send_error(self, event: h11.Request, status: HTTPStatus, msg: str = None, explain: str = None): try: short_msg, long_msg = responses[status] except KeyError: short_msg, long_msg = '???', '???' if msg is None: msg = short_msg if explain is None: explain = long_msg headers = [] self.log.error('code {}, message {}'.format(status, msg)) body = None if status >= 200 and status not in (HTTPStatus.NO_CONTENT, HTTPStatus.RESET_CONTENT, HTTPStatus.NOT_MODIFIED): body = self.error_message_format.format(code=status, message=msg, explain=explain).encode('UTF-8', 'replace') headers.extend([('Content-Type', self.error_content_type), ('Content-Length', str(len(body)))]) headers.append(('Connection', 'close')) response = h11.Response(status_code=status, headers=headers) self.send(response) if event.method != 'HEAD' and body: self.send(h11.Data(data=body)) self.send(h11.EndOfMessage())
async def connect(self, data=None, _json=None, headers=[]): self.stream = await NetworkStream.connect(self.hostname, self.port) if self.scheme == 'https': self.stream = SSLStream(self.stream, self.hostname) outheaders = [ ('Host', self.hostname), ('Connection', 'close'), ('Accept-Encoding', 'gzip'), ] if _json is not None: # overrides data data = json.dumps(_json) outheaders.append(('Content-Type', 'application/json')) if data is not None: if hasattr(data, 'items'): data = urllib.parse.urlencode(data) outheaders.append( ('Content-Type', 'application/x-www-form-urlencoded')) if hasattr(data, 'encode'): data = data.encode('UTF-8') outheaders.append(('Content-Length', str(len(data)))) outheaders.extend(headers) await self.send( h11.Request(method=self.method, target=self.qs, headers=outheaders)) if data is not None: await self.send(h11.Data(data=data)) await self.send(h11.EndOfMessage()) self.connected = True self.log.debug('%s', f'{self.url}: connected to {self.stream!r}')
async def handle_request( server_handle: ServerHandle, wrapper: ClientWrapper, request: h11.Request, ) -> None: body = b"" while True: event = await wrapper.next_event() if isinstance(event, h11.EndOfMessage): break body += cast(h11.Data, event).data method_name, args = parse_request(body, dict(request.headers)) try: method = lookup_method(server_handle, method_name) result = await method(*args) root = format_success(result) status_code = 200 except Exception as exc: root = format_error(exc) status_code = 500 data = build_xml(root) headers = wrapper.basic_headers() headers.append((b"Content-length", str(len(data)).encode())) response = h11.Response(status_code=status_code, headers=headers) await wrapper.send(response) await wrapper.send(h11.Data(data=data)) await wrapper.send(h11.EndOfMessage())
def test_response_framing_1_content_length(example): resp, data, _ = example.send( h11.Request(method='GET', target='/', headers=[('Host', 'example')]), h11.EndOfMessage()) assert (b'content-length', b'14') in resp.headers assert b'transfer-encoding' not in dict(resp.headers) assert data.data == b'Hello world!\r\n'
def test_handshake_rejection_with_body() -> None: events = _make_handshake_rejection(400, body=b"Hello") assert events == [ h11.Response(headers=[(b"content-length", b"5")], status_code=400), h11.Data(data=b"Hello"), h11.EndOfMessage(), ]
async def _send_http_reply( self, stream: Stream, conn: h11.Connection, status_code: int, headers: Dict[bytes, bytes] = {}, data: Optional[bytes] = None, ) -> None: reason = HTTPStatus(status_code).phrase headers = list({ **headers, # Add default headers b"server": self.server_header, b"date": format_date_time(None).encode("ascii"), b"content-Length": str(len(data or b"")).encode("ascii"), # Inform we don't support keep-alive (h11 will know what to do from there) b"connection": b"close", }.items()) try: await stream.send_all( conn.send( h11.Response(status_code=status_code, headers=headers, reason=reason))) if data: await stream.send_all(conn.send(h11.Data(data=data))) await stream.send_all(conn.send(h11.EndOfMessage())) except trio.BrokenResourceError: # Given we don't support keep-alive, the connection is going to be # shutdown anyway, so we can safely ignore the fact peer has left pass
def _handle_error(self) -> None: self._send(h11.Response( status_code=400, headers=chain( [('content-length', '0'), ('connection', 'close')], self.response_headers(), ), )) self._send(h11.EndOfMessage())
async def test_http1_keep_alive_during( client_stream: trio.testing._memory_streams.MemorySendStream, ) -> None: client = h11.Connection(h11.CLIENT) await client_stream.send_all(client.send(REQUEST)) await trio.sleep(2 * KEEP_ALIVE_TIMEOUT) # Key is that this doesn't error await client_stream.send_all(client.send(h11.EndOfMessage()))
def _send_body(self): if self._response.body: self.chunk(self._response.body) self._log_headers(self._response.raw_headers) self._handler.send_event(h11.EndOfMessage( headers=_encode_headers(self._response.raw_headers), ))
async def _handle_websocket(self, websocket: Websocket) -> None: response = await self.app.handle_websocket(websocket) if response is not None: if self.active: self.connection.close( wsproto.connection.CloseReason.INTERNAL_ERROR) self.write(self.connection.bytes_to_send()) else: headers = chain( ((key, value) for key, value in response.headers.items()), self.response_headers(), ) self.write( self.connection._upgrade_connection.send( h11.Response(status_code=response.status_code, headers=headers), ), ) if not suppress_body('GET', response.status_code): async for data in response.response: self.write( self.connection._upgrade_connection.send( h11.Data(data=data)), ) await self.drain() self.write( self.connection._upgrade_connection.send( h11.EndOfMessage()), ) self.close()
async def asgi_send(self, message: dict) -> None: """Called by the ASGI instance to send a message.""" if message["type"] == "http.response.start" and self.state == ASGIHTTPState.REQUEST: self.response = message elif message["type"] == "http.response.body" and self.state in { ASGIHTTPState.REQUEST, ASGIHTTPState.RESPONSE, }: if self.state == ASGIHTTPState.REQUEST: headers = chain( ( (bytes(key).strip(), bytes(value).strip()) for key, value in self.response["headers"] ), self.response_headers(), ) await self.asend( h11.Response(status_code=int(self.response["status"]), headers=headers) ) self.state = ASGIHTTPState.RESPONSE if ( not suppress_body(self.scope["method"], int(self.response["status"])) and message.get("body", b"") != b"" ): await self.asend(h11.Data(data=bytes(message["body"]))) if not message.get("more_body", False): if self.state != ASGIHTTPState.CLOSED: await self.asend(h11.EndOfMessage()) await self.asgi_put({"type": "http.disconnect"}) self.state = ASGIHTTPState.CLOSED else: raise UnexpectedMessage(self.state, message["type"])
async def post(self, target: str, body: bytes) -> bytes: # send request data = self.connection.send( h11.Request(method="POST", target=target, headers=[ *self.get_headers(), ("Content-Length", str(len(body))) ])) data += self.connection.send(h11.Data(data=body)) data += self.connection.send(h11.EndOfMessage()) await self.write(data) # get response response = await self.next_event() data = await self.next_event() eom = await self.next_event() print(response) print(data) print(eom) set_cookie = lookup_alist(response.headers, b"set-cookie") if set_cookie is not None: self.cookie = set_cookie if response.status_code >= 300: raise Exception("error posting", data.data) self.connection.start_next_cycle() return data.data
async def send_events(connection, stream, events): for event in events: data = connection.send(event) await stream.send_all(data) if not isinstance(event, h11.EndOfMessage): data = connection.send(h11.EndOfMessage()) await stream.send_all(data)
async def respond_to_h11(self, h11_conn, event): """ Most generic response to an h11 connection possible. """ stream = H11Stream(h11_conn, event, (None, None)) print("stream:", stream) handler = self.router.match(stream) print("handler:", handler) print("htype:", type(handler)) handler_result = await handler(stream) if handler_result is not None: status, response = handler_result else: status, response = None, None print("response:", response) if not stream._headers_sent: content_type, response = response_to_bytes(handler, response) content_length = str(len(response)) headers = h11_conn.basic_headers() headers.append(('Content-Type', content_type)) headers.append(('Content-Length', content_length)) resp = h11.Response(status_code=status, headers=headers) await h11_conn.send(resp) if response: await h11_conn.send(h11.Data(data=response)) await h11_conn.send(h11.EndOfMessage()) await h11_conn.close()
async def stream_send(self, event: StreamEvent) -> None: if isinstance(event, Response): if event.status_code >= 200: await self._send_h11_event( h11.Response( headers=chain(event.headers, self.config.response_headers("h11")), status_code=event.status_code, )) else: await self._send_h11_event( h11.InformationalResponse( headers=chain(event.headers, self.config.response_headers("h11")), status_code=event.status_code, )) elif isinstance(event, Body): await self._send_h11_event(h11.Data(data=event.data)) elif isinstance(event, EndBody): await self._send_h11_event(h11.EndOfMessage()) elif isinstance(event, Data): await self.send(RawData(data=event.data)) elif isinstance(event, EndData): pass elif isinstance(event, StreamClosed): await self._maybe_recycle()
async def asgi_send(self, message: dict) -> None: """Called by the ASGI instance to send a message.""" if message['type'] == 'http.response.start' and self.state == ASGIState.REQUEST: self.response = message elif ( message['type'] == 'http.response.body' and self.state in {ASGIState.REQUEST, ASGIState.RESPONSE} ): if self.state == ASGIState.REQUEST: headers = chain( ( (bytes(key).strip(), bytes(value).strip()) for key, value in self.response['headers'] ), self.response_headers(), ) self.send(h11.Response(status_code=int(self.response['status']), headers=headers)) self.state = ASGIState.RESPONSE if ( not suppress_body(self.scope['method'], int(self.response['status'])) and message.get('body', b'') != b'' ): self.send(h11.Data(data=bytes(message['body']))) await self.drain() if not message.get('more_body', False): if self.state != ASGIState.CLOSED: self.send(h11.EndOfMessage()) self.app_queue.put_nowait({'type': 'http.disconnect'}) self.state = ASGIState.CLOSED else: raise Exception( f"Unexpected message type, {message['type']} given the state {self.state}", )
def test_h11_as_client(): with socket_server(SingleMindedRequestHandler) as httpd: with closing(socket.create_connection(httpd.server_address)) as s: c = h11.Connection(h11.CLIENT) s.sendall( c.send( h11.Request(method="GET", target="/foo", headers=[("Host", "localhost")]))) s.sendall(c.send(h11.EndOfMessage())) data = bytearray() while True: event = c.next_event() print(event) if event is h11.NEED_DATA: # Use a small read buffer to make things more challenging # and exercise more paths :-) c.receive_data(s.recv(10)) continue if type(event) is h11.Response: assert event.status_code == 200 if type(event) is h11.Data: data += event.data if type(event) is h11.EndOfMessage: break assert bytes(data) == test_file_data
async def make_h11_request(socket): conn = h11.Connection(our_role=h11.CLIENT) req = h11.Request( method="GET", target="/", headers=[ ("Host", "localhost"), ("Connection", "close"), ], ) data = conn.send(req) await socket.sendall(data) req = h11.EndOfMessage() data = conn.send(req) await socket.sendall(data) async def next_event(): while True: event = conn.next_event() if event is h11.NEED_DATA: data = await socket.recv(2 * 16) conn.receive_data(data) continue return event while True: print("getting event") event = await next_event() print(event) if type(event) is h11.EndOfMessage: break
async def test_http1_request(nursery: trio._core._run.Nursery) -> None: client_stream, server_stream = trio.testing.memory_stream_pair() server_stream.socket = MockSocket() server = TCPServer(sanity_framework, Config(), WorkerContext(), server_stream) nursery.start_soon(server.run) client = h11.Connection(h11.CLIENT) await client_stream.send_all( client.send( h11.Request( method="POST", target="/", headers=[ (b"host", b"hypercorn"), (b"connection", b"close"), (b"content-length", b"%d" % len(SANITY_BODY)), ], ))) await client_stream.send_all(client.send(h11.Data(data=SANITY_BODY))) await client_stream.send_all(client.send(h11.EndOfMessage())) events = [] while True: event = client.next_event() if event == h11.NEED_DATA: # bytes cast is key otherwise b"" is lost data = bytes(await client_stream.receive_some(1024)) client.receive_data(data) elif isinstance(event, h11.ConnectionClosed): break else: events.append(event) assert events == [ h11.Response( status_code=200, headers=[ (b"content-length", b"15"), (b"date", b"Thu, 01 Jan 1970 01:23:20 GMT"), (b"server", b"hypercorn-h11"), (b"connection", b"close"), ], http_version=b"1.1", reason=b"", ), h11.Data(data=b"Hello & Goodbye"), h11.EndOfMessage(headers=[]), # type: ignore ]
def test_basics_1_not_found(example): resp, data, _ = example.send( h11.Request(method='GET', target='/', headers=[('Host', 'example')]), h11.EndOfMessage()) assert resp.status_code == 404 assert resp.reason == b'Not Found' assert (b'content-type', b'text/plain; charset=utf-8') in resp.headers assert b'Error! ' in data.data
def test_basics_1_head(example): resp, _ = example.send( h11.Request(method='HEAD', target='/hello', headers=[('Host', 'example')]), h11.EndOfMessage()) assert resp.status_code == 200 assert resp.reason == b'OK' assert (b'content-type', b'text/plain') in resp.headers
async def test_close_on_framework_error( event_loop: asyncio.AbstractEventLoop) -> None: connection = MockConnection(event_loop, framework=ErrorFramework) await connection.send( h11.Request(method='GET', target='/', headers=BASIC_HEADERS)) await connection.send(h11.EndOfMessage()) await connection.transport.closed.wait( ) # This is the key part, must close on error