HTTP/3 Support (#2378)
This commit is contained in:
@@ -4,7 +4,7 @@ from inspect import isawaitable
|
||||
from typing import TYPE_CHECKING, Any, Callable, Iterable, Optional
|
||||
|
||||
|
||||
if TYPE_CHECKING: # no cov
|
||||
if TYPE_CHECKING:
|
||||
from sanic import Sanic
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
|
||||
if TYPE_CHECKING: # no cov
|
||||
if TYPE_CHECKING:
|
||||
from sanic.app import Sanic
|
||||
|
||||
import asyncio
|
||||
|
||||
@@ -2,10 +2,14 @@ from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from aioquic.h3.connection import H3_ALPN, H3Connection
|
||||
|
||||
from sanic.http.constants import HTTP
|
||||
from sanic.http.http3 import Http3
|
||||
from sanic.touchup.meta import TouchUpMeta
|
||||
|
||||
|
||||
if TYPE_CHECKING: # no cov
|
||||
if TYPE_CHECKING:
|
||||
from sanic.app import Sanic
|
||||
|
||||
import sys
|
||||
@@ -13,24 +17,68 @@ import sys
|
||||
from asyncio import CancelledError
|
||||
from time import monotonic as current_time
|
||||
|
||||
from aioquic.asyncio import QuicConnectionProtocol
|
||||
from aioquic.quic.events import (
|
||||
DatagramFrameReceived,
|
||||
ProtocolNegotiated,
|
||||
QuicEvent,
|
||||
)
|
||||
|
||||
from sanic.exceptions import RequestTimeout, ServiceUnavailable
|
||||
from sanic.http import Http, Stage
|
||||
from sanic.log import error_logger, logger
|
||||
from sanic.log import Colors, error_logger, logger
|
||||
from sanic.models.server_types import ConnInfo
|
||||
from sanic.request import Request
|
||||
from sanic.server.protocols.base_protocol import SanicProtocol
|
||||
|
||||
|
||||
class HttpProtocol(SanicProtocol, metaclass=TouchUpMeta):
|
||||
class HttpProtocolMixin:
|
||||
__slots__ = ()
|
||||
__version__: HTTP
|
||||
|
||||
def _setup_connection(self, *args, **kwargs):
|
||||
self._http = self.HTTP_CLASS(self, *args, **kwargs)
|
||||
self._time = current_time()
|
||||
try:
|
||||
self.check_timeouts()
|
||||
except AttributeError:
|
||||
...
|
||||
|
||||
def _setup(self):
|
||||
self.request: Optional[Request] = None
|
||||
self.access_log = self.app.config.ACCESS_LOG
|
||||
self.request_handler = self.app.handle_request
|
||||
self.error_handler = self.app.error_handler
|
||||
self.request_timeout = self.app.config.REQUEST_TIMEOUT
|
||||
self.response_timeout = self.app.config.RESPONSE_TIMEOUT
|
||||
self.keep_alive_timeout = self.app.config.KEEP_ALIVE_TIMEOUT
|
||||
self.request_max_size = self.app.config.REQUEST_MAX_SIZE
|
||||
self.request_class = self.app.request_class or Request
|
||||
|
||||
@property
|
||||
def http(self):
|
||||
if not hasattr(self, "_http"):
|
||||
return None
|
||||
return self._http
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
return self.__class__.__version__
|
||||
|
||||
|
||||
class HttpProtocol(HttpProtocolMixin, SanicProtocol, metaclass=TouchUpMeta):
|
||||
"""
|
||||
This class provides implements the HTTP 1.1 protocol on top of our
|
||||
Sanic Server transport
|
||||
"""
|
||||
|
||||
HTTP_CLASS = Http
|
||||
|
||||
__touchup__ = (
|
||||
"send",
|
||||
"connection_task",
|
||||
)
|
||||
__version__ = HTTP.VERSION_1
|
||||
__slots__ = (
|
||||
# request params
|
||||
"request",
|
||||
@@ -72,25 +120,12 @@ class HttpProtocol(SanicProtocol, metaclass=TouchUpMeta):
|
||||
unix=unix,
|
||||
)
|
||||
self.url = None
|
||||
self.request: Optional[Request] = None
|
||||
self.access_log = self.app.config.ACCESS_LOG
|
||||
self.request_handler = self.app.handle_request
|
||||
self.error_handler = self.app.error_handler
|
||||
self.request_timeout = self.app.config.REQUEST_TIMEOUT
|
||||
self.response_timeout = self.app.config.RESPONSE_TIMEOUT
|
||||
self.keep_alive_timeout = self.app.config.KEEP_ALIVE_TIMEOUT
|
||||
self.request_max_size = self.app.config.REQUEST_MAX_SIZE
|
||||
self.request_class = self.app.request_class or Request
|
||||
self.state = state if state else {}
|
||||
self._setup()
|
||||
if "requests_count" not in self.state:
|
||||
self.state["requests_count"] = 0
|
||||
self._exception = None
|
||||
|
||||
def _setup_connection(self):
|
||||
self._http = Http(self)
|
||||
self._time = current_time()
|
||||
self.check_timeouts()
|
||||
|
||||
async def connection_task(self): # no cov
|
||||
"""
|
||||
Run a HTTP connection.
|
||||
@@ -241,3 +276,39 @@ class HttpProtocol(SanicProtocol, metaclass=TouchUpMeta):
|
||||
self._data_received.set()
|
||||
except Exception:
|
||||
error_logger.exception("protocol.data_received")
|
||||
|
||||
|
||||
class Http3Protocol(HttpProtocolMixin, QuicConnectionProtocol):
|
||||
HTTP_CLASS = Http3
|
||||
__version__ = HTTP.VERSION_3
|
||||
|
||||
def __init__(self, *args, app: Sanic, **kwargs) -> None:
|
||||
self.app = app
|
||||
super().__init__(*args, **kwargs)
|
||||
self._setup()
|
||||
self._connection: Optional[H3Connection] = None
|
||||
|
||||
def quic_event_received(self, event: QuicEvent) -> None:
|
||||
logger.debug(
|
||||
f"{Colors.BLUE}[quic_event_received]: "
|
||||
f"{Colors.PURPLE}{event}{Colors.END}",
|
||||
extra={"verbosity": 2},
|
||||
)
|
||||
if isinstance(event, ProtocolNegotiated):
|
||||
self._setup_connection(transmit=self.transmit)
|
||||
if event.alpn_protocol in H3_ALPN:
|
||||
self._connection = H3Connection(
|
||||
self._quic, enable_webtransport=True
|
||||
)
|
||||
elif isinstance(event, DatagramFrameReceived):
|
||||
if event.data == b"quack":
|
||||
self._quic.send_datagram_frame(b"quack-ack")
|
||||
|
||||
# pass event to the HTTP layer
|
||||
if self._connection is not None:
|
||||
for http_event in self._connection.handle_event(event):
|
||||
self._http.http_event_received(http_event)
|
||||
|
||||
@property
|
||||
def connection(self) -> Optional[H3Connection]:
|
||||
return self._connection
|
||||
|
||||
@@ -11,7 +11,7 @@ from sanic.server import HttpProtocol
|
||||
from ..websockets.impl import WebsocketImplProtocol
|
||||
|
||||
|
||||
if TYPE_CHECKING: # no cov
|
||||
if TYPE_CHECKING:
|
||||
from websockets import http11
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ from ssl import SSLContext
|
||||
from typing import TYPE_CHECKING, Dict, Optional, Type, Union
|
||||
|
||||
from sanic.config import Config
|
||||
from sanic.http.constants import HTTP
|
||||
from sanic.http.tls import get_ssl_context
|
||||
from sanic.server.events import trigger_events
|
||||
|
||||
|
||||
@@ -21,12 +23,15 @@ from functools import partial
|
||||
from signal import SIG_IGN, SIGINT, SIGTERM, Signals
|
||||
from signal import signal as signal_func
|
||||
|
||||
from aioquic.asyncio import serve as quic_serve
|
||||
|
||||
from sanic.application.ext import setup_ext
|
||||
from sanic.compat import OS_IS_WINDOWS, ctrlc_workaround_for_windows
|
||||
from sanic.http.http3 import SessionTicketStore, get_config
|
||||
from sanic.log import error_logger, logger
|
||||
from sanic.models.server_types import Signal
|
||||
from sanic.server.async_server import AsyncioServer
|
||||
from sanic.server.protocols.http_protocol import HttpProtocol
|
||||
from sanic.server.protocols.http_protocol import Http3Protocol, HttpProtocol
|
||||
from sanic.server.socket import (
|
||||
bind_socket,
|
||||
bind_unix_socket,
|
||||
@@ -52,6 +57,7 @@ def serve(
|
||||
signal=Signal(),
|
||||
state=None,
|
||||
asyncio_server_kwargs=None,
|
||||
version=HTTP.VERSION_1,
|
||||
):
|
||||
"""Start asynchronous HTTP Server on an individual process.
|
||||
|
||||
@@ -88,6 +94,87 @@ def serve(
|
||||
|
||||
app.asgi = False
|
||||
|
||||
if version is HTTP.VERSION_3:
|
||||
return _serve_http_3(host, port, app, loop, ssl)
|
||||
return _serve_http_1(
|
||||
host,
|
||||
port,
|
||||
app,
|
||||
ssl,
|
||||
sock,
|
||||
unix,
|
||||
reuse_port,
|
||||
loop,
|
||||
protocol,
|
||||
backlog,
|
||||
register_sys_signals,
|
||||
run_multiple,
|
||||
run_async,
|
||||
connections,
|
||||
signal,
|
||||
state,
|
||||
asyncio_server_kwargs,
|
||||
)
|
||||
|
||||
|
||||
def _setup_system_signals(
|
||||
app: Sanic,
|
||||
run_multiple: bool,
|
||||
register_sys_signals: bool,
|
||||
loop: asyncio.AbstractEventLoop,
|
||||
) -> None:
|
||||
# Ignore SIGINT when run_multiple
|
||||
if run_multiple:
|
||||
signal_func(SIGINT, SIG_IGN)
|
||||
os.environ["SANIC_WORKER_PROCESS"] = "true"
|
||||
|
||||
# Register signals for graceful termination
|
||||
if register_sys_signals:
|
||||
if OS_IS_WINDOWS:
|
||||
ctrlc_workaround_for_windows(app)
|
||||
else:
|
||||
for _signal in [SIGTERM] if run_multiple else [SIGINT, SIGTERM]:
|
||||
loop.add_signal_handler(_signal, app.stop)
|
||||
|
||||
|
||||
def _run_server_forever(loop, before_stop, after_stop, cleanup, unix):
|
||||
pid = os.getpid()
|
||||
try:
|
||||
logger.info("Starting worker [%s]", pid)
|
||||
loop.run_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
logger.info("Stopping worker [%s]", pid)
|
||||
|
||||
loop.run_until_complete(before_stop())
|
||||
|
||||
if cleanup:
|
||||
cleanup()
|
||||
|
||||
loop.run_until_complete(after_stop())
|
||||
remove_unix_socket(unix)
|
||||
|
||||
|
||||
def _serve_http_1(
|
||||
host,
|
||||
port,
|
||||
app,
|
||||
ssl,
|
||||
sock,
|
||||
unix,
|
||||
reuse_port,
|
||||
loop,
|
||||
protocol,
|
||||
backlog,
|
||||
register_sys_signals,
|
||||
run_multiple,
|
||||
run_async,
|
||||
connections,
|
||||
signal,
|
||||
state,
|
||||
asyncio_server_kwargs,
|
||||
):
|
||||
connections = connections if connections is not None else set()
|
||||
protocol_kwargs = _build_protocol_kwargs(protocol, app.config)
|
||||
server = partial(
|
||||
@@ -135,30 +222,7 @@ def serve(
|
||||
error_logger.exception("Unable to start server", exc_info=True)
|
||||
return
|
||||
|
||||
# Ignore SIGINT when run_multiple
|
||||
if run_multiple:
|
||||
signal_func(SIGINT, SIG_IGN)
|
||||
os.environ["SANIC_WORKER_PROCESS"] = "true"
|
||||
|
||||
# Register signals for graceful termination
|
||||
if register_sys_signals:
|
||||
if OS_IS_WINDOWS:
|
||||
ctrlc_workaround_for_windows(app)
|
||||
else:
|
||||
for _signal in [SIGTERM] if run_multiple else [SIGINT, SIGTERM]:
|
||||
loop.add_signal_handler(_signal, app.stop)
|
||||
|
||||
loop.run_until_complete(app._server_event("init", "after"))
|
||||
pid = os.getpid()
|
||||
try:
|
||||
logger.info("Starting worker [%s]", pid)
|
||||
loop.run_forever()
|
||||
finally:
|
||||
logger.info("Stopping worker [%s]", pid)
|
||||
|
||||
# Run the on_stop function if provided
|
||||
loop.run_until_complete(app._server_event("shutdown", "before"))
|
||||
|
||||
def _cleanup():
|
||||
# Wait for event loop to finish and all connections to drain
|
||||
http_server.close()
|
||||
loop.run_until_complete(http_server.wait_closed())
|
||||
@@ -188,8 +252,51 @@ def serve(
|
||||
conn.websocket.fail_connection(code=1001)
|
||||
else:
|
||||
conn.abort()
|
||||
loop.run_until_complete(app._server_event("shutdown", "after"))
|
||||
remove_unix_socket(unix)
|
||||
|
||||
_setup_system_signals(app, run_multiple, register_sys_signals, loop)
|
||||
loop.run_until_complete(app._server_event("init", "after"))
|
||||
_run_server_forever(
|
||||
loop,
|
||||
partial(app._server_event, "shutdown", "before"),
|
||||
partial(app._server_event, "shutdown", "after"),
|
||||
_cleanup,
|
||||
unix,
|
||||
)
|
||||
|
||||
|
||||
def _serve_http_3(
|
||||
host,
|
||||
port,
|
||||
app,
|
||||
loop,
|
||||
ssl,
|
||||
register_sys_signals: bool = True,
|
||||
run_multiple: bool = False,
|
||||
):
|
||||
protocol = partial(Http3Protocol, app=app)
|
||||
ticket_store = SessionTicketStore()
|
||||
ssl_context = get_ssl_context(app, ssl)
|
||||
config = get_config(app, ssl_context)
|
||||
coro = quic_serve(
|
||||
host,
|
||||
port,
|
||||
configuration=config,
|
||||
create_protocol=protocol,
|
||||
session_ticket_fetcher=ticket_store.pop,
|
||||
session_ticket_handler=ticket_store.add,
|
||||
)
|
||||
server = AsyncioServer(app, loop, coro, [])
|
||||
loop.run_until_complete(server.startup())
|
||||
loop.run_until_complete(server.before_start())
|
||||
loop.run_until_complete(server)
|
||||
_setup_system_signals(app, run_multiple, register_sys_signals, loop)
|
||||
loop.run_until_complete(server.after_start())
|
||||
|
||||
# TODO: Create connection cleanup and graceful shutdown
|
||||
cleanup = None
|
||||
_run_server_forever(
|
||||
loop, server.before_stop, server.after_stop, cleanup, None
|
||||
)
|
||||
|
||||
|
||||
def serve_single(server_settings):
|
||||
|
||||
@@ -9,7 +9,7 @@ from websockets.typing import Data
|
||||
from sanic.exceptions import ServerError
|
||||
|
||||
|
||||
if TYPE_CHECKING: # no cov
|
||||
if TYPE_CHECKING:
|
||||
from .impl import WebsocketImplProtocol
|
||||
|
||||
UTF8Decoder = codecs.getincrementaldecoder("utf-8")
|
||||
@@ -37,7 +37,7 @@ class WebsocketFrameAssembler:
|
||||
"get_id",
|
||||
"put_id",
|
||||
)
|
||||
if TYPE_CHECKING: # no cov
|
||||
if TYPE_CHECKING:
|
||||
protocol: "WebsocketImplProtocol"
|
||||
read_mutex: asyncio.Lock
|
||||
write_mutex: asyncio.Lock
|
||||
|
||||
Reference in New Issue
Block a user