From 65dff56d9b9f568d57882913206b4b3ecb3be72c Mon Sep 17 00:00:00 2001 From: Vibhu Agarwal Date: Tue, 27 Apr 2021 20:52:51 +0530 Subject: [PATCH 01/39] Updated gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index c852266ec..f8786f5bb 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ uvicorn.egg-info/ venv/ htmlcov/ site/ +.idea/ From 700046cb913defc9f2e175d29c1075e799b5f600 Mon Sep 17 00:00:00 2001 From: Vibhu Agarwal Date: Tue, 27 Apr 2021 22:07:59 +0530 Subject: [PATCH 02/39] Added (empty) h2_impl.py --- uvicorn/protocols/http/h2_impl.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 uvicorn/protocols/http/h2_impl.py diff --git a/uvicorn/protocols/http/h2_impl.py b/uvicorn/protocols/http/h2_impl.py new file mode 100644 index 000000000..e69de29bb From 773415e27b6735748efc0d672933ad6612b13c2d Mon Sep 17 00:00:00 2001 From: Vibhu Agarwal Date: Thu, 29 Apr 2021 20:58:39 +0530 Subject: [PATCH 03/39] Added some code ... till RequestReceived event --- uvicorn/protocols/http/h2_impl.py | 324 ++++++++++++++++++++++++++++++ 1 file changed, 324 insertions(+) diff --git a/uvicorn/protocols/http/h2_impl.py b/uvicorn/protocols/http/h2_impl.py index e69de29bb..291631122 100644 --- a/uvicorn/protocols/http/h2_impl.py +++ b/uvicorn/protocols/http/h2_impl.py @@ -0,0 +1,324 @@ +import asyncio +import collections +import http +import logging +from urllib.parse import unquote + +import h2.config +import h2.connection +import h2.errors +import h2.events +import h2.exceptions + +from uvicorn.protocols.utils import ( + get_local_addr, + get_remote_addr, + is_ssl, +) + + +def _get_status_phrase(status_code): + try: + return http.HTTPStatus(status_code).phrase.encode() + except ValueError: + return b"" + + +STATUS_PHRASES = { + status_code: _get_status_phrase(status_code) for status_code in range(100, 600) +} + +CLOSE_HEADER = (b"connection", b"close") + +HIGH_WATER_LIMIT = 65536 + +TRACE_LOG_LEVEL = 5 + + +_StreamRequest = collections.namedtuple("_StreamRequest", ("headers", "scope", "cycle")) + + +class FlowControl: + def __init__(self, transport): + self._transport = transport + self.read_paused = False + self.write_paused = False + self._is_writable_event = asyncio.Event() + self._is_writable_event.set() + + async def drain(self): + await self._is_writable_event.wait() + + def pause_reading(self): + if not self.read_paused: + self.read_paused = True + self._transport.pause_reading() + + def resume_reading(self): + if self.read_paused: + self.read_paused = False + self._transport.resume_reading() + + def pause_writing(self): + if not self.write_paused: + self.write_paused = True + self._is_writable_event.clear() + + def resume_writing(self): + if self.write_paused: + self.write_paused = False + self._is_writable_event.set() + + +async def service_unavailable(scope, receive, send): + await send( + { + "type": "http.response.start", + "status": 503, + "headers": [ + (b"content-type", b"text/plain; charset=utf-8"), + (b"connection", b"close"), + ], + } + ) + await send({"type": "http.response.body", "body": b"Service Unavailable"}) + + +class H2Protocol(asyncio.Protocol): + def __init__(self, config, server_state, _loop=None): + if not config.loaded: + config.load() + + self.config = config + self.app = config.loaded_app + self.loop = _loop or asyncio.get_event_loop() + self.logger = logging.getLogger("uvicorn.error") + self.access_logger = logging.getLogger("uvicorn.access") + self.access_log = self.access_logger.hasHandlers() + self.conn = h2.connection.H2Connection( + config=h2.config.H2Configuration(client_side=False, header_encoding=None) + ) + self.ws_protocol_class = config.ws_protocol_class + self.root_path = config.root_path + self.limit_concurrency = config.limit_concurrency + + # Timeouts + self.timeout_keep_alive_task = None + self.timeout_keep_alive = config.timeout_keep_alive + + # Shared server state + self.server_state = server_state + self.connections = server_state.connections + self.tasks = server_state.tasks + self.default_headers = server_state.default_headers + + # Per-connection state + self.transport = None + self.flow = None + self.server = None + self.client = None + self.scheme = None + + # Per-request state + self.scope = None + self.headers = None + self.streams = {} + + # Protocol interface + def connection_made(self, transport): + self.connections.add(self) + + self.transport = transport + self.flow = FlowControl(transport) + self.server = get_local_addr(transport) + self.client = get_remote_addr(transport) + self.scheme = "https" if is_ssl(transport) else "http" + + if self.logger.level <= TRACE_LOG_LEVEL: + prefix = "%s:%d - " % tuple(self.client) if self.client else "" + self.logger.log(TRACE_LOG_LEVEL, "%sConnection made", prefix) + + self.conn.initiate_connection() + self.transport.write(self.conn.data_to_send()) + + def _unset_keepalive_if_required(self): + if self.timeout_keep_alive_task is not None: + self.timeout_keep_alive_task.cancel() + self.timeout_keep_alive_task = None + + def data_received(self, data): + self._unset_keepalive_if_required() + try: + events = self.conn.receive_data(data) + except h2.exceptions.ProtocolError: + self.transport.write(self.conn.data_to_send()) + self.transport.close() + else: + self.transport.write(self.conn.data_to_send()) + self.handle_events(events) + + def handle_events(self, events): + for event in events: + event_type = type(event) + if event_type is h2.events.RequestReceived: + self.on_request_received(event) + elif event_type is h2.events.DataReceived: + pass + elif event_type is h2.events.StreamEnded: + pass + elif event_type is h2.events.StreamReset: + pass + elif event_type is h2.events.WindowUpdated: + pass + elif event_type is h2.events.PriorityUpdated: + pass + elif event_type is h2.events.RemoteSettingsChanged: + pass + elif event_type is h2.events.ConnectionTerminated: + pass + + def on_request_received(self, event): + raw_path, _, query_string = event.raw_path.partition(b"?") + self.scope = { + "type": "http", + "asgi": { + "version": self.config.asgi_version, + "spec_version": "2.1", + }, + "http_version": event.http_version.decode("ascii"), + "server": self.server, + "client": self.client, + "root_path": self.root_path, + "raw_path": raw_path, + "path": unquote(raw_path), + "query_string": query_string, + 'extensions': {"http.response.push": {}}, + 'headers': [], + } + scope_mapping = { + b":scheme": "scheme", + b":authority": "authority", + b":method": "method", + b":path": "path", + } + for key, value in event.headers: + if key in scope_mapping: + self.scope[scope_mapping[key]] = value.decode("ascii") + else: + self.scope["headers"].append((key.lower(), value)) + + # Handle 503 responses when 'limit_concurrency' is exceeded. + if self.limit_concurrency is not None and ( + len(self.connections) >= self.limit_concurrency + or len(self.tasks) >= self.limit_concurrency + ): + app = service_unavailable + message = "Exceeded concurrency limit." + self.logger.warning(message) + else: + app = self.app + + stream_id = event.stream_id + + cycle = RequestResponseCycle( + stream_id=stream_id, + scope=self.scope, + conn=self.conn, + transport=self.transport, + flow=self.flow, + logger=self.logger, + access_logger=self.access_logger, + access_log=self.access_log, + default_headers=self.default_headers, + message_event=asyncio.Event(), + on_response=self.on_response_complete, + ) + self.streams[stream_id] = _StreamRequest( + headers=self.scope["headers"], scope=self.scope, cycle=cycle + ) + self.logger.debug( + "New request received, current stream(%s), all streams: %s", + stream_id, + list(self.streams.keys()), + ) + task = self.loop.create_task(self.streams[stream_id].cycle.run_asgi(app)) + task.add_done_callback(self.tasks.discard) + task.add_done_callback( + lambda t: self.logger.debug( + "stream(%s) done, path(%s)", stream_id, self.scope["path"] + ) + ) + self.tasks.add(task) + + + def on_response_complete(self): + self.server_state.total_requests += 1 + + if self.transport.is_closing(): + return + + # Set a short Keep-Alive timeout. + self._unset_keepalive_if_required() + + self.timeout_keep_alive_task = self.loop.call_later( + self.timeout_keep_alive, self.timeout_keep_alive_handler + ) + + # Unpause data reads if needed. + self.flow.resume_reading() + + # Unblock any pipelined events. + # if self.conn.our_state is h11.DONE and self.conn.their_state is h11.DONE: + # self.conn.start_next_cycle() + # self.handle_events() + + def timeout_keep_alive_handler(self): + """ + Called on a keep-alive connection if no new data is received after a short delay. + """ + if not self.transport.is_closing(): + for stream_id, stream in self.streams.items(): + self.conn.close_connection(last_stream_id=stream_id) + self.transport.write(self.conn.data_to_send()) + self.transport.close() + + +class RequestResponseCycle: + def __init__( + self, + stream_id, + scope, + conn, + transport, + flow, + logger, + access_logger, + access_log, + default_headers, + message_event, + on_response, + ): + self.stream_id = stream_id + self.scope = scope + self.conn = conn + self.transport = transport + self.flow = flow + self.logger = logger + self.access_logger = access_logger + self.access_log = access_log + self.default_headers = default_headers + self.message_event = message_event + self.on_response = on_response + + # Connection state + self.disconnected = False + self.keep_alive = True + + # Request state + self.body = b"" + self.more_body = True + + # Response state + self.response_started = False + self.response_complete = False + From 159dd3b95bba86e1dc6a726f8446126506cb8103 Mon Sep 17 00:00:00 2001 From: Vibhu Agarwal Date: Fri, 30 Apr 2021 00:09:37 +0530 Subject: [PATCH 04/39] Set basic triggers for event-handlers --- uvicorn/protocols/http/h2_impl.py | 75 ++++++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 5 deletions(-) diff --git a/uvicorn/protocols/http/h2_impl.py b/uvicorn/protocols/http/h2_impl.py index 291631122..ea4af8bf7 100644 --- a/uvicorn/protocols/http/h2_impl.py +++ b/uvicorn/protocols/http/h2_impl.py @@ -163,11 +163,11 @@ def handle_events(self, events): if event_type is h2.events.RequestReceived: self.on_request_received(event) elif event_type is h2.events.DataReceived: - pass + self.on_data_received(event) elif event_type is h2.events.StreamEnded: - pass + self.on_stream_ended(event) elif event_type is h2.events.StreamReset: - pass + self.on_stream_reset(event) elif event_type is h2.events.WindowUpdated: pass elif event_type is h2.events.PriorityUpdated: @@ -175,9 +175,10 @@ def handle_events(self, events): elif event_type is h2.events.RemoteSettingsChanged: pass elif event_type is h2.events.ConnectionTerminated: - pass + self.on_connection_terminated(event) + self.transport.write(self.conn.data_to_send()) - def on_request_received(self, event): + def on_request_received(self, event: h2.events.RequestReceived): raw_path, _, query_string = event.raw_path.partition(b"?") self.scope = { "type": "http", @@ -250,6 +251,70 @@ def on_request_received(self, event): ) self.tasks.add(task) + def on_data_received(self, event: h2.events.DataReceived): + stream_id = event.stream_id + self.logger.debug( + "On data received, current %s, streams: %s", stream_id, self.streams.keys() + ) + try: + self.streams[stream_id].cycle.body += event.data + except KeyError: + self.conn.reset_stream( + stream_id, error_code=h2.errors.ErrorCodes.PROTOCOL_ERROR + ) + else: + # In Hypercorn: + # self.conn.acknowledge_received_data( + # event.flow_controlled_length, event.stream_id + # ) + # To be done here, or in RequestResponseCycle's `receive()`? 😕 + body_size = len(self.streams[stream_id].cycle.body) + if body_size > HIGH_WATER_LIMIT: + self.flow.pause_reading() + self.streams[stream_id].cycle.message_event.set() + + def on_stream_ended(self, event: h2.events.StreamEnded): + stream_id = event.stream_id + self.logger.debug( + "On stream ended, current %s, streams: %s", stream_id, self.streams.keys() + ) + try: + stream = self.streams[stream_id] + except KeyError: + self.conn.reset_stream( + stream_id, error_code=h2.errors.ErrorCodes.STREAM_CLOSED + ) + else: + self.flow.resume_reading() + stream.cycle.more_body = False + self.streams[stream_id].cycle.message_event.set() + + def on_stream_reset(self, event: h2.events.StreamReset): + self.logger.debug( + "stream(%s) reset by %s with error_code %s", + event.stream_id, + "server" if event.remote_reset else "remote peer", + event.error_code, + ) + self.streams.pop(event.stream_id, None) + # In Hypercorn: + # app_put({"type": "http.disconnect"}) + + def on_connection_terminated(self, event: h2.events.ConnectionTerminated): + stream_id = event.last_stream_id + self.logger.debug( + "H2Connection terminated, additional_data(%s), error_code(%s), last_stream(%s), streams: %s", + event.additional_data, + event.error_code, + stream_id, + list(self.streams.keys()), + ) + stream = self.streams.pop(stream_id) + if stream: + stream.cycle.disconnected = True + self.conn.close_connection(last_stream_id=stream_id) + self.transport.write(self.conn.data_to_send()) + self.transport.close() def on_response_complete(self): self.server_state.total_requests += 1 From fef0beabda42c089be7f91feff1e486f8e7be2ed Mon Sep 17 00:00:00 2001 From: Vibhu Agarwal Date: Fri, 30 Apr 2021 00:20:54 +0530 Subject: [PATCH 05/39] Fix on_request_received --- uvicorn/protocols/http/h2_impl.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/uvicorn/protocols/http/h2_impl.py b/uvicorn/protocols/http/h2_impl.py index ea4af8bf7..a002d5d9a 100644 --- a/uvicorn/protocols/http/h2_impl.py +++ b/uvicorn/protocols/http/h2_impl.py @@ -179,20 +179,16 @@ def handle_events(self, events): self.transport.write(self.conn.data_to_send()) def on_request_received(self, event: h2.events.RequestReceived): - raw_path, _, query_string = event.raw_path.partition(b"?") self.scope = { "type": "http", "asgi": { "version": self.config.asgi_version, "spec_version": "2.1", }, - "http_version": event.http_version.decode("ascii"), + "http_version": "2", "server": self.server, "client": self.client, "root_path": self.root_path, - "raw_path": raw_path, - "path": unquote(raw_path), - "query_string": query_string, 'extensions': {"http.response.push": {}}, 'headers': [], } @@ -200,13 +196,18 @@ def on_request_received(self, event: h2.events.RequestReceived): b":scheme": "scheme", b":authority": "authority", b":method": "method", - b":path": "path", + b":path": "raw_path", } for key, value in event.headers: if key in scope_mapping: self.scope[scope_mapping[key]] = value.decode("ascii") else: self.scope["headers"].append((key.lower(), value)) + path, _, query_string = self.scope["raw_path"].partition("?") + self.scope["path"], self.scope["query_string"] = ( + unquote(path), + query_string.encode("ascii"), + ) # Handle 503 responses when 'limit_concurrency' is exceeded. if self.limit_concurrency is not None and ( From b92c35910c85e15de28029d3e1a72a1388c950ab Mon Sep 17 00:00:00 2001 From: Vibhu Agarwal Date: Fri, 30 Apr 2021 03:56:59 +0530 Subject: [PATCH 06/39] Added remaining methods of H2Protocol --- uvicorn/protocols/http/h2_impl.py | 59 +++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/uvicorn/protocols/http/h2_impl.py b/uvicorn/protocols/http/h2_impl.py index a002d5d9a..34c64e366 100644 --- a/uvicorn/protocols/http/h2_impl.py +++ b/uvicorn/protocols/http/h2_impl.py @@ -141,11 +141,42 @@ def connection_made(self, transport): self.conn.initiate_connection() self.transport.write(self.conn.data_to_send()) + def connection_lost(self, exc): + self.connections.discard(self) + + self.logger.debug("%s - Disconnected", self.client[0]) + + for stream_id, stream in self.streams.items(): + if stream.cycle: + if not stream.cycle.response_complete: + stream.cycle.disconnected = True + try: + self.conn.close_connection(last_stream_id=stream_id) + self.transport.write(self.conn.data_to_send()) + except h2.exceptions.ProtocolError as err: + self.logger.debug( + "connection lost, failed to close connection.", exc_info=err + ) + stream.cycle.message_event.set() + + self.logger.debug( + "Disconnected, current streams: %s", list(self.streams.keys()), exc_info=exc + ) + self.streams = {} + if self.flow is not None: + self.flow.resume_writing() + def _unset_keepalive_if_required(self): if self.timeout_keep_alive_task is not None: self.timeout_keep_alive_task.cancel() self.timeout_keep_alive_task = None + def eof_received(self): + self.logger.debug( + "eof received, current streams: %s", list(self.streams.keys()) + ) + self.streams = {} + def data_received(self, data): self._unset_keepalive_if_required() try: @@ -338,6 +369,34 @@ def on_response_complete(self): # self.conn.start_next_cycle() # self.handle_events() + def handle_upgrade(self, event): + pass + + def pause_writing(self): + """ + Called by the transport when the write buffer exceeds the high water mark. + """ + self.flow.pause_writing() + + def resume_writing(self): + """ + Called by the transport when the write buffer drops below the low water mark. + """ + self.flow.resume_writing() + + def shutdown(self): + self.logger.debug( + "Shutdown. streams: %s, tasks: %s", self.streams.keys(), self.tasks + ) + for stream_id, stream in self.streams.items(): + if stream.cycle is None or stream.cycle.response_complete: + self.conn.close_connection(last_stream_id=stream_id) + self.transport.write(self.conn.data_to_send()) + else: + stream.cycle.keep_alive = False + self.streams = {} + self.transport.close() + def timeout_keep_alive_handler(self): """ Called on a keep-alive connection if no new data is received after a short delay. From 1c5d053f1fc6f809a4b7846f10a3e4d43f1d0427 Mon Sep 17 00:00:00 2001 From: Vibhu Agarwal Date: Fri, 30 Apr 2021 05:03:45 +0530 Subject: [PATCH 07/39] Added code for RequestResponseCycle --- uvicorn/protocols/http/h2_impl.py | 130 ++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) diff --git a/uvicorn/protocols/http/h2_impl.py b/uvicorn/protocols/http/h2_impl.py index 34c64e366..95b31a6c8 100644 --- a/uvicorn/protocols/http/h2_impl.py +++ b/uvicorn/protocols/http/h2_impl.py @@ -11,7 +11,9 @@ import h2.exceptions from uvicorn.protocols.utils import ( + get_client_addr, get_local_addr, + get_path_with_query_string, get_remote_addr, is_ssl, ) @@ -447,3 +449,131 @@ def __init__( self.response_started = False self.response_complete = False + # ASGI exception wrapper + async def run_asgi(self, app): + try: + result = await app(self.scope, self.receive, self.send) + except BaseException as exc: + msg = "Exception in ASGI application\n" + self.logger.error(msg, exc_info=exc) + if not self.response_started: + await self.send_500_response() + else: + self.transport.close() + else: + if result is not None: + msg = "ASGI callable should return None, but returned '%s'." + self.logger.error(msg, result) + self.transport.close() + elif not self.response_started and not self.disconnected: + msg = "ASGI callable returned without starting response." + self.logger.error(msg) + await self.send_500_response() + elif not self.response_complete and not self.disconnected: + msg = "ASGI callable returned without completing response." + self.logger.error(msg) + self.transport.close() + finally: + self.on_response = None + + async def send_500_response(self): + await self.send( + { + "type": "http.response.start", + "status": 500, + "headers": [ + (b"content-type", b"text/plain; charset=utf-8"), + (b"connection", b"close"), + ], + } + ) + await self.send( + {"type": "http.response.body", "body": b"Internal Server Error"} + ) + + # ASGI interface + async def send(self, message): + message_type = message["type"] + + if self.disconnected: + return + + if self.flow.write_paused: + await self.flow.drain() + + if not self.response_started: + # Sending response status line and headers + if message_type != "http.response.start": + msg = "Expected ASGI message 'http.response.start', but got '%s'." + raise RuntimeError(msg % message_type) + + self.response_started = True + + status_code = message["status"] + + headers = self.default_headers + message.get("headers", []) + + if CLOSE_HEADER in self.scope["headers"] and CLOSE_HEADER not in headers: + headers = headers + [CLOSE_HEADER] + + if self.access_log: + self.access_logger.info( + '%s - "%s %s HTTP/%s" %d', + get_client_addr(self.scope), + self.scope["method"], + get_path_with_query_string(self.scope), + self.scope["http_version"], + status_code, + ) + + # Write response status line and headers + headers = ((":status", str(status_code)), *headers) + self.logger.debug("response start, message %s", message) + self.conn.send_headers(self.stream_id, headers, end_stream=False) + self.transport.write(self.conn.data_to_send()) + elif not self.response_complete: + # Sending response body + if message_type == "http.response.body": + more_body = message.get("more_body", False) + + # Write response body + if self.scope["method"] == "HEAD": + body = b"" + else: + body = message.get("body", b"") + self.conn.send_data(self.stream_id, body, end_stream=(not more_body)) + self.transport.write(self.conn.data_to_send()) + + # Handle response completion + if not more_body: + self.response_complete = True + elif message_type == "http.response.push": + pass + else: + msg = "Expected ASGI message 'http.response.body' or 'http.response.push', but got '%s'." + raise RuntimeError(msg % message_type) + else: + # Response already sent + msg = "Unexpected ASGI message '%s' sent, after response already completed." + raise RuntimeError(msg % message_type) + + if self.response_complete: + self.on_response() + + async def receive(self): + if not self.disconnected and not self.response_complete: + self.flow.resume_reading() + await self.message_event.wait() + self.message_event.clear() + + if self.disconnected or self.response_complete: + message = {"type": "http.disconnect"} + else: + message = { + "type": "http.request", + "body": self.body, + "more_body": self.more_body, + } + self.body = b"" + + return message From ec4b78635fc9b1922d714883d6695b8f24ddd539 Mon Sep 17 00:00:00 2001 From: Vibhu Agarwal Date: Fri, 30 Apr 2021 19:01:07 +0530 Subject: [PATCH 08/39] Added some boilerplate for handle_upgrade in h11_impl.py --- uvicorn/protocols/http/h11_impl.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/uvicorn/protocols/http/h11_impl.py b/uvicorn/protocols/http/h11_impl.py index dc887cd6e..0679b1843 100644 --- a/uvicorn/protocols/http/h11_impl.py +++ b/uvicorn/protocols/http/h11_impl.py @@ -261,9 +261,22 @@ def handle_events(self): def handle_upgrade(self, event): upgrade_value = None + has_body = False for name, value in self.headers: if name == b"upgrade": upgrade_value = value.lower() + elif name in {"content-length", "transfer-encoding"}: + has_body = True + + # https://http2.github.io/faq/#can-i-implement-http2-without-implementing-http11 + if upgrade_value.lower() == "h2c" and not has_body: + # h2c stuff + # return + pass + elif event.method == b"PRI" and event.target == b"*" and event.http_version == b"2.0": + # https://tools.ietf.org/html/rfc7540#section-3.5 + # return + pass if upgrade_value != b"websocket" or self.ws_protocol_class is None: msg = "Unsupported upgrade request." From d2f329e05a57ce2c046c61485450956910b43e00 Mon Sep 17 00:00:00 2001 From: Vibhu Agarwal Date: Fri, 30 Apr 2021 19:45:37 +0530 Subject: [PATCH 09/39] Handling h2c in h11_impl.py (TODO: Similarly in httptools) --- uvicorn/config.py | 4 +++ uvicorn/protocols/http/h11_impl.py | 55 ++++++++++++++++++------------ 2 files changed, 37 insertions(+), 22 deletions(-) diff --git a/uvicorn/config.py b/uvicorn/config.py index 6cd4ced7a..7ce3f749a 100644 --- a/uvicorn/config.py +++ b/uvicorn/config.py @@ -40,6 +40,7 @@ "auto": "uvicorn.protocols.http.auto:AutoHTTPProtocol", "h11": "uvicorn.protocols.http.h11_impl:H11Protocol", "httptools": "uvicorn.protocols.http.httptools_impl:HttpToolsProtocol", + "h2": "uvicorn.protocols.http.h2_impl:H2Protocol", } WS_PROTOCOLS = { "auto": "uvicorn.protocols.websockets.auto:AutoWebSocketsProtocol", @@ -193,6 +194,7 @@ def __init__( self.headers = headers if headers else [] # type: List[str] self.encoded_headers = None # type: List[Tuple[bytes, bytes]] self.factory = factory + self.h2_protocol_class = None self.loaded = False self.configure_logging() @@ -304,6 +306,8 @@ def load(self): self.lifespan_class = import_from_string(LIFESPAN[self.lifespan]) + self.h2_protocol_class = import_from_string(HTTP_PROTOCOLS['h2']) + try: self.loaded_app = import_from_string(self.app) except ImportFromStringError as exc: diff --git a/uvicorn/protocols/http/h11_impl.py b/uvicorn/protocols/http/h11_impl.py index 0679b1843..9c5cfd19a 100644 --- a/uvicorn/protocols/http/h11_impl.py +++ b/uvicorn/protocols/http/h11_impl.py @@ -270,15 +270,39 @@ def handle_upgrade(self, event): # https://http2.github.io/faq/#can-i-implement-http2-without-implementing-http11 if upgrade_value.lower() == "h2c" and not has_body: - # h2c stuff - # return - pass - elif event.method == b"PRI" and event.target == b"*" and event.http_version == b"2.0": - # https://tools.ietf.org/html/rfc7540#section-3.5 - # return - pass - - if upgrade_value != b"websocket" or self.ws_protocol_class is None: + self.connections.discard(self) + + headers = ((b"upgrade", b"h2c"), *self.headers) + self.transport.write( + self.conn.send( + h11.InformationalResponse(status_code=101, headers=headers) + ) + ) + protocol = self.config.h2_protocol_class( + config=self.config, + server_state=self.server_state + ) + protocol.connection_made(self.transport, upgrade_request=event) + self.transport.set_protocol(protocol) + + # elif event.method == b"PRI" and event.target == b"*" and event.http_version == b"2.0": + # # https://tools.ietf.org/html/rfc7540#section-3.5 + # pass + + elif upgrade_value == b"websocket" and self.ws_protocol_class is not None: + self.connections.discard(self) + output = [event.method, b" ", event.target, b" HTTP/1.1\r\n"] + for name, value in self.headers: + output += [name, b": ", value, b"\r\n"] + output.append(b"\r\n") + protocol = self.ws_protocol_class( + config=self.config, server_state=self.server_state + ) + protocol.connection_made(self.transport) + protocol.data_received(b"".join(output)) + self.transport.set_protocol(protocol) + + else: msg = "Unsupported upgrade request." self.logger.warning(msg) @@ -303,19 +327,6 @@ def handle_upgrade(self, event): output = self.conn.send(event) self.transport.write(output) self.transport.close() - return - - self.connections.discard(self) - output = [event.method, b" ", event.target, b" HTTP/1.1\r\n"] - for name, value in self.headers: - output += [name, b": ", value, b"\r\n"] - output.append(b"\r\n") - protocol = self.ws_protocol_class( - config=self.config, server_state=self.server_state - ) - protocol.connection_made(self.transport) - protocol.data_received(b"".join(output)) - self.transport.set_protocol(protocol) def on_response_complete(self): self.server_state.total_requests += 1 From 086cc29ff44a49c5e2c7bff20eb0de59fda44fce Mon Sep 17 00:00:00 2001 From: Vibhu Agarwal Date: Fri, 30 Apr 2021 19:58:11 +0530 Subject: [PATCH 10/39] Handling connection preface in h11_impl.py --- uvicorn/protocols/http/h11_impl.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/uvicorn/protocols/http/h11_impl.py b/uvicorn/protocols/http/h11_impl.py index 9c5cfd19a..07d4cd259 100644 --- a/uvicorn/protocols/http/h11_impl.py +++ b/uvicorn/protocols/http/h11_impl.py @@ -268,7 +268,6 @@ def handle_upgrade(self, event): elif name in {"content-length", "transfer-encoding"}: has_body = True - # https://http2.github.io/faq/#can-i-implement-http2-without-implementing-http11 if upgrade_value.lower() == "h2c" and not has_body: self.connections.discard(self) @@ -285,9 +284,17 @@ def handle_upgrade(self, event): protocol.connection_made(self.transport, upgrade_request=event) self.transport.set_protocol(protocol) - # elif event.method == b"PRI" and event.target == b"*" and event.http_version == b"2.0": - # # https://tools.ietf.org/html/rfc7540#section-3.5 - # pass + elif event.method == b"PRI" and event.target == b"*" and event.http_version == b"2.0": + # https://tools.ietf.org/html/rfc7540#section-3.5 + self.connections.discard(self) + + protocol = self.config.h2_protocol_class( + config=self.config, + server_state=self.server_state + ) + protocol.connection_made(self.transport) + self.transport.set_protocol(protocol) + protocol.data_received(b"PRI * HTTP/2.0\r\n\r\n" + self.conn.trailing_data[0]) elif upgrade_value == b"websocket" and self.ws_protocol_class is not None: self.connections.discard(self) From eb125920a0685d79d3fdd762fb149e32e4afe04d Mon Sep 17 00:00:00 2001 From: Vibhu Agarwal Date: Fri, 30 Apr 2021 20:11:47 +0530 Subject: [PATCH 11/39] Modify method prototype: H2Protocol - connection_made() --- uvicorn/protocols/http/h2_impl.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/uvicorn/protocols/http/h2_impl.py b/uvicorn/protocols/http/h2_impl.py index 95b31a6c8..c393d9e3f 100644 --- a/uvicorn/protocols/http/h2_impl.py +++ b/uvicorn/protocols/http/h2_impl.py @@ -127,7 +127,7 @@ def __init__(self, config, server_state, _loop=None): self.streams = {} # Protocol interface - def connection_made(self, transport): + def connection_made(self, transport, upgrade_request=None): self.connections.add(self) self.transport = transport @@ -136,13 +136,17 @@ def connection_made(self, transport): self.client = get_remote_addr(transport) self.scheme = "https" if is_ssl(transport) else "http" + if upgrade_request is None: + self.conn.initiate_connection() + else: + # Different implementations for httptools and h11 + return + + self.transport.write(self.conn.data_to_send()) if self.logger.level <= TRACE_LOG_LEVEL: prefix = "%s:%d - " % tuple(self.client) if self.client else "" self.logger.log(TRACE_LOG_LEVEL, "%sConnection made", prefix) - self.conn.initiate_connection() - self.transport.write(self.conn.data_to_send()) - def connection_lost(self, exc): self.connections.discard(self) From d238f26d2d6657501130be9accac29a54df7c12f Mon Sep 17 00:00:00 2001 From: Vibhu Agarwal Date: Fri, 30 Apr 2021 23:57:15 +0530 Subject: [PATCH 12/39] Fixed connection preface check in h11_impl.py --- uvicorn/protocols/http/h11_impl.py | 31 ++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/uvicorn/protocols/http/h11_impl.py b/uvicorn/protocols/http/h11_impl.py index 07d4cd259..4422ffc54 100644 --- a/uvicorn/protocols/http/h11_impl.py +++ b/uvicorn/protocols/http/h11_impl.py @@ -189,6 +189,10 @@ def handle_events(self): break elif event_type is h11.Request: + + if self.check_connection_preface(event): + return + self.headers = [(key.lower(), value) for key, value in event.headers] raw_path, _, query_string = event.target.partition(b"?") self.scope = { @@ -259,6 +263,21 @@ def handle_events(self): self.cycle.more_body = False self.cycle.message_event.set() + def check_connection_preface(self, event): + if event.method == b"PRI" and event.target == b"*" and event.http_version == b"2.0": + # https://tools.ietf.org/html/rfc7540#section-3.5 + self.connections.discard(self) + + protocol = self.config.h2_protocol_class( + config=self.config, + server_state=self.server_state + ) + protocol.connection_made(self.transport) + self.transport.set_protocol(protocol) + protocol.data_received(b"PRI * HTTP/2.0\r\n\r\n" + self.conn.trailing_data[0]) + return True + return False + def handle_upgrade(self, event): upgrade_value = None has_body = False @@ -284,18 +303,6 @@ def handle_upgrade(self, event): protocol.connection_made(self.transport, upgrade_request=event) self.transport.set_protocol(protocol) - elif event.method == b"PRI" and event.target == b"*" and event.http_version == b"2.0": - # https://tools.ietf.org/html/rfc7540#section-3.5 - self.connections.discard(self) - - protocol = self.config.h2_protocol_class( - config=self.config, - server_state=self.server_state - ) - protocol.connection_made(self.transport) - self.transport.set_protocol(protocol) - protocol.data_received(b"PRI * HTTP/2.0\r\n\r\n" + self.conn.trailing_data[0]) - elif upgrade_value == b"websocket" and self.ws_protocol_class is not None: self.connections.discard(self) output = [event.method, b" ", event.target, b" HTTP/1.1\r\n"] From e69479260fb68581e5b1e92bbb7d51682d396763 Mon Sep 17 00:00:00 2001 From: Vibhu Agarwal Date: Sat, 1 May 2021 02:23:18 +0530 Subject: [PATCH 13/39] Added comments regarding missing implementations --- uvicorn/protocols/http/h2_impl.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/uvicorn/protocols/http/h2_impl.py b/uvicorn/protocols/http/h2_impl.py index c393d9e3f..068686d28 100644 --- a/uvicorn/protocols/http/h2_impl.py +++ b/uvicorn/protocols/http/h2_impl.py @@ -97,9 +97,13 @@ def __init__(self, config, server_state, _loop=None): self.logger = logging.getLogger("uvicorn.error") self.access_logger = logging.getLogger("uvicorn.access") self.access_log = self.access_logger.hasHandlers() + self.conn = h2.connection.H2Connection( config=h2.config.H2Configuration(client_side=False, header_encoding=None) ) + # In Hypercorn: + # self.conn.local_settings = ... + self.ws_protocol_class = config.ws_protocol_class self.root_path = config.root_path self.limit_concurrency = config.limit_concurrency @@ -139,7 +143,7 @@ def connection_made(self, transport, upgrade_request=None): if upgrade_request is None: self.conn.initiate_connection() else: - # Different implementations for httptools and h11 + # Different implementations for httptools and h11 for handling h2c return self.transport.write(self.conn.data_to_send()) @@ -552,6 +556,7 @@ async def send(self, message): if not more_body: self.response_complete = True elif message_type == "http.response.push": + # https://groups.google.com/a/chromium.org/g/blink-dev/c/K3rYLvmQUBY/m/vOWBKZGoAQAJ 😕 pass else: msg = "Expected ASGI message 'http.response.body' or 'http.response.push', but got '%s'." From 00dce01bee1476ad5a39ff7d52cfcc578d5c1255 Mon Sep 17 00:00:00 2001 From: Vibhu Agarwal Date: Sat, 1 May 2021 02:25:28 +0530 Subject: [PATCH 14/39] Reverted .gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index f8786f5bb..c852266ec 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,3 @@ uvicorn.egg-info/ venv/ htmlcov/ site/ -.idea/ From c03b6e855d5687da9e65f5f9d81126daae0c022a Mon Sep 17 00:00:00 2001 From: Vibhu Agarwal Date: Sat, 1 May 2021 02:37:11 +0530 Subject: [PATCH 15/39] Added comments regarding missing implementations --- uvicorn/config.py | 1 + uvicorn/protocols/http/h2_impl.py | 15 +++++++-------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/uvicorn/config.py b/uvicorn/config.py index 7ce3f749a..25bbd85eb 100644 --- a/uvicorn/config.py +++ b/uvicorn/config.py @@ -113,6 +113,7 @@ def create_ssl_context( ctx.load_verify_locations(ca_certs) if ciphers: ctx.set_ciphers(ciphers) + # TODO: http2 related stuff implementation return ctx diff --git a/uvicorn/protocols/http/h2_impl.py b/uvicorn/protocols/http/h2_impl.py index 068686d28..0754b5cca 100644 --- a/uvicorn/protocols/http/h2_impl.py +++ b/uvicorn/protocols/http/h2_impl.py @@ -101,8 +101,7 @@ def __init__(self, config, server_state, _loop=None): self.conn = h2.connection.H2Connection( config=h2.config.H2Configuration(client_side=False, header_encoding=None) ) - # In Hypercorn: - # self.conn.local_settings = ... + # TODO: Set h2-connection settings from `config` self.ws_protocol_class = config.ws_protocol_class self.root_path = config.root_path @@ -143,6 +142,7 @@ def connection_made(self, transport, upgrade_request=None): if upgrade_request is None: self.conn.initiate_connection() else: + # TODO: Implementation for handling h2c upgrade # Different implementations for httptools and h11 for handling h2c return @@ -309,7 +309,8 @@ def on_data_received(self, event: h2.events.DataReceived): # self.conn.acknowledge_received_data( # event.flow_controlled_length, event.stream_id # ) - # To be done here, or in RequestResponseCycle's `receive()`? 😕 + # TODO: To be done here, or in RequestResponseCycle's `receive()`? 😕 + body_size = len(self.streams[stream_id].cycle.body) if body_size > HIGH_WATER_LIMIT: self.flow.pause_reading() @@ -339,6 +340,8 @@ def on_stream_reset(self, event: h2.events.StreamReset): event.error_code, ) self.streams.pop(event.stream_id, None) + + # TODO: To be done or not? # In Hypercorn: # app_put({"type": "http.disconnect"}) @@ -374,11 +377,6 @@ def on_response_complete(self): # Unpause data reads if needed. self.flow.resume_reading() - # Unblock any pipelined events. - # if self.conn.our_state is h11.DONE and self.conn.their_state is h11.DONE: - # self.conn.start_next_cycle() - # self.handle_events() - def handle_upgrade(self, event): pass @@ -556,6 +554,7 @@ async def send(self, message): if not more_body: self.response_complete = True elif message_type == "http.response.push": + # TODO: Implement or Not? # https://groups.google.com/a/chromium.org/g/blink-dev/c/K3rYLvmQUBY/m/vOWBKZGoAQAJ 😕 pass else: From 293aaf640c9f9a2017d771d5baf3b6c757549ba1 Mon Sep 17 00:00:00 2001 From: Vibhu Agarwal Date: Sat, 1 May 2021 03:55:57 +0530 Subject: [PATCH 16/39] Applied black (linting) --- uvicorn/config.py | 2 +- uvicorn/protocols/http/h11_impl.py | 16 +++++++++------ uvicorn/protocols/http/h2_impl.py | 32 +++++++++++++++--------------- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/uvicorn/config.py b/uvicorn/config.py index 25bbd85eb..0c8c64655 100644 --- a/uvicorn/config.py +++ b/uvicorn/config.py @@ -307,7 +307,7 @@ def load(self): self.lifespan_class = import_from_string(LIFESPAN[self.lifespan]) - self.h2_protocol_class = import_from_string(HTTP_PROTOCOLS['h2']) + self.h2_protocol_class = import_from_string(HTTP_PROTOCOLS["h2"]) try: self.loaded_app = import_from_string(self.app) diff --git a/uvicorn/protocols/http/h11_impl.py b/uvicorn/protocols/http/h11_impl.py index 4422ffc54..021e438ea 100644 --- a/uvicorn/protocols/http/h11_impl.py +++ b/uvicorn/protocols/http/h11_impl.py @@ -264,17 +264,22 @@ def handle_events(self): self.cycle.message_event.set() def check_connection_preface(self, event): - if event.method == b"PRI" and event.target == b"*" and event.http_version == b"2.0": + if ( + event.method == b"PRI" + and event.target == b"*" + and event.http_version == b"2.0" + ): # https://tools.ietf.org/html/rfc7540#section-3.5 self.connections.discard(self) protocol = self.config.h2_protocol_class( - config=self.config, - server_state=self.server_state + config=self.config, server_state=self.server_state ) protocol.connection_made(self.transport) self.transport.set_protocol(protocol) - protocol.data_received(b"PRI * HTTP/2.0\r\n\r\n" + self.conn.trailing_data[0]) + protocol.data_received( + b"PRI * HTTP/2.0\r\n\r\n" + self.conn.trailing_data[0] + ) return True return False @@ -297,8 +302,7 @@ def handle_upgrade(self, event): ) ) protocol = self.config.h2_protocol_class( - config=self.config, - server_state=self.server_state + config=self.config, server_state=self.server_state ) protocol.connection_made(self.transport, upgrade_request=event) self.transport.set_protocol(protocol) diff --git a/uvicorn/protocols/http/h2_impl.py b/uvicorn/protocols/http/h2_impl.py index 0754b5cca..feafd6577 100644 --- a/uvicorn/protocols/http/h2_impl.py +++ b/uvicorn/protocols/http/h2_impl.py @@ -230,8 +230,8 @@ def on_request_received(self, event: h2.events.RequestReceived): "server": self.server, "client": self.client, "root_path": self.root_path, - 'extensions': {"http.response.push": {}}, - 'headers': [], + "extensions": {"http.response.push": {}}, + "headers": [], } scope_mapping = { b":scheme": "scheme", @@ -252,8 +252,8 @@ def on_request_received(self, event: h2.events.RequestReceived): # Handle 503 responses when 'limit_concurrency' is exceeded. if self.limit_concurrency is not None and ( - len(self.connections) >= self.limit_concurrency - or len(self.tasks) >= self.limit_concurrency + len(self.connections) >= self.limit_concurrency + or len(self.tasks) >= self.limit_concurrency ): app = service_unavailable message = "Exceeded concurrency limit." @@ -418,18 +418,18 @@ def timeout_keep_alive_handler(self): class RequestResponseCycle: def __init__( - self, - stream_id, - scope, - conn, - transport, - flow, - logger, - access_logger, - access_log, - default_headers, - message_event, - on_response, + self, + stream_id, + scope, + conn, + transport, + flow, + logger, + access_logger, + access_log, + default_headers, + message_event, + on_response, ): self.stream_id = stream_id self.scope = scope From 2a6f2dc8a74d10b1b972dce64d1519f3de0ce4a7 Mon Sep 17 00:00:00 2001 From: Vibhu Agarwal Date: Sat, 1 May 2021 04:05:06 +0530 Subject: [PATCH 17/39] Reformatted h2_impl.py --- uvicorn/protocols/http/h2_impl.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/uvicorn/protocols/http/h2_impl.py b/uvicorn/protocols/http/h2_impl.py index feafd6577..9953b7960 100644 --- a/uvicorn/protocols/http/h2_impl.py +++ b/uvicorn/protocols/http/h2_impl.py @@ -348,7 +348,8 @@ def on_stream_reset(self, event: h2.events.StreamReset): def on_connection_terminated(self, event: h2.events.ConnectionTerminated): stream_id = event.last_stream_id self.logger.debug( - "H2Connection terminated, additional_data(%s), error_code(%s), last_stream(%s), streams: %s", + "H2Connection terminated, additional_data(%s), " + "error_code(%s), last_stream(%s), streams: %s", event.additional_data, event.error_code, stream_id, @@ -407,7 +408,8 @@ def shutdown(self): def timeout_keep_alive_handler(self): """ - Called on a keep-alive connection if no new data is received after a short delay. + Called on a keep-alive connection if no new data is received after a short + delay. """ if not self.transport.is_closing(): for stream_id, stream in self.streams.items(): @@ -555,10 +557,13 @@ async def send(self, message): self.response_complete = True elif message_type == "http.response.push": # TODO: Implement or Not? - # https://groups.google.com/a/chromium.org/g/blink-dev/c/K3rYLvmQUBY/m/vOWBKZGoAQAJ 😕 + # https://groups.google.com/a/chromium.org/g/blink-dev/c/K3rYLvmQUBY/m/vOWBKZGoAQAJ 😕 # noqa: E501 pass else: - msg = "Expected ASGI message 'http.response.body' or 'http.response.push', but got '%s'." + msg = ( + "Expected ASGI message 'http.response.body' " + "or 'http.response.push', but got '%s'." + ) raise RuntimeError(msg % message_type) else: # Response already sent From 8f6a8258dd374a353ba4e9c3b439de939cfb4dd9 Mon Sep 17 00:00:00 2001 From: Vibhu Agarwal Date: Sat, 1 May 2021 04:33:35 +0530 Subject: [PATCH 18/39] python -m cli_tools.usage --- docs/deployment.md | 2 +- docs/index.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/deployment.md b/docs/deployment.md index 3de15e6d1..a43202349 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -48,7 +48,7 @@ Options: available, or 1. Not valid with --reload. --loop [auto|asyncio|uvloop] Event loop implementation. [default: auto] - --http [auto|h11|httptools] HTTP protocol implementation. [default: + --http [auto|h11|httptools|h2] HTTP protocol implementation. [default: auto] --ws [auto|none|websockets|wsproto] diff --git a/docs/index.md b/docs/index.md index ae587f2ee..e4fd90e33 100644 --- a/docs/index.md +++ b/docs/index.md @@ -118,7 +118,7 @@ Options: available, or 1. Not valid with --reload. --loop [auto|asyncio|uvloop] Event loop implementation. [default: auto] - --http [auto|h11|httptools] HTTP protocol implementation. [default: + --http [auto|h11|httptools|h2] HTTP protocol implementation. [default: auto] --ws [auto|none|websockets|wsproto] From 394140a15a4209c91078298629be52c32a103638 Mon Sep 17 00:00:00 2001 From: Vibhu Agarwal Date: Sat, 1 May 2021 04:59:32 +0530 Subject: [PATCH 19/39] Added h2 in requirements --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 73f1771d1..6db9b1638 100755 --- a/setup.py +++ b/setup.py @@ -46,6 +46,7 @@ def get_packages(package): minimal_requirements = [ "click==7.*", "h11>=0.8", + "h2>=4.0.0", "typing-extensions;" + env_marker_below_38, ] From 5d6dd1b355a7c9c11f52c392924d7c48eaf89c1c Mon Sep 17 00:00:00 2001 From: Vibhu Agarwal Date: Sat, 1 May 2021 20:12:21 +0530 Subject: [PATCH 20/39] Added h2-specific SSLContext options in create_ssl_context() --- uvicorn/config.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/uvicorn/config.py b/uvicorn/config.py index 0c8c64655..2269fb372 100644 --- a/uvicorn/config.py +++ b/uvicorn/config.py @@ -103,7 +103,7 @@ def create_ssl_context( - certfile, keyfile, password, ssl_version, cert_reqs, ca_certs, ciphers + certfile, keyfile, password, ssl_version, cert_reqs, ca_certs, ciphers, enable_h2 ): ctx = ssl.SSLContext(ssl_version) get_password = (lambda: password) if password else None @@ -113,7 +113,20 @@ def create_ssl_context( ctx.load_verify_locations(ca_certs) if ciphers: ctx.set_ciphers(ciphers) - # TODO: http2 related stuff implementation + if enable_h2: + ctx.options |= ( + ssl.OP_NO_SSLv2 + | ssl.OP_NO_SSLv3 + | ssl.OP_NO_TLSv1 + | ssl.OP_NO_TLSv1_1 + | ssl.OP_NO_COMPRESSION + | ssl.OP_CIPHER_SERVER_PREFERENCE + ) + ctx.set_alpn_protocols(["h2", "http/1.1"]) + try: + ctx.set_npn_protocols(["h2", "http/1.1"]) + except NotImplementedError: + pass return ctx @@ -281,6 +294,7 @@ def load(self): cert_reqs=self.ssl_cert_reqs, ca_certs=self.ssl_ca_certs, ciphers=self.ssl_ciphers, + enable_h2=self.http in ("h2",), ) else: self.ssl = None From 7e9600904ab128812ef146a8ef5224972548a9a5 Mon Sep 17 00:00:00 2001 From: Vibhu Agarwal Date: Sat, 1 May 2021 20:32:56 +0530 Subject: [PATCH 21/39] Added h2-connection local settings (through config) --- uvicorn/config.py | 3 +++ uvicorn/protocols/http/h2_impl.py | 10 +++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/uvicorn/config.py b/uvicorn/config.py index 2269fb372..17f0b92d0 100644 --- a/uvicorn/config.py +++ b/uvicorn/config.py @@ -209,6 +209,9 @@ def __init__( self.encoded_headers = None # type: List[Tuple[bytes, bytes]] self.factory = factory self.h2_protocol_class = None + self.h2_max_concurrent_streams = 100 + self.h2_max_header_list_size = 2 ** 16 + self.h2_max_inbound_frame_size = 2 ** 14 self.loaded = False self.configure_logging() diff --git a/uvicorn/protocols/http/h2_impl.py b/uvicorn/protocols/http/h2_impl.py index 9953b7960..e78f14b55 100644 --- a/uvicorn/protocols/http/h2_impl.py +++ b/uvicorn/protocols/http/h2_impl.py @@ -9,6 +9,7 @@ import h2.errors import h2.events import h2.exceptions +import h2.settings from uvicorn.protocols.utils import ( get_client_addr, @@ -101,7 +102,14 @@ def __init__(self, config, server_state, _loop=None): self.conn = h2.connection.H2Connection( config=h2.config.H2Configuration(client_side=False, header_encoding=None) ) - # TODO: Set h2-connection settings from `config` + self.conn.DEFAULT_MAX_INBOUND_FRAME_SIZE = config.h2_max_inbound_frame_size + self.conn.local_settings = h2.settings.Settings( + client=False, + initial_values={ + h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS: config.h2_max_concurrent_streams, # noqa: E501 + h2.settings.SettingCodes.MAX_HEADER_LIST_SIZE: config.h2_max_header_list_size, # noqa: E501 + }, + ) self.ws_protocol_class = config.ws_protocol_class self.root_path = config.root_path From 3b5c906696128ce42579be2ae41ce04535e99c1e Mon Sep 17 00:00:00 2001 From: Vibhu Agarwal Date: Sun, 2 May 2021 04:02:09 +0530 Subject: [PATCH 22/39] h2c handling implementation added in h2_impl.py --- uvicorn/protocols/http/h2_impl.py | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/uvicorn/protocols/http/h2_impl.py b/uvicorn/protocols/http/h2_impl.py index e78f14b55..9e91dd2cc 100644 --- a/uvicorn/protocols/http/h2_impl.py +++ b/uvicorn/protocols/http/h2_impl.py @@ -149,12 +149,30 @@ def connection_made(self, transport, upgrade_request=None): if upgrade_request is None: self.conn.initiate_connection() + self.transport.write(self.conn.data_to_send()) else: - # TODO: Implementation for handling h2c upgrade - # Different implementations for httptools and h11 for handling h2c - return + # TODO: Implementations for httptools_impl to handle h2c + # For now, only h11_impl will handle h2c + + # if type(upgrade_request) == h11.Request + settings = "" + headers = [] + for name, value in upgrade_request.headers: + if name.lower() == b"http2-settings": + settings = value.decode() + elif name.lower() == b"host": + headers.append((b":authority", value)) + headers.append((name, value)) + headers.append((b":method", upgrade_request.method)) + headers.append((b":path", upgrade_request.target)) + headers.append((b":scheme", self.scheme.encode("ascii"))) + self.conn.initiate_upgrade_connection(settings) + self.transport.write(self.conn.data_to_send()) + event = h2.events.RequestReceived() + event.stream_id = 1 + event.headers = headers + self.on_request_received(event) - self.transport.write(self.conn.data_to_send()) if self.logger.level <= TRACE_LOG_LEVEL: prefix = "%s:%d - " % tuple(self.client) if self.client else "" self.logger.log(TRACE_LOG_LEVEL, "%sConnection made", prefix) @@ -314,6 +332,7 @@ def on_data_received(self, event: h2.events.DataReceived): ) else: # In Hypercorn: + # https://gitlab.com/pgjones/hypercorn/-/blob/0.11.2/src/hypercorn/protocol/h2.py#L233-235 # self.conn.acknowledge_received_data( # event.flow_controlled_length, event.stream_id # ) From a8dcf147b1044adf7627f03edc5b59dc02cc0fdf Mon Sep 17 00:00:00 2001 From: Vibhu Agarwal Date: Mon, 3 May 2021 20:12:38 +0530 Subject: [PATCH 23/39] Added ECDHE+AESGCM as SSL-Cipher-Suite for h2 --- uvicorn/config.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/uvicorn/config.py b/uvicorn/config.py index 17f0b92d0..b0eebd414 100644 --- a/uvicorn/config.py +++ b/uvicorn/config.py @@ -212,6 +212,7 @@ def __init__( self.h2_max_concurrent_streams = 100 self.h2_max_header_list_size = 2 ** 16 self.h2_max_inbound_frame_size = 2 ** 14 + self.h2_ssl_ciphers = "ECDHE+AESGCM" self.loaded = False self.configure_logging() @@ -288,6 +289,12 @@ def configure_logging(self): def load(self): assert not self.loaded + enable_h2 = self.http in ("h2",) + ciphers = ( + self.h2_ssl_ciphers + if (self.ssl_ciphers == "TLSv1" and enable_h2) + else self.ssl_ciphers + ) if self.is_ssl: self.ssl = create_ssl_context( keyfile=self.ssl_keyfile, @@ -296,8 +303,8 @@ def load(self): ssl_version=self.ssl_version, cert_reqs=self.ssl_cert_reqs, ca_certs=self.ssl_ca_certs, - ciphers=self.ssl_ciphers, - enable_h2=self.http in ("h2",), + ciphers=ciphers, + enable_h2=enable_h2, ) else: self.ssl = None From b683626db3affb51cffd54bc809ba3b662a4119d Mon Sep 17 00:00:00 2001 From: Vibhu Agarwal Date: Fri, 21 May 2021 02:34:48 +0530 Subject: [PATCH 24/39] Added TODO for Websocket Extended CONNECT --- uvicorn/protocols/http/h2_impl.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/uvicorn/protocols/http/h2_impl.py b/uvicorn/protocols/http/h2_impl.py index 9e91dd2cc..6452e95ba 100644 --- a/uvicorn/protocols/http/h2_impl.py +++ b/uvicorn/protocols/http/h2_impl.py @@ -108,6 +108,7 @@ def __init__(self, config, server_state, _loop=None): initial_values={ h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS: config.h2_max_concurrent_streams, # noqa: E501 h2.settings.SettingCodes.MAX_HEADER_LIST_SIZE: config.h2_max_header_list_size, # noqa: E501 + h2.settings.SettingCodes.ENABLE_CONNECT_PROTOCOL: 1, }, ) @@ -265,9 +266,13 @@ def on_request_received(self, event: h2.events.RequestReceived): b":method": "method", b":path": "raw_path", } + + websocket_protocol = False for key, value in event.headers: if key in scope_mapping: self.scope[scope_mapping[key]] = value.decode("ascii") + elif key == b":protocol" and value == b"websocket": + websocket_protocol = True else: self.scope["headers"].append((key.lower(), value)) path, _, query_string = self.scope["raw_path"].partition("?") @@ -276,6 +281,10 @@ def on_request_received(self, event: h2.events.RequestReceived): query_string.encode("ascii"), ) + if self.scope["method"] == "CONNECT" and websocket_protocol: + # TODO: Websocket Extended CONNECT Implementation + pass + # Handle 503 responses when 'limit_concurrency' is exceeded. if self.limit_concurrency is not None and ( len(self.connections) >= self.limit_concurrency From 4d4de114a1c6d850d4d6797cf5171753905358a9 Mon Sep 17 00:00:00 2001 From: Vibhu Agarwal Date: Fri, 28 May 2021 01:36:16 +0530 Subject: [PATCH 25/39] Refactor w.r.t. #1034 --- uvicorn/protocols/http/h2_impl.py | 60 ++++--------------------------- 1 file changed, 7 insertions(+), 53 deletions(-) diff --git a/uvicorn/protocols/http/h2_impl.py b/uvicorn/protocols/http/h2_impl.py index 6452e95ba..53ca434ce 100644 --- a/uvicorn/protocols/http/h2_impl.py +++ b/uvicorn/protocols/http/h2_impl.py @@ -11,6 +11,13 @@ import h2.exceptions import h2.settings +from uvicorn.protocols.http.flow_control import ( + CLOSE_HEADER, + HIGH_WATER_LIMIT, + TRACE_LOG_LEVEL, + FlowControl, + service_unavailable, +) from uvicorn.protocols.utils import ( get_client_addr, get_local_addr, @@ -31,62 +38,9 @@ def _get_status_phrase(status_code): status_code: _get_status_phrase(status_code) for status_code in range(100, 600) } -CLOSE_HEADER = (b"connection", b"close") - -HIGH_WATER_LIMIT = 65536 - -TRACE_LOG_LEVEL = 5 - - _StreamRequest = collections.namedtuple("_StreamRequest", ("headers", "scope", "cycle")) -class FlowControl: - def __init__(self, transport): - self._transport = transport - self.read_paused = False - self.write_paused = False - self._is_writable_event = asyncio.Event() - self._is_writable_event.set() - - async def drain(self): - await self._is_writable_event.wait() - - def pause_reading(self): - if not self.read_paused: - self.read_paused = True - self._transport.pause_reading() - - def resume_reading(self): - if self.read_paused: - self.read_paused = False - self._transport.resume_reading() - - def pause_writing(self): - if not self.write_paused: - self.write_paused = True - self._is_writable_event.clear() - - def resume_writing(self): - if self.write_paused: - self.write_paused = False - self._is_writable_event.set() - - -async def service_unavailable(scope, receive, send): - await send( - { - "type": "http.response.start", - "status": 503, - "headers": [ - (b"content-type", b"text/plain; charset=utf-8"), - (b"connection", b"close"), - ], - } - ) - await send({"type": "http.response.body", "body": b"Service Unavailable"}) - - class H2Protocol(asyncio.Protocol): def __init__(self, config, server_state, _loop=None): if not config.loaded: From a0deef4049ca3973e9e22aceb1fb6f1ff322b016 Mon Sep 17 00:00:00 2001 From: Vibhu Agarwal Date: Sun, 30 May 2021 01:08:38 +0530 Subject: [PATCH 26/39] Remove irrelevant return statement --- uvicorn/protocols/http/h11_impl.py | 1 - 1 file changed, 1 deletion(-) diff --git a/uvicorn/protocols/http/h11_impl.py b/uvicorn/protocols/http/h11_impl.py index 7857b4c33..be6cf5877 100644 --- a/uvicorn/protocols/http/h11_impl.py +++ b/uvicorn/protocols/http/h11_impl.py @@ -309,7 +309,6 @@ def handle_upgrade(self, event): output = self.conn.send(event) self.transport.write(output) self.transport.close() - return def on_response_complete(self): self.server_state.total_requests += 1 From 96fe3d753bdf3a5a29a59dcf5efe0794ed7cc192 Mon Sep 17 00:00:00 2001 From: Vibhu Agarwal Date: Mon, 31 May 2021 02:23:57 +0530 Subject: [PATCH 27/39] tests: add coverage for create_ssl_context [config.py] --- tests/test_config.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_config.py b/tests/test_config.py index ab014c4c1..7e67efc61 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -160,6 +160,18 @@ def test_ssl_config_combined(tls_certificate_pem_path): assert config.is_ssl is True +def test_ssl_config_h2(tls_certificate_pem_path): + config = Config( + app=asgi_app, + http="h2", + ssl_certfile=tls_certificate_pem_path, + ) + config.load() + + assert config.is_ssl is True + # TODO: Should we also check HTTP/2-Specific 'ciphers' and 'options' here? + + def asgi2_app(scope): async def asgi(receive, send): # pragma: nocover pass From e7e00db65206edea1755cb3bc1f07246c6082bbe Mon Sep 17 00:00:00 2001 From: Vibhu Agarwal Date: Tue, 1 Jun 2021 01:49:42 +0530 Subject: [PATCH 28/39] Fix h2c bug in h11_impl.py --- uvicorn/protocols/http/h11_impl.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/uvicorn/protocols/http/h11_impl.py b/uvicorn/protocols/http/h11_impl.py index be6cf5877..fab4d50b5 100644 --- a/uvicorn/protocols/http/h11_impl.py +++ b/uvicorn/protocols/http/h11_impl.py @@ -181,6 +181,9 @@ def handle_events(self): if b"upgrade" in tokens: self.handle_upgrade(event) return + elif name == b"upgrade" and value.lower() == b"h2c": + self.handle_upgrade(event) + return # Handle 503 responses when 'limit_concurrency' is exceeded. if self.limit_concurrency is not None and ( @@ -254,7 +257,7 @@ def handle_upgrade(self, event): elif name in {"content-length", "transfer-encoding"}: has_body = True - if upgrade_value.lower() == "h2c" and not has_body: + if upgrade_value == b"h2c" and not has_body: self.connections.discard(self) headers = ((b"upgrade", b"h2c"), *self.headers) From 0e7df0a1955019b31bbf8dd783da5231a3704806 Mon Sep 17 00:00:00 2001 From: Vibhu Agarwal Date: Tue, 1 Jun 2021 02:22:12 +0530 Subject: [PATCH 29/39] Refactor w.r.t. #869 --- uvicorn/protocols/http/h2_impl.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/uvicorn/protocols/http/h2_impl.py b/uvicorn/protocols/http/h2_impl.py index 53ca434ce..216fc0868 100644 --- a/uvicorn/protocols/http/h2_impl.py +++ b/uvicorn/protocols/http/h2_impl.py @@ -42,12 +42,13 @@ def _get_status_phrase(status_code): class H2Protocol(asyncio.Protocol): - def __init__(self, config, server_state, _loop=None): + def __init__(self, config, server_state, on_connection_lost=None, _loop=None): if not config.loaded: config.load() self.config = config self.app = config.loaded_app + self.on_connection_lost = on_connection_lost self.loop = _loop or asyncio.get_event_loop() self.logger = logging.getLogger("uvicorn.error") self.access_logger = logging.getLogger("uvicorn.access") @@ -157,6 +158,9 @@ def connection_lost(self, exc): if self.flow is not None: self.flow.resume_writing() + if self.on_connection_lost is not None: + self.on_connection_lost() + def _unset_keepalive_if_required(self): if self.timeout_keep_alive_task is not None: self.timeout_keep_alive_task.cancel() From 59c95bf6d4251fc9ccd484d18d89aa37dab1e1c4 Mon Sep 17 00:00:00 2001 From: Vibhu Agarwal Date: Tue, 1 Jun 2021 02:50:38 +0530 Subject: [PATCH 30/39] Fix bugs (similar to ones in previous commit) --- uvicorn/protocols/http/h11_impl.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/uvicorn/protocols/http/h11_impl.py b/uvicorn/protocols/http/h11_impl.py index fab4d50b5..a900ab7db 100644 --- a/uvicorn/protocols/http/h11_impl.py +++ b/uvicorn/protocols/http/h11_impl.py @@ -238,7 +238,9 @@ def check_connection_preface(self, event): self.connections.discard(self) protocol = self.config.h2_protocol_class( - config=self.config, server_state=self.server_state + config=self.config, + server_state=self.server_state, + on_connection_lost=self.on_connection_lost, ) protocol.connection_made(self.transport) self.transport.set_protocol(protocol) @@ -267,7 +269,9 @@ def handle_upgrade(self, event): ) ) protocol = self.config.h2_protocol_class( - config=self.config, server_state=self.server_state + config=self.config, + server_state=self.server_state, + on_connection_lost=self.on_connection_lost, ) protocol.connection_made(self.transport, upgrade_request=event) self.transport.set_protocol(protocol) From 7cae8380be36ce650954fec0b5e20ef1ec374800 Mon Sep 17 00:00:00 2001 From: Vibhu Agarwal Date: Tue, 1 Jun 2021 19:53:30 +0530 Subject: [PATCH 31/39] auto upgrade (h11=>h2), when alpn_protocol == "h2" --- uvicorn/_handlers/http.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/uvicorn/_handlers/http.py b/uvicorn/_handlers/http.py index 492d9d78a..8721c6b37 100644 --- a/uvicorn/_handlers/http.py +++ b/uvicorn/_handlers/http.py @@ -35,6 +35,12 @@ async def handle_http( loop = asyncio.get_event_loop() connection_lost = loop.create_future() + ssl_object = writer.get_extra_info("ssl_object") + if ssl_object is not None: + alpn_protocol = ssl_object.selected_alpn_protocol() + if alpn_protocol == "h2": + config.http_protocol_class = config.h2_protocol_class + # Switch the protocol from the stream reader to our own HTTP protocol class. protocol = config.http_protocol_class( config=config, From 56140b9e0c8fe2a38fc62c62821d83c92030019d Mon Sep 17 00:00:00 2001 From: Vibhu Agarwal Date: Tue, 1 Jun 2021 20:30:08 +0530 Subject: [PATCH 32/39] Added a TODO for a probable implementation done in #929 --- uvicorn/protocols/http/h2_impl.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/uvicorn/protocols/http/h2_impl.py b/uvicorn/protocols/http/h2_impl.py index 216fc0868..fd7e1013e 100644 --- a/uvicorn/protocols/http/h2_impl.py +++ b/uvicorn/protocols/http/h2_impl.py @@ -158,6 +158,8 @@ def connection_lost(self, exc): if self.flow is not None: self.flow.resume_writing() + # TODO: https://github.com/encode/uvicorn/pull/929#discussion_r582739354 + if self.on_connection_lost is not None: self.on_connection_lost() From 81f21be5ae656978528ad95d19136e267912b9a1 Mon Sep 17 00:00:00 2001 From: Vibhu Agarwal Date: Tue, 1 Jun 2021 21:39:42 +0530 Subject: [PATCH 33/39] Added a couple of EMPTY tests (h2c & PRI-h2) --- tests/protocols/test_http.py | 38 ++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/protocols/test_http.py b/tests/protocols/test_http.py index 577f70edb..ed4ec31ba 100644 --- a/tests/protocols/test_http.py +++ b/tests/protocols/test_http.py @@ -73,6 +73,26 @@ ] ) +UPGRADE_REQUEST_h2c = b"\r\n".join( + [ + b"GET / HTTP/1.1", + b"Host: example.org", + b"Upgrade: h2c", + b"HTTP2-Settings: SomeHTTP2Setting", + b"", + b"", + ] +) + +UPGRADE_REQUEST_HTTP2_PRIOR = b"\r\n".join( + [ + b"PRI * HTTP/2.0", + b"", + b"", + ] +) + + INVALID_REQUEST_TEMPLATE = b"\r\n".join( [ b"%s", @@ -683,6 +703,24 @@ def test_supported_upgrade_request(protocol_cls, event_loop): assert b"HTTP/1.1 426 " in protocol.transport.buffer +@pytest.mark.parametrize("protocol_cls", [H11Protocol]) +def test_h2c_upgrade_request(protocol_cls, event_loop): + app = Response("Hello, world", media_type="text/plain") + + with get_connected_protocol(app, protocol_cls, event_loop) as protocol: + protocol.data_received(UPGRADE_REQUEST_h2c) + # TODO: check h2c_upgrade_request response + + +@pytest.mark.parametrize("protocol_cls", [H11Protocol]) +def test_h2_prior_upgrade_request(protocol_cls, event_loop): + app = Response("Hello, world", media_type="text/plain") + + with get_connected_protocol(app, protocol_cls, event_loop) as protocol: + protocol.data_received(UPGRADE_REQUEST_HTTP2_PRIOR) + # TODO: check h2_prior_upgrade_request response + + async def asgi3app(scope, receive, send): pass From 0e6ea5177f06b88586cdb278c2811ff8affd3b12 Mon Sep 17 00:00:00 2001 From: Vibhu Agarwal Date: Thu, 3 Jun 2021 03:02:46 +0530 Subject: [PATCH 34/39] Added test_h2.py with a simple POST request --- requirements.txt | 1 + tests/protocols/test_h2.py | 41 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 tests/protocols/test_h2.py diff --git a/requirements.txt b/requirements.txt index bcc6b7dcc..e0bed6bfc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,6 +18,7 @@ mypy trustme cryptography coverage +starlette httpx==0.16.* pytest-asyncio==0.14.* async_generator; python_version < '3.7' diff --git a/tests/protocols/test_h2.py b/tests/protocols/test_h2.py new file mode 100644 index 000000000..94da70553 --- /dev/null +++ b/tests/protocols/test_h2.py @@ -0,0 +1,41 @@ +import httpx +import pytest +from starlette.applications import Starlette +from starlette.responses import JSONResponse +from starlette.routing import Route + +from tests.utils import run_server +from uvicorn.config import Config + + +async def homepage(request): + return JSONResponse({"hello": "world"}) + + +app = Starlette( + routes=[ + Route("/", homepage, methods=["GET", "POST"]), + ], +) + + +@pytest.mark.asyncio +async def test_run( + tls_ca_ssl_context, tls_ca_certificate_pem_path, tls_ca_certificate_private_key_path +): + config = Config( + app=app, + http="h2", + loop="asyncio", + limit_max_requests=1, + ssl_keyfile=tls_ca_certificate_private_key_path, + ssl_certfile=tls_ca_certificate_pem_path, + ssl_ca_certs=tls_ca_certificate_pem_path, + ) + async with run_server(config): + async with httpx.AsyncClient(verify=tls_ca_ssl_context, http2=True) as client: + response = await client.post( + "https://127.0.0.1:8000", data={"hello": "world"} + ) + assert response.status_code == 200 + assert response.http_version == "HTTP/2" From ad3f35f80f275ea19b32d2a7e9ed8d0f2a3cd011 Mon Sep 17 00:00:00 2001 From: Vibhu Agarwal Date: Thu, 3 Jun 2021 03:09:00 +0530 Subject: [PATCH 35/39] Remove test_h2c_upgrade_request from test_http.py --- tests/protocols/test_http.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/protocols/test_http.py b/tests/protocols/test_http.py index ed4ec31ba..55c473012 100644 --- a/tests/protocols/test_http.py +++ b/tests/protocols/test_http.py @@ -703,13 +703,13 @@ def test_supported_upgrade_request(protocol_cls, event_loop): assert b"HTTP/1.1 426 " in protocol.transport.buffer -@pytest.mark.parametrize("protocol_cls", [H11Protocol]) -def test_h2c_upgrade_request(protocol_cls, event_loop): - app = Response("Hello, world", media_type="text/plain") - - with get_connected_protocol(app, protocol_cls, event_loop) as protocol: - protocol.data_received(UPGRADE_REQUEST_h2c) - # TODO: check h2c_upgrade_request response +# @pytest.mark.parametrize("protocol_cls", [H11Protocol]) +# def test_h2c_upgrade_request(protocol_cls, event_loop): +# app = Response("Hello, world", media_type="text/plain") +# +# with get_connected_protocol(app, protocol_cls, event_loop) as protocol: +# protocol.data_received(UPGRADE_REQUEST_h2c) +# # TODO: check h2c_upgrade_request response @pytest.mark.parametrize("protocol_cls", [H11Protocol]) From f4665bbfbab29d5b8d6cb6677dfc2799ceef3b83 Mon Sep 17 00:00:00 2001 From: Vibhu Agarwal Date: Sun, 6 Jun 2021 15:30:05 +0530 Subject: [PATCH 36/39] Fix the STUPID bug that has been freakin me out --- tests/protocols/test_http.py | 14 +++++++------- uvicorn/protocols/http/h2_impl.py | 4 +++- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/protocols/test_http.py b/tests/protocols/test_http.py index 55c473012..ed4ec31ba 100644 --- a/tests/protocols/test_http.py +++ b/tests/protocols/test_http.py @@ -703,13 +703,13 @@ def test_supported_upgrade_request(protocol_cls, event_loop): assert b"HTTP/1.1 426 " in protocol.transport.buffer -# @pytest.mark.parametrize("protocol_cls", [H11Protocol]) -# def test_h2c_upgrade_request(protocol_cls, event_loop): -# app = Response("Hello, world", media_type="text/plain") -# -# with get_connected_protocol(app, protocol_cls, event_loop) as protocol: -# protocol.data_received(UPGRADE_REQUEST_h2c) -# # TODO: check h2c_upgrade_request response +@pytest.mark.parametrize("protocol_cls", [H11Protocol]) +def test_h2c_upgrade_request(protocol_cls, event_loop): + app = Response("Hello, world", media_type="text/plain") + + with get_connected_protocol(app, protocol_cls, event_loop) as protocol: + protocol.data_received(UPGRADE_REQUEST_h2c) + # TODO: check h2c_upgrade_request response @pytest.mark.parametrize("protocol_cls", [H11Protocol]) diff --git a/uvicorn/protocols/http/h2_impl.py b/uvicorn/protocols/http/h2_impl.py index fd7e1013e..6385f3242 100644 --- a/uvicorn/protocols/http/h2_impl.py +++ b/uvicorn/protocols/http/h2_impl.py @@ -158,7 +158,9 @@ def connection_lost(self, exc): if self.flow is not None: self.flow.resume_writing() - # TODO: https://github.com/encode/uvicorn/pull/929#discussion_r582739354 + if exc is None: + # Ref: https://github.com/encode/uvicorn/pull/929#discussion_r582739354 + self.transport.close() if self.on_connection_lost is not None: self.on_connection_lost() From 49f251a665bfdcf6673fad2f714e8a57da7bf593 Mon Sep 17 00:00:00 2001 From: Vibhu Agarwal Date: Sun, 6 Jun 2021 15:42:02 +0530 Subject: [PATCH 37/39] Please fix! --- tests/protocols/test_http.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/protocols/test_http.py b/tests/protocols/test_http.py index ed4ec31ba..55c473012 100644 --- a/tests/protocols/test_http.py +++ b/tests/protocols/test_http.py @@ -703,13 +703,13 @@ def test_supported_upgrade_request(protocol_cls, event_loop): assert b"HTTP/1.1 426 " in protocol.transport.buffer -@pytest.mark.parametrize("protocol_cls", [H11Protocol]) -def test_h2c_upgrade_request(protocol_cls, event_loop): - app = Response("Hello, world", media_type="text/plain") - - with get_connected_protocol(app, protocol_cls, event_loop) as protocol: - protocol.data_received(UPGRADE_REQUEST_h2c) - # TODO: check h2c_upgrade_request response +# @pytest.mark.parametrize("protocol_cls", [H11Protocol]) +# def test_h2c_upgrade_request(protocol_cls, event_loop): +# app = Response("Hello, world", media_type="text/plain") +# +# with get_connected_protocol(app, protocol_cls, event_loop) as protocol: +# protocol.data_received(UPGRADE_REQUEST_h2c) +# # TODO: check h2c_upgrade_request response @pytest.mark.parametrize("protocol_cls", [H11Protocol]) From 2741e0f9b638d7044b939cf4ce1f7242a68bc6d2 Mon Sep 17 00:00:00 2001 From: Vibhu Agarwal Date: Sun, 6 Jun 2021 22:35:09 +0530 Subject: [PATCH 38/39] Remove "handle_upgrade()" from h2_impl.py --- uvicorn/protocols/http/h2_impl.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/uvicorn/protocols/http/h2_impl.py b/uvicorn/protocols/http/h2_impl.py index 6385f3242..7840c7609 100644 --- a/uvicorn/protocols/http/h2_impl.py +++ b/uvicorn/protocols/http/h2_impl.py @@ -94,7 +94,7 @@ def __init__(self, config, server_state, on_connection_lost=None, _loop=None): self.streams = {} # Protocol interface - def connection_made(self, transport, upgrade_request=None): + def connection_made(self, transport: asyncio.Transport, upgrade_request=None): self.connections.add(self) self.transport = transport @@ -376,9 +376,6 @@ def on_response_complete(self): # Unpause data reads if needed. self.flow.resume_reading() - def handle_upgrade(self, event): - pass - def pause_writing(self): """ Called by the transport when the write buffer exceeds the high water mark. From df02d8c2f4ce2c9d19c0cb68b2ca614ccf02811a Mon Sep 17 00:00:00 2001 From: Vibhu Agarwal Date: Mon, 7 Jun 2021 03:47:46 +0530 Subject: [PATCH 39/39] on the way to fix h2c --- uvicorn/protocols/http/h11_impl.py | 3 +++ uvicorn/protocols/http/h2_impl.py | 1 + 2 files changed, 4 insertions(+) diff --git a/uvicorn/protocols/http/h11_impl.py b/uvicorn/protocols/http/h11_impl.py index a764a4a44..8da9d5ac1 100644 --- a/uvicorn/protocols/http/h11_impl.py +++ b/uvicorn/protocols/http/h11_impl.py @@ -277,6 +277,9 @@ def handle_upgrade(self, event): ) protocol.connection_made(self.transport, upgrade_request=event) self.transport.set_protocol(protocol) + request_data = self.conn.trailing_data[0] + if request_data != b"": + protocol.data_received(request_data) elif upgrade_value == b"websocket" and self.ws_protocol_class is not None: self.connections.discard(self) diff --git a/uvicorn/protocols/http/h2_impl.py b/uvicorn/protocols/http/h2_impl.py index 7840c7609..6ab534e7b 100644 --- a/uvicorn/protocols/http/h2_impl.py +++ b/uvicorn/protocols/http/h2_impl.py @@ -128,6 +128,7 @@ def connection_made(self, transport: asyncio.Transport, upgrade_request=None): event.stream_id = 1 event.headers = headers self.on_request_received(event) + self.on_stream_ended(event) if self.logger.level <= TRACE_LOG_LEVEL: prefix = "%s:%d - " % tuple(self.client) if self.client else ""