Merge branch 'main' into zhiwei/route-overwrite

This commit is contained in:
Zhiwei 2023-03-20 09:56:24 -04:00 committed by GitHub
commit e5696342f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 830 additions and 85 deletions

View File

@ -2,6 +2,22 @@ from sanic.__version__ import __version__
from sanic.app import Sanic from sanic.app import Sanic
from sanic.blueprints import Blueprint from sanic.blueprints import Blueprint
from sanic.constants import HTTPMethod from sanic.constants import HTTPMethod
from sanic.exceptions import (
BadRequest,
ExpectationFailed,
FileNotFound,
Forbidden,
HeaderNotFound,
InternalServerError,
InvalidHeader,
MethodNotAllowed,
NotFound,
RangeNotSatisfiable,
SanicException,
ServerError,
ServiceUnavailable,
Unauthorized,
)
from sanic.request import Request from sanic.request import Request
from sanic.response import ( from sanic.response import (
HTTPResponse, HTTPResponse,
@ -9,6 +25,7 @@ from sanic.response import (
file, file,
html, html,
json, json,
raw,
redirect, redirect,
text, text,
) )
@ -17,16 +34,34 @@ from sanic.server.websockets.impl import WebsocketImplProtocol as Websocket
__all__ = ( __all__ = (
"__version__", "__version__",
# Common objects
"Sanic", "Sanic",
"Blueprint", "Blueprint",
"HTTPMethod", "HTTPMethod",
"HTTPResponse", "HTTPResponse",
"Request", "Request",
"Websocket", "Websocket",
# Common exceptions
"BadRequest",
"ExpectationFailed",
"FileNotFound",
"Forbidden",
"HeaderNotFound",
"InternalServerError",
"InvalidHeader",
"MethodNotAllowed",
"NotFound",
"RangeNotSatisfiable",
"SanicException",
"ServerError",
"ServiceUnavailable",
"Unauthorized",
# Common response methods
"empty", "empty",
"file", "file",
"html", "html",
"json", "json",
"raw",
"redirect", "redirect",
"text", "text",
) )

View File

@ -16,7 +16,7 @@ from asyncio import (
) )
from asyncio.futures import Future from asyncio.futures import Future
from collections import defaultdict, deque from collections import defaultdict, deque
from contextlib import suppress from contextlib import contextmanager, suppress
from functools import partial from functools import partial
from inspect import isawaitable from inspect import isawaitable
from os import environ from os import environ
@ -33,6 +33,7 @@ from typing import (
Deque, Deque,
Dict, Dict,
Iterable, Iterable,
Iterator,
List, List,
Optional, Optional,
Set, Set,
@ -91,6 +92,7 @@ from sanic.signals import Signal, SignalRouter
from sanic.touchup import TouchUp, TouchUpMeta from sanic.touchup import TouchUp, TouchUpMeta
from sanic.types.shared_ctx import SharedContext from sanic.types.shared_ctx import SharedContext
from sanic.worker.inspector import Inspector from sanic.worker.inspector import Inspector
from sanic.worker.loader import CertLoader
from sanic.worker.manager import WorkerManager from sanic.worker.manager import WorkerManager
@ -138,6 +140,7 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta):
"_test_client", "_test_client",
"_test_manager", "_test_manager",
"blueprints", "blueprints",
"certloader_class",
"config", "config",
"configure_logging", "configure_logging",
"ctx", "ctx",
@ -180,6 +183,7 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta):
loads: Optional[Callable[..., Any]] = None, loads: Optional[Callable[..., Any]] = None,
inspector: bool = False, inspector: bool = False,
inspector_class: Optional[Type[Inspector]] = None, inspector_class: Optional[Type[Inspector]] = None,
certloader_class: Optional[Type[CertLoader]] = None,
) -> None: ) -> None:
super().__init__(name=name) super().__init__(name=name)
# logging # logging
@ -214,6 +218,9 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta):
self.asgi = False self.asgi = False
self.auto_reload = False self.auto_reload = False
self.blueprints: Dict[str, Blueprint] = {} self.blueprints: Dict[str, Blueprint] = {}
self.certloader_class: Type[CertLoader] = (
certloader_class or CertLoader
)
self.configure_logging: bool = configure_logging self.configure_logging: bool = configure_logging
self.ctx: Any = ctx or SimpleNamespace() self.ctx: Any = ctx or SimpleNamespace()
self.error_handler: ErrorHandler = error_handler or ErrorHandler() self.error_handler: ErrorHandler = error_handler or ErrorHandler()
@ -436,14 +443,15 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta):
ctx = params.pop("route_context") ctx = params.pop("route_context")
routes = self.router.add(**params) with self.amend():
if isinstance(routes, Route): routes = self.router.add(**params)
routes = [routes] if isinstance(routes, Route):
routes = [routes]
for r in routes: for r in routes:
r.extra.websocket = websocket r.extra.websocket = websocket
r.extra.static = params.get("static", False) r.extra.static = params.get("static", False)
r.ctx.__dict__.update(ctx) r.ctx.__dict__.update(ctx)
return routes return routes
@ -452,17 +460,19 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta):
middleware: FutureMiddleware, middleware: FutureMiddleware,
route_names: Optional[List[str]] = None, route_names: Optional[List[str]] = None,
): ):
if route_names: with self.amend():
return self.register_named_middleware( if route_names:
middleware.middleware, route_names, middleware.attach_to return self.register_named_middleware(
) middleware.middleware, route_names, middleware.attach_to
else: )
return self.register_middleware( else:
middleware.middleware, middleware.attach_to return self.register_middleware(
) middleware.middleware, middleware.attach_to
)
def _apply_signal(self, signal: FutureSignal) -> Signal: def _apply_signal(self, signal: FutureSignal) -> Signal:
return self.signal_router.add(*signal) with self.amend():
return self.signal_router.add(*signal)
def dispatch( def dispatch(
self, self,
@ -1523,6 +1533,27 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta):
# Lifecycle # Lifecycle
# -------------------------------------------------------------------- # # -------------------------------------------------------------------- #
@contextmanager
def amend(self) -> Iterator[None]:
"""
If the application has started, this function allows changes
to be made to add routes, middleware, and signals.
"""
if not self.state.is_started:
yield
else:
do_router = self.router.finalized
do_signal_router = self.signal_router.finalized
if do_router:
self.router.reset()
if do_signal_router:
self.signal_router.reset()
yield
if do_signal_router:
self.signalize(self.config.TOUCHUP)
if do_router:
self.finalize()
def finalize(self): def finalize(self):
try: try:
self.router.finalize() self.router.finalize()

View File

@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, Optional
from urllib.parse import quote from urllib.parse import quote
from sanic.compat import Header from sanic.compat import Header
from sanic.exceptions import ServerError from sanic.exceptions import BadRequest, ServerError
from sanic.helpers import Default from sanic.helpers import Default
from sanic.http import Stage from sanic.http import Stage
from sanic.log import error_logger, logger from sanic.log import error_logger, logger
@ -132,12 +132,20 @@ class ASGIApp:
instance.sanic_app.state.is_started = True instance.sanic_app.state.is_started = True
setattr(instance.transport, "add_task", sanic_app.loop.create_task) setattr(instance.transport, "add_task", sanic_app.loop.create_task)
headers = Header( try:
[ headers = Header(
(key.decode("latin-1"), value.decode("latin-1")) [
for key, value in scope.get("headers", []) (
] key.decode("ASCII"),
) value.decode(errors="surrogateescape"),
)
for key, value in scope.get("headers", [])
]
)
except UnicodeDecodeError:
raise BadRequest(
"Header names can only contain US-ASCII characters"
)
path = ( path = (
scope["path"][1:] scope["path"][1:]
if scope["path"].startswith("/") if scope["path"].startswith("/")

View File

@ -1,5 +1,6 @@
from asyncio import CancelledError from asyncio import CancelledError, Protocol
from typing import Any, Dict, Optional, Union from os import PathLike
from typing import Any, Dict, Optional, Sequence, Union
from sanic.helpers import STATUS_CODES from sanic.helpers import STATUS_CODES
@ -9,51 +10,158 @@ class RequestCancelled(CancelledError):
class ServerKilled(Exception): class ServerKilled(Exception):
... """
Exception Sanic server uses when killing a server process for something
unexpected happening.
"""
class SanicException(Exception): class SanicException(Exception):
"""
Generic exception that will generate an HTTP response when raised
in the context of a request lifecycle.
Usually it is best practice to use one of the more specific exceptions
than this generic. Even when trying to raise a 500, it is generally
preferrable to use :class:`.ServerError`
.. code-block:: python
raise SanicException(
"Something went wrong",
status_code=999,
context={
"info": "Some additional details",
},
headers={
"X-Foo": "bar"
}
)
:param message: The message to be sent to the client. If ``None``
then the appropriate HTTP response status message will be used
instead, defaults to None
:type message: Optional[Union[str, bytes]], optional
:param status_code: The HTTP response code to send, if applicable. If
``None``, then it will be 500, defaults to None
:type status_code: Optional[int], optional
:param quiet: When ``True``, the error traceback will be suppressed
from the logs, defaults to None
:type quiet: Optional[bool], optional
:param context: Additional mapping of key/value data that will be
sent to the client upon exception, defaults to None
:type context: Optional[Dict[str, Any]], optional
:param extra: Additional mapping of key/value data that will NOT be
sent to the client when in PRODUCTION mode, defaults to None
:type extra: Optional[Dict[str, Any]], optional
:param headers: Additional headers that should be sent with the HTTP
response, defaults to None
:type headers: Optional[Dict[str, Any]], optional
"""
status_code: int = 500
quiet: Optional[bool] = False
headers: Dict[str, str] = {}
message: str = "" message: str = ""
def __init__( def __init__(
self, self,
message: Optional[Union[str, bytes]] = None, message: Optional[Union[str, bytes]] = None,
status_code: Optional[int] = None, status_code: Optional[int] = None,
*,
quiet: Optional[bool] = None, quiet: Optional[bool] = None,
context: Optional[Dict[str, Any]] = None, context: Optional[Dict[str, Any]] = None,
extra: Optional[Dict[str, Any]] = None, extra: Optional[Dict[str, Any]] = None,
headers: Optional[Dict[str, Any]] = None,
) -> None: ) -> None:
self.context = context self.context = context
self.extra = extra self.extra = extra
status_code = status_code or getattr(
self.__class__, "status_code", None
)
quiet = quiet or getattr(self.__class__, "quiet", None)
headers = headers or getattr(self.__class__, "headers", {})
if message is None: if message is None:
if self.message: if self.message:
message = self.message message = self.message
elif status_code is not None: elif status_code:
msg: bytes = STATUS_CODES.get(status_code, b"") msg: bytes = STATUS_CODES.get(status_code, b"")
message = msg.decode("utf8") message = msg.decode("utf8")
super().__init__(message) super().__init__(message)
if status_code is not None: self.status_code = status_code
self.status_code = status_code self.quiet = quiet
self.headers = headers
# quiet=None/False/True with None meaning choose by status
if quiet or quiet is None and status_code not in (None, 500):
self.quiet = True
class NotFound(SanicException): class HTTPException(SanicException):
"""
A base class for other exceptions and should not be called directly.
"""
def __init__(
self,
message: Optional[Union[str, bytes]] = None,
*,
quiet: Optional[bool] = None,
context: Optional[Dict[str, Any]] = None,
extra: Optional[Dict[str, Any]] = None,
headers: Optional[Dict[str, Any]] = None,
) -> None:
super().__init__(
message,
quiet=quiet,
context=context,
extra=extra,
headers=headers,
)
class NotFound(HTTPException):
""" """
**Status**: 404 Not Found **Status**: 404 Not Found
:param message: The message to be sent to the client. If ``None``
then the HTTP status 'Not Found' will be sent, defaults to None
:type message: Optional[Union[str, bytes]], optional
:param quiet: When ``True``, the error traceback will be suppressed
from the logs, defaults to None
:type quiet: Optional[bool], optional
:param context: Additional mapping of key/value data that will be
sent to the client upon exception, defaults to None
:type context: Optional[Dict[str, Any]], optional
:param extra: Additional mapping of key/value data that will NOT be
sent to the client when in PRODUCTION mode, defaults to None
:type extra: Optional[Dict[str, Any]], optional
:param headers: Additional headers that should be sent with the HTTP
response, defaults to None
:type headers: Optional[Dict[str, Any]], optional
""" """
status_code = 404 status_code = 404
quiet = True quiet = True
class BadRequest(SanicException): class BadRequest(HTTPException):
""" """
**Status**: 400 Bad Request **Status**: 400 Bad Request
:param message: The message to be sent to the client. If ``None``
then the HTTP status 'Bad Request' will be sent, defaults to None
:type message: Optional[Union[str, bytes]], optional
:param quiet: When ``True``, the error traceback will be suppressed
from the logs, defaults to None
:type quiet: Optional[bool], optional
:param context: Additional mapping of key/value data that will be
sent to the client upon exception, defaults to None
:type context: Optional[Dict[str, Any]], optional
:param extra: Additional mapping of key/value data that will NOT be
sent to the client when in PRODUCTION mode, defaults to None
:type extra: Optional[Dict[str, Any]], optional
:param headers: Additional headers that should be sent with the HTTP
response, defaults to None
:type headers: Optional[Dict[str, Any]], optional
""" """
status_code = 400 status_code = 400
@ -61,51 +169,133 @@ class BadRequest(SanicException):
InvalidUsage = BadRequest InvalidUsage = BadRequest
BadURL = BadRequest
class BadURL(BadRequest): class MethodNotAllowed(HTTPException):
...
class MethodNotAllowed(SanicException):
""" """
**Status**: 405 Method Not Allowed **Status**: 405 Method Not Allowed
:param message: The message to be sent to the client. If ``None``
then the HTTP status 'Method Not Allowed' will be sent,
defaults to None
:type message: Optional[Union[str, bytes]], optional
:param method: The HTTP method that was used, defaults to an empty string
:type method: Optional[str], optional
:param allowed_methods: The HTTP methods that can be used instead of the
one that was attempted
:type allowed_methods: Optional[Sequence[str]], optional
:param quiet: When ``True``, the error traceback will be suppressed
from the logs, defaults to None
:type quiet: Optional[bool], optional
:param context: Additional mapping of key/value data that will be
sent to the client upon exception, defaults to None
:type context: Optional[Dict[str, Any]], optional
:param extra: Additional mapping of key/value data that will NOT be
sent to the client when in PRODUCTION mode, defaults to None
:type extra: Optional[Dict[str, Any]], optional
:param headers: Additional headers that should be sent with the HTTP
response, defaults to None
:type headers: Optional[Dict[str, Any]], optional
""" """
status_code = 405 status_code = 405
quiet = True quiet = True
def __init__(self, message, method, allowed_methods): def __init__(
super().__init__(message) self,
self.headers = {"Allow": ", ".join(allowed_methods)} message: Optional[Union[str, bytes]] = None,
method: str = "",
allowed_methods: Optional[Sequence[str]] = None,
*,
quiet: Optional[bool] = None,
context: Optional[Dict[str, Any]] = None,
extra: Optional[Dict[str, Any]] = None,
headers: Optional[Dict[str, Any]] = None,
):
super().__init__(
message,
quiet=quiet,
context=context,
extra=extra,
headers=headers,
)
if allowed_methods:
self.headers = {
**self.headers,
"Allow": ", ".join(allowed_methods),
}
self.method = method
self.allowed_methods = allowed_methods
MethodNotSupported = MethodNotAllowed MethodNotSupported = MethodNotAllowed
class ServerError(SanicException): class ServerError(HTTPException):
""" """
**Status**: 500 Internal Server Error **Status**: 500 Internal Server Error
A general server-side error has occurred. If no other HTTP exception is
appropriate, then this should be used
:param message: The message to be sent to the client. If ``None``
then the HTTP status 'Internal Server Error' will be sent,
defaults to None
:type message: Optional[Union[str, bytes]], optional
:param quiet: When ``True``, the error traceback will be suppressed
from the logs, defaults to None
:type quiet: Optional[bool], optional
:param context: Additional mapping of key/value data that will be
sent to the client upon exception, defaults to None
:type context: Optional[Dict[str, Any]], optional
:param extra: Additional mapping of key/value data that will NOT be
sent to the client when in PRODUCTION mode, defaults to None
:type extra: Optional[Dict[str, Any]], optional
:param headers: Additional headers that should be sent with the HTTP
response, defaults to None
:type headers: Optional[Dict[str, Any]], optional
""" """
status_code = 500 status_code = 500
class ServiceUnavailable(SanicException): InternalServerError = ServerError
class ServiceUnavailable(HTTPException):
""" """
**Status**: 503 Service Unavailable **Status**: 503 Service Unavailable
The server is currently unavailable (because it is overloaded or The server is currently unavailable (because it is overloaded or
down for maintenance). Generally, this is a temporary state. down for maintenance). Generally, this is a temporary state.
:param message: The message to be sent to the client. If ``None``
then the HTTP status 'Bad Request' will be sent, defaults to None
:type message: Optional[Union[str, bytes]], optional
:param quiet: When ``True``, the error traceback will be suppressed
from the logs, defaults to None
:type quiet: Optional[bool], optional
:param context: Additional mapping of key/value data that will be
sent to the client upon exception, defaults to None
:type context: Optional[Dict[str, Any]], optional
:param extra: Additional mapping of key/value data that will NOT be
sent to the client when in PRODUCTION mode, defaults to None
:type extra: Optional[Dict[str, Any]], optional
:param headers: Additional headers that should be sent with the HTTP
response, defaults to None
:type headers: Optional[Dict[str, Any]], optional
""" """
status_code = 503 status_code = 503
quiet = True quiet = True
class URLBuildError(ServerError): class URLBuildError(HTTPException):
""" """
**Status**: 500 Internal Server Error **Status**: 500 Internal Server Error
An exception used by Sanic internals when unable to build a URL.
""" """
status_code = 500 status_code = 500
@ -114,30 +304,77 @@ class URLBuildError(ServerError):
class FileNotFound(NotFound): class FileNotFound(NotFound):
""" """
**Status**: 404 Not Found **Status**: 404 Not Found
A specific form of :class:`.NotFound` that is specifically when looking
for a file on the file system at a known path.
:param message: The message to be sent to the client. If ``None``
then the HTTP status 'Not Found' will be sent, defaults to None
:type message: Optional[Union[str, bytes]], optional
:param path: The path, if any, to the file that could not
be found, defaults to None
:type path: Optional[PathLike], optional
:param relative_url: A relative URL of the file, defaults to None
:type relative_url: Optional[str], optional
:param quiet: When ``True``, the error traceback will be suppressed
from the logs, defaults to None
:type quiet: Optional[bool], optional
:param context: Additional mapping of key/value data that will be
sent to the client upon exception, defaults to None
:type context: Optional[Dict[str, Any]], optional
:param extra: Additional mapping of key/value data that will NOT be
sent to the client when in PRODUCTION mode, defaults to None
:type extra: Optional[Dict[str, Any]], optional
:param headers: Additional headers that should be sent with the HTTP
response, defaults to None
:type headers: Optional[Dict[str, Any]], optional
""" """
def __init__(self, message, path, relative_url): def __init__(
super().__init__(message) self,
message: Optional[Union[str, bytes]] = None,
path: Optional[PathLike] = None,
relative_url: Optional[str] = None,
*,
quiet: Optional[bool] = None,
context: Optional[Dict[str, Any]] = None,
extra: Optional[Dict[str, Any]] = None,
headers: Optional[Dict[str, Any]] = None,
):
super().__init__(
message,
quiet=quiet,
context=context,
extra=extra,
headers=headers,
)
self.path = path self.path = path
self.relative_url = relative_url self.relative_url = relative_url
class RequestTimeout(SanicException): class RequestTimeout(HTTPException):
"""The Web server (running the Web site) thinks that there has been too """
The Web server (running the Web site) thinks that there has been too
long an interval of time between 1) the establishment of an IP long an interval of time between 1) the establishment of an IP
connection (socket) between the client and the server and connection (socket) between the client and the server and
2) the receipt of any data on that socket, so the server has dropped 2) the receipt of any data on that socket, so the server has dropped
the connection. The socket connection has actually been lost - the Web the connection. The socket connection has actually been lost - the Web
server has 'timed out' on that particular socket connection. server has 'timed out' on that particular socket connection.
This is an internal exception thrown by Sanic and should not be used
directly.
""" """
status_code = 408 status_code = 408
quiet = True quiet = True
class PayloadTooLarge(SanicException): class PayloadTooLarge(HTTPException):
""" """
**Status**: 413 Payload Too Large **Status**: 413 Payload Too Large
This is an internal exception thrown by Sanic and should not be used
directly.
""" """
status_code = 413 status_code = 413
@ -147,34 +384,126 @@ class PayloadTooLarge(SanicException):
class HeaderNotFound(BadRequest): class HeaderNotFound(BadRequest):
""" """
**Status**: 400 Bad Request **Status**: 400 Bad Request
:param message: The message to be sent to the client. If ``None``
then the HTTP status 'Bad Request' will be sent, defaults to None
:type message: Optional[Union[str, bytes]], optional
:param quiet: When ``True``, the error traceback will be suppressed
from the logs, defaults to None
:type quiet: Optional[bool], optional
:param context: Additional mapping of key/value data that will be
sent to the client upon exception, defaults to None
:type context: Optional[Dict[str, Any]], optional
:param extra: Additional mapping of key/value data that will NOT be
sent to the client when in PRODUCTION mode, defaults to None
:type extra: Optional[Dict[str, Any]], optional
:param headers: Additional headers that should be sent with the HTTP
response, defaults to None
:type headers: Optional[Dict[str, Any]], optional
""" """
class InvalidHeader(BadRequest): class InvalidHeader(BadRequest):
""" """
**Status**: 400 Bad Request **Status**: 400 Bad Request
:param message: The message to be sent to the client. If ``None``
then the HTTP status 'Bad Request' will be sent, defaults to None
:type message: Optional[Union[str, bytes]], optional
:param quiet: When ``True``, the error traceback will be suppressed
from the logs, defaults to None
:type quiet: Optional[bool], optional
:param context: Additional mapping of key/value data that will be
sent to the client upon exception, defaults to None
:type context: Optional[Dict[str, Any]], optional
:param extra: Additional mapping of key/value data that will NOT be
sent to the client when in PRODUCTION mode, defaults to None
:type extra: Optional[Dict[str, Any]], optional
:param headers: Additional headers that should be sent with the HTTP
response, defaults to None
:type headers: Optional[Dict[str, Any]], optional
""" """
class RangeNotSatisfiable(SanicException): class ContentRange(Protocol):
total: int
class RangeNotSatisfiable(HTTPException):
""" """
**Status**: 416 Range Not Satisfiable **Status**: 416 Range Not Satisfiable
:param message: The message to be sent to the client. If ``None``
then the HTTP status 'Range Not Satisfiable' will be sent,
defaults to None
:type message: Optional[Union[str, bytes]], optional
:param content_range: An object meeting the :class:`.ContentRange` protocol
that has a ``total`` property, defaults to None
:type content_range: Optional[ContentRange], optional
:param quiet: When ``True``, the error traceback will be suppressed
from the logs, defaults to None
:type quiet: Optional[bool], optional
:param context: Additional mapping of key/value data that will be
sent to the client upon exception, defaults to None
:type context: Optional[Dict[str, Any]], optional
:param extra: Additional mapping of key/value data that will NOT be
sent to the client when in PRODUCTION mode, defaults to None
:type extra: Optional[Dict[str, Any]], optional
:param headers: Additional headers that should be sent with the HTTP
response, defaults to None
:type headers: Optional[Dict[str, Any]], optional
""" """
status_code = 416 status_code = 416
quiet = True quiet = True
def __init__(self, message, content_range): def __init__(
super().__init__(message) self,
self.headers = {"Content-Range": f"bytes */{content_range.total}"} message: Optional[Union[str, bytes]] = None,
content_range: Optional[ContentRange] = None,
*,
quiet: Optional[bool] = None,
context: Optional[Dict[str, Any]] = None,
extra: Optional[Dict[str, Any]] = None,
headers: Optional[Dict[str, Any]] = None,
):
super().__init__(
message,
quiet=quiet,
context=context,
extra=extra,
headers=headers,
)
if content_range is not None:
self.headers = {
**self.headers,
"Content-Range": f"bytes */{content_range.total}",
}
ContentRangeError = RangeNotSatisfiable ContentRangeError = RangeNotSatisfiable
class ExpectationFailed(SanicException): class ExpectationFailed(HTTPException):
""" """
**Status**: 417 Expectation Failed **Status**: 417 Expectation Failed
:param message: The message to be sent to the client. If ``None``
then the HTTP status 'Expectation Failed' will be sent,
defaults to None
:type message: Optional[Union[str, bytes]], optional
:param quiet: When ``True``, the error traceback will be suppressed
from the logs, defaults to None
:type quiet: Optional[bool], optional
:param context: Additional mapping of key/value data that will be
sent to the client upon exception, defaults to None
:type context: Optional[Dict[str, Any]], optional
:param extra: Additional mapping of key/value data that will NOT be
sent to the client when in PRODUCTION mode, defaults to None
:type extra: Optional[Dict[str, Any]], optional
:param headers: Additional headers that should be sent with the HTTP
response, defaults to None
:type headers: Optional[Dict[str, Any]], optional
""" """
status_code = 417 status_code = 417
@ -184,9 +513,25 @@ class ExpectationFailed(SanicException):
HeaderExpectationFailed = ExpectationFailed HeaderExpectationFailed = ExpectationFailed
class Forbidden(SanicException): class Forbidden(HTTPException):
""" """
**Status**: 403 Forbidden **Status**: 403 Forbidden
:param message: The message to be sent to the client. If ``None``
then the HTTP status 'Forbidden' will be sent, defaults to None
:type message: Optional[Union[str, bytes]], optional
:param quiet: When ``True``, the error traceback will be suppressed
from the logs, defaults to None
:type quiet: Optional[bool], optional
:param context: Additional mapping of key/value data that will be
sent to the client upon exception, defaults to None
:type context: Optional[Dict[str, Any]], optional
:param extra: Additional mapping of key/value data that will NOT be
sent to the client when in PRODUCTION mode, defaults to None
:type extra: Optional[Dict[str, Any]], optional
:param headers: Additional headers that should be sent with the HTTP
response, defaults to None
:type headers: Optional[Dict[str, Any]], optional
""" """
status_code = 403 status_code = 403
@ -202,20 +547,33 @@ class InvalidRangeType(RangeNotSatisfiable):
quiet = True quiet = True
class PyFileError(Exception): class PyFileError(SanicException):
def __init__(self, file): def __init__(
super().__init__("could not execute config file %s", file) self,
file,
status_code: Optional[int] = None,
*,
quiet: Optional[bool] = None,
context: Optional[Dict[str, Any]] = None,
extra: Optional[Dict[str, Any]] = None,
headers: Optional[Dict[str, Any]] = None,
):
super().__init__(
"could not execute config file %s" % file,
status_code=status_code,
quiet=quiet,
context=context,
extra=extra,
headers=headers,
)
class Unauthorized(SanicException): class Unauthorized(HTTPException):
""" """
**Status**: 401 Unauthorized **Status**: 401 Unauthorized
:param message: Message describing the exception. When present, additional keyword arguments may be used to complete
:param status_code: HTTP Status code. the WWW-Authentication header.
:param scheme: Name of the authentication scheme to be used.
When present, kwargs is used to complete the WWW-Authentication header.
Examples:: Examples::
@ -240,21 +598,58 @@ class Unauthorized(SanicException):
raise Unauthorized("Auth required.", raise Unauthorized("Auth required.",
scheme="Bearer", scheme="Bearer",
realm="Restricted Area") realm="Restricted Area")
:param message: The message to be sent to the client. If ``None``
then the HTTP status 'Bad Request' will be sent, defaults to None
:type message: Optional[Union[str, bytes]], optional
:param scheme: Name of the authentication scheme to be used.
:type scheme: Optional[str], optional
:param quiet: When ``True``, the error traceback will be suppressed
from the logs, defaults to None
:type quiet: Optional[bool], optional
:param context: Additional mapping of key/value data that will be
sent to the client upon exception, defaults to None
:type context: Optional[Dict[str, Any]], optional
:param extra: Additional mapping of key/value data that will NOT be
sent to the client when in PRODUCTION mode, defaults to None
:type extra: Optional[Dict[str, Any]], optional
:param headers: Additional headers that should be sent with the HTTP
response, defaults to None
:type headers: Optional[Dict[str, Any]], optional
""" """
status_code = 401 status_code = 401
quiet = True quiet = True
def __init__(self, message, status_code=None, scheme=None, **kwargs): def __init__(
super().__init__(message, status_code) self,
message: Optional[Union[str, bytes]] = None,
scheme: Optional[str] = None,
*,
quiet: Optional[bool] = None,
context: Optional[Dict[str, Any]] = None,
extra: Optional[Dict[str, Any]] = None,
headers: Optional[Dict[str, Any]] = None,
**challenges,
):
super().__init__(
message,
quiet=quiet,
context=context,
extra=extra,
headers=headers,
)
# if auth-scheme is specified, set "WWW-Authenticate" header # if auth-scheme is specified, set "WWW-Authenticate" header
if scheme is not None: if scheme is not None:
values = ['{!s}="{!s}"'.format(k, v) for k, v in kwargs.items()] values = [
'{!s}="{!s}"'.format(k, v) for k, v in challenges.items()
]
challenge = ", ".join(values) challenge = ", ".join(values)
self.headers = { self.headers = {
"WWW-Authenticate": f"{scheme} {challenge}".rstrip() **self.headers,
"WWW-Authenticate": f"{scheme} {challenge}".rstrip(),
} }

View File

@ -75,4 +75,4 @@ class ContentRangeHandler:
} }
def __bool__(self): def __bool__(self):
return self.size > 0 return hasattr(self, "size") and self.size > 0

View File

@ -428,7 +428,9 @@ class Http(Stream, metaclass=TouchUpMeta):
if self.request is None: if self.request is None:
self.create_empty_request() self.create_empty_request()
request_middleware = not isinstance(exception, ServiceUnavailable) request_middleware = not isinstance(
exception, (ServiceUnavailable, RequestCancelled)
)
try: try:
await app.handle_exception( await app.handle_exception(
self.request, exception, request_middleware self.request, exception, request_middleware

View File

@ -159,7 +159,7 @@ class CertSimple(SanicSSLContext):
# try common aliases, rename to cert/key # try common aliases, rename to cert/key
certfile = kw["cert"] = kw.pop("certificate", None) or cert certfile = kw["cert"] = kw.pop("certificate", None) or cert
keyfile = kw["key"] = kw.pop("keyfile", None) or key keyfile = kw["key"] = kw.pop("keyfile", None) or key
password = kw.pop("password", None) password = kw.get("password", None)
if not certfile or not keyfile: if not certfile or not keyfile:
raise ValueError("SSL dict needs filenames for cert and key.") raise ValueError("SSL dict needs filenames for cert and key.")
subject = {} subject = {}

View File

@ -62,13 +62,13 @@ LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict( # no cov
}, },
formatters={ formatters={
"generic": { "generic": {
"format": "%(asctime)s [%(process)d] [%(levelname)s] %(message)s", "format": "%(asctime)s [%(process)s] [%(levelname)s] %(message)s",
"datefmt": "[%Y-%m-%d %H:%M:%S %z]", "datefmt": "[%Y-%m-%d %H:%M:%S %z]",
"class": "logging.Formatter", "class": "logging.Formatter",
}, },
"access": { "access": {
"format": "%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: " "format": "%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: "
+ "%(request)s %(message)s %(status)d %(byte)d", + "%(request)s %(message)s %(status)s %(byte)s",
"datefmt": "[%Y-%m-%d %H:%M:%S %z]", "datefmt": "[%Y-%m-%d %H:%M:%S %z]",
"class": "logging.Formatter", "class": "logging.Formatter",
}, },

View File

@ -811,7 +811,7 @@ class StartupMixin(metaclass=SanicMeta):
ssl = kwargs.get("ssl") ssl = kwargs.get("ssl")
if isinstance(ssl, SanicSSLContext): if isinstance(ssl, SanicSSLContext):
kwargs["ssl"] = kwargs["ssl"].sanic kwargs["ssl"] = ssl.sanic
manager = WorkerManager( manager = WorkerManager(
primary.state.workers, primary.state.workers,

View File

@ -148,7 +148,26 @@ async def validate_file(
last_modified = datetime.fromtimestamp( last_modified = datetime.fromtimestamp(
float(last_modified), tz=timezone.utc float(last_modified), tz=timezone.utc
).replace(microsecond=0) ).replace(microsecond=0)
if last_modified <= if_modified_since:
if (
last_modified.utcoffset() is None
and if_modified_since.utcoffset() is not None
):
logger.warning(
"Cannot compare tz-aware and tz-naive datetimes. To avoid "
"this conflict Sanic is converting last_modified to UTC."
)
last_modified.replace(tzinfo=timezone.utc)
elif (
last_modified.utcoffset() is not None
and if_modified_since.utcoffset() is None
):
logger.warning(
"Cannot compare tz-aware and tz-naive datetimes. To avoid "
"this conflict Sanic is converting if_modified_since to UTC."
)
if_modified_since.replace(tzinfo=timezone.utc)
if last_modified.timestamp() <= if_modified_since.timestamp():
return HTTPResponse(status=304) return HTTPResponse(status=304)

View File

@ -10,7 +10,7 @@ except ImportError: # websockets >= 11.0
from websockets.typing import Subprotocol from websockets.typing import Subprotocol
from sanic.exceptions import ServerError from sanic.exceptions import SanicException
from sanic.log import logger from sanic.log import logger
from sanic.server import HttpProtocol from sanic.server import HttpProtocol
@ -123,7 +123,7 @@ class WebSocketProtocol(HttpProtocol):
"Failed to open a WebSocket connection.\n" "Failed to open a WebSocket connection.\n"
"See server log for more information.\n" "See server log for more information.\n"
) )
raise ServerError(msg, status_code=500) raise SanicException(msg, status_code=500)
if 100 <= resp.status_code <= 299: if 100 <= resp.status_code <= 299:
first_line = ( first_line = (
f"HTTP/1.1 {resp.status_code} {resp.reason_phrase}\r\n" f"HTTP/1.1 {resp.status_code} {resp.reason_phrase}\r\n"
@ -138,7 +138,7 @@ class WebSocketProtocol(HttpProtocol):
rbody += b"\r\n\r\n" rbody += b"\r\n\r\n"
await super().send(rbody) await super().send(rbody)
else: else:
raise ServerError(resp.body, resp.status_code) raise SanicException(resp.body, resp.status_code)
self.websocket = WebsocketImplProtocol( self.websocket = WebsocketImplProtocol(
ws_proto, ws_proto,
ping_interval=self.websocket_ping_interval, ping_interval=self.websocket_ping_interval,

View File

@ -5,6 +5,7 @@ import sys
from importlib import import_module from importlib import import_module
from pathlib import Path from pathlib import Path
from ssl import SSLContext
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Union, cast from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Union, cast
from sanic.http.tls.context import process_to_context from sanic.http.tls.context import process_to_context
@ -103,8 +104,16 @@ class CertLoader:
"trustme": TrustmeCreator, "trustme": TrustmeCreator,
} }
def __init__(self, ssl_data: Dict[str, Union[str, os.PathLike]]): def __init__(
self,
ssl_data: Optional[
Union[SSLContext, Dict[str, Union[str, os.PathLike]]]
],
):
self._ssl_data = ssl_data self._ssl_data = ssl_data
self._creator_class = None
if not ssl_data or not isinstance(ssl_data, dict):
return
creator_name = cast(str, ssl_data.get("creator")) creator_name = cast(str, ssl_data.get("creator"))

View File

@ -73,8 +73,8 @@ def worker_serve(
info.settings["app"] = a info.settings["app"] = a
a.state.server_info.append(info) a.state.server_info.append(info)
if isinstance(ssl, dict): if isinstance(ssl, dict) or app.certloader_class is not CertLoader:
cert_loader = CertLoader(ssl) cert_loader = app.certloader_class(ssl or {})
ssl = cert_loader.load(app) ssl = cert_loader.load(app)
for info in app.state.server_info: for info in app.state.server_info:
info.settings["ssl"] = ssl info.settings["ssl"] = ssl

View File

@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDCTCCAfGgAwIBAgIUa7OOlAGQfXOgUgRENJ9GbUgO7kwwDQYJKoZIhvcNAQEL
BQAwFDESMBAGA1UEAwwJMTI3LjAuMC4xMB4XDTIzMDMyMDA3MzE1M1oXDTIzMDQx
OTA3MzE1M1owFDESMBAGA1UEAwwJMTI3LjAuMC4xMIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEAn2/RqVpzO7GFrgVGiowR5CzcFzf1tSFti1K/WIGr/jsu
NP+1R3sim17pgg6SCOFnUMRS0KnDihkzoeP6z+0tFsrbCH4V1+fq0iud8WgYQrgD
3ttUcHrz04p7wsMoeqndUQoLbyJzP8MpA2XJsoacdIVkuLv2AESGXLhJym/e9HGN
g8bqdz25X0hVTczZW1FN9AZyWWVf9Go6jqC7LCaOnYXAnOkEy2/JHdkeNXYFZHB3
71UemfkCjfp0vlRV8pVpkBGMhRNFphBTfxdqeWiGQwVqrhaJO4M7DJlQHCAPY16P
o9ywnhLDhFHD7KIfTih9XxrdgTowqcwyGX3e3aJpTwIDAQABo1MwUTAdBgNVHQ4E
FgQU5NogMq6mRBeGl4i6hIuUlcR2bVEwHwYDVR0jBBgwFoAU5NogMq6mRBeGl4i6
hIuUlcR2bVEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAYW34
JY1kd0UO5HE41oxJD4PioQboXXX0al4RgKaUUsPykeHQbK0q0TSYAZLwRjooTVUO
Wvna5bU2mzyULqA2r/Cr/w4zb9xybO3SiHFHcU1RacouauHXROHwRm98i8A73xnH
vHws5BADr2ggnVcPNh4VOQ9ZvBlC7jhgpvMjqOEu5ZPCovhfZYfSsvBDHcD74ZYm
Di9DvqsJmrb23Dv3SUykm3W+Ql2q+JyjFj30rhD89CFwJ9iSlFwTYEwZLHA+mV6p
UKy3I3Fiht1Oc+nIivX5uhRSMbDVvDTVHbjjPujxxFjkiHXMjtwvwfg4Sb6du61q
AjBRFyXbNu4hZkkHOA==
-----END CERTIFICATE-----

View File

@ -0,0 +1,30 @@
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQI94UBqjaZlG4CAggA
MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBCvJhEy+3/+0Ec0gpd5dkP6BIIE
0E7rLplTe9rxK3sR9V0cx8Xn6V+uFhG3p7dzeMDCCKpGo9MEaacF5m+paGnBkMlH
Pz3rRoLA5jqzwXl4US/C5E1Or//2YBgF1XXKi3BPF/bVx/g6vR+xeobf9kQGbqQk
FNPYtP7mpg2dekp5BUsKSosIt8BkknWFvhBeNuGZT/zlMUuq1WpMe4KIh/W9IdNr
HolcuZJWBhQAwGPciWIZRyq48wKa++W7Jdg/aG8FviJQnjaAUv4CyZJHUJnaNwUx
iHOETpzIC+bhF2K+s4g5w68VCj6Jtz78sIBEZKzo7LI5QHdRHqYB5SJ/dGiV+h09
R/rQ/M+24mwHDlRSCxxq0yuDwUuGBlHyATeDCFeE3L5OX8yTLuqYJ6vUa6UbzMYA
8H4l5zfu9RrAhKYa9tD+4ONxMmHziIgmn5zvSXeBwJKfeUbnN4IKWLsSoSVspBRh
zLl51DMAnem4NEjLfIW8WYjhsvSYwd9BYqxXaAiv4Wjx9ZV1yLqFICC7tejpVdRT
afI0qMOfWu4ma6xVBg1ezLgF1wHIPrq6euTvWdnifYQopVICALlltEo5oxQ2i/OM
NY8RyovWujiGNsa3pId9HmZXiLyLXjKPstGWRK4liMyc2EiP099gTdBvrb+VQp+I
EyPavmh3WNhgZGOh3qah39X8HrBprc0PPfSPlxpaWdNMIIMSbcIWWdJEA/e4tcy/
uBaV4H3sNCtBApgrb6B9YUbS9CXNUburJo19T1sk2uCaO12qYfdu2IDEnFf8JiF3
i7nyftotRuoKq2D+V8d0PeMi/vJSo6+eZIn7VNe6ejYf+w0s7sxlpiKVzkslyOhq
n0T4M3ZkSwGIETzgkRRuTY1OK7slhglMgXlQ2FuIUUo6CRg9WjRJvI5rujLzLWfB
hkgP8STirjTV0DUWPFGtUcenvEcZPkYIQcoPHxOJGNW3ZPXNpt4RjbvPLeVzDm0O
WJiay/qhag/bXGqKraO3b6Y7FOzJa8kG4G0XrcFY1s2oCXRqRqYJAtwaEeVCjCSJ
Qy0OZkqcJEU7pv98pLMpG9OWz4Gle77g4KoQUJjQGtmg0MUMoPd0iPRmvkxsYg8E
Q9uZS3m6PpWmmYDY0Ik1w/4avs3skl2mW3dqcZGLEepkjiQSnFABsuvxKd+uIEQy
lyf9FrynXVcUI87LUkuniLRKwZZzFALVuc+BwtO3SA5mvEK22ZEq9QOysbwlpN54
G5xXJKJEeexUSjEUIij4J89RLsXldibhp7YYZ7rFviR6chIqC0V7G6VqAM9TOCrV
PWZXr3ZY5/pCZYs5DYKFJBFMSQ2UT/++VYxdZCeBH75vaxugbS8RdUM+iVDevWpQ
/AnP1FolNAgkVhi3Rw4L16SibkqpEzIi1svPWKMwXdvewA32UidLElhuTWWjI2Wm
veXhmEqwk/7ML4JMI7wHcDQdvSKen0mCL2J9tB7A/pewYyDE0ffIUmjxglOtw30f
ZOlQKhMaKJGXp00U2zsHA2NJRI/hThbJncsnZyvuLei0P42RrF+r64b/0gUH6IZ5
wPUttT815KSNoy+XXXum9YGDYYFoAL+6WVEkl6dgo+X0hcH7DDf5Nkewiq8UcJGh
/69vFIfp+JlpicXzZ+R42LO3T3luC907aFBywF3pmi//
-----END ENCRYPTED PRIVATE KEY-----

View File

@ -7,6 +7,9 @@ from unittest.mock import call
import pytest import pytest
import uvicorn import uvicorn
from httpx import Headers
from pytest import MonkeyPatch
from sanic import Sanic from sanic import Sanic
from sanic.application.state import Mode from sanic.application.state import Mode
from sanic.asgi import ASGIApp, Lifespan, MockTransport from sanic.asgi import ASGIApp, Lifespan, MockTransport
@ -626,3 +629,26 @@ async def test_error_on_lifespan_exception_stop(app: Sanic):
) )
] ]
) )
@pytest.mark.asyncio
async def test_asgi_headers_decoding(app: Sanic, monkeypatch: MonkeyPatch):
@app.get("/")
def handler(request: Request):
return text("")
headers_init = Headers.__init__
def mocked_headers_init(self, *args, **kwargs):
if "encoding" in kwargs:
kwargs.pop("encoding")
headers_init(self, encoding="utf-8", *args, **kwargs)
monkeypatch.setattr(Headers, "__init__", mocked_headers_init)
message = "Header names can only contain US-ASCII characters"
with pytest.raises(BadRequest, match=message):
_, response = await app.asgi_client.get("/", headers={"😂": "😅"})
_, response = await app.asgi_client.get("/", headers={"Test-Header": "😅"})
assert response.status_code == 200

54
tests/test_late_adds.py Normal file
View File

@ -0,0 +1,54 @@
import pytest
from sanic import Sanic, text
@pytest.fixture
def late_app(app: Sanic):
app.config.TOUCHUP = False
app.get("/")(lambda _: text(""))
return app
def test_late_route(late_app: Sanic):
@late_app.before_server_start
async def late(app: Sanic):
@app.get("/late")
def handler(_):
return text("late")
_, response = late_app.test_client.get("/late")
assert response.status_code == 200
assert response.text == "late"
def test_late_middleware(late_app: Sanic):
@late_app.get("/late")
def handler(request):
return text(request.ctx.late)
@late_app.before_server_start
async def late(app: Sanic):
@app.on_request
def handler(request):
request.ctx.late = "late"
_, response = late_app.test_client.get("/late")
assert response.status_code == 200
assert response.text == "late"
def test_late_signal(late_app: Sanic):
@late_app.get("/late")
def handler(request):
return text(request.ctx.late)
@late_app.before_server_start
async def late(app: Sanic):
@app.signal("http.lifecycle.request")
def handler(request):
request.ctx.late = "late"
_, response = late_app.test_client.get("/late")
assert response.status_code == 200
assert response.text == "late"

View File

@ -0,0 +1,55 @@
from datetime import datetime, timezone
from logging import INFO
import pytest
from sanic.compat import Header
from sanic.response.convenience import validate_file
@pytest.mark.parametrize(
"ifmod,lastmod,expected",
(
("Sat, 01 Apr 2023 00:00:00 GMT", 1672524000, None),
(
"Sat, 01 Apr 2023 00:00:00",
1672524000,
"converting if_modified_since",
),
(
"Sat, 01 Apr 2023 00:00:00 GMT",
datetime(2023, 1, 1, 0, 0, 0),
"converting last_modified",
),
(
"Sat, 01 Apr 2023 00:00:00",
datetime(2023, 1, 1, 0, 0, 0),
None,
),
(
"Sat, 01 Apr 2023 00:00:00 GMT",
datetime(2023, 1, 1, 0, 0, 0).replace(tzinfo=timezone.utc),
None,
),
(
"Sat, 01 Apr 2023 00:00:00",
datetime(2023, 1, 1, 0, 0, 0).replace(tzinfo=timezone.utc),
"converting if_modified_since",
),
),
)
@pytest.mark.asyncio
async def test_file_timestamp_validation(
lastmod, ifmod, expected, caplog: pytest.LogCaptureFixture
):
headers = Header([["If-Modified-Since", ifmod]])
with caplog.at_level(INFO):
response = await validate_file(headers, lastmod)
assert response.status == 304
records = caplog.records
if not expected:
assert len(records) == 0
else:
record = records[0]
assert expected in record.message

View File

@ -12,7 +12,7 @@ from urllib.parse import urlparse
import pytest import pytest
from sanic_testing.testing import HOST, PORT from sanic_testing.testing import HOST, PORT, SanicTestClient
import sanic.http.tls.creators import sanic.http.tls.creators
@ -29,16 +29,24 @@ from sanic.http.tls.creators import (
get_ssl_context, get_ssl_context,
) )
from sanic.response import text from sanic.response import text
from sanic.worker.loader import CertLoader
current_dir = os.path.dirname(os.path.realpath(__file__)) current_dir = os.path.dirname(os.path.realpath(__file__))
localhost_dir = os.path.join(current_dir, "certs/localhost") localhost_dir = os.path.join(current_dir, "certs/localhost")
password_dir = os.path.join(current_dir, "certs/password")
sanic_dir = os.path.join(current_dir, "certs/sanic.example") sanic_dir = os.path.join(current_dir, "certs/sanic.example")
invalid_dir = os.path.join(current_dir, "certs/invalid.nonexist") invalid_dir = os.path.join(current_dir, "certs/invalid.nonexist")
localhost_cert = os.path.join(localhost_dir, "fullchain.pem") localhost_cert = os.path.join(localhost_dir, "fullchain.pem")
localhost_key = os.path.join(localhost_dir, "privkey.pem") localhost_key = os.path.join(localhost_dir, "privkey.pem")
sanic_cert = os.path.join(sanic_dir, "fullchain.pem") sanic_cert = os.path.join(sanic_dir, "fullchain.pem")
sanic_key = os.path.join(sanic_dir, "privkey.pem") sanic_key = os.path.join(sanic_dir, "privkey.pem")
password_dict = {
"cert": os.path.join(password_dir, "fullchain.pem"),
"key": os.path.join(password_dir, "privkey.pem"),
"password": "password",
"names": ["localhost"],
}
@pytest.fixture @pytest.fixture
@ -420,6 +428,29 @@ def test_no_certs_on_list(app):
assert "No certificates" in str(excinfo.value) assert "No certificates" in str(excinfo.value)
def test_custom_cert_loader():
class MyCertLoader(CertLoader):
def load(self, app: Sanic):
self._ssl_data = {
"key": localhost_key,
"cert": localhost_cert,
}
return super().load(app)
app = Sanic("custom", certloader_class=MyCertLoader)
@app.get("/test")
async def handler(request):
return text("ssl test")
client = SanicTestClient(app, port=44556)
request, response = client.get("https://localhost:44556/test")
assert request.scheme == "https"
assert response.status_code == 200
assert response.text == "ssl test"
def test_logger_vhosts(caplog): def test_logger_vhosts(caplog):
app = Sanic(name="test_logger_vhosts") app = Sanic(name="test_logger_vhosts")
@ -677,3 +708,34 @@ def test_ssl_in_multiprocess_mode(app: Sanic, caplog):
logging.INFO, logging.INFO,
"Goin' Fast @ https://127.0.0.1:8000", "Goin' Fast @ https://127.0.0.1:8000",
) in caplog.record_tuples ) in caplog.record_tuples
@pytest.mark.skipif(
sys.platform not in ("linux", "darwin"),
reason="This test requires fork context",
)
def test_ssl_in_multiprocess_mode_password(
app: Sanic, caplog: pytest.LogCaptureFixture
):
event = Event()
@app.main_process_start
async def main_start(app: Sanic):
app.shared_ctx.event = event
@app.after_server_start
async def shutdown(app):
app.shared_ctx.event.set()
app.stop()
assert not event.is_set()
with use_context("fork"):
with caplog.at_level(logging.INFO):
app.run(ssl=password_dict)
assert event.is_set()
assert (
"sanic.root",
logging.INFO,
"Goin' Fast @ https://127.0.0.1:8000",
) in caplog.record_tuples