
* First attempt at new Websockets implementation based on websockets >= 9.0, with sans-i/o features. Requires more work. * Update sanic/websocket.py Co-authored-by: Adam Hopkins <adam@amhopkins.com> * Update sanic/websocket.py Co-authored-by: Adam Hopkins <adam@amhopkins.com> * Update sanic/websocket.py Co-authored-by: Adam Hopkins <adam@amhopkins.com> * wip, update websockets code to new Sans/IO API * Refactored new websockets impl into own modules Incorporated other suggestions made by team * Another round of work on the new websockets impl * Added websocket_timeout support (matching previous/legacy support) * Lots more comments * Incorporated suggested changes from previous round of review * Changed RuntimeError usage to ServerError * Changed SanicException usage to ServerError * Removed some redundant asserts * Change remaining asserts to ServerErrors * Fixed some timeout handling issues * Fixed websocket.close() handling, and made it more robust * Made auto_close task smarter and more error-resilient * Made fail_connection routine smarter and more error-resilient * Further new websockets impl fixes * Update compatibility with Websockets v10 * Track server connection state in a more precise way * Try to handle the shutdown process more gracefully * Add a new end_connection() helper, to use as an alterative to close() or fail_connection() * Kill the auto-close task and keepalive-timeout task when sanic is shutdown * Deprecate WEBSOCKET_READ_LIMIT and WEBSOCKET_WRITE_LIMIT configs, they are not used in this implementation. * Change a warning message to debug level Remove default values for deprecated websocket parameters * Fix flake8 errors * Fix a couple of missed failing tests * remove websocket bench from examples * Integrate suggestions from code reviews Use Optional[T] instead of union[T,None] Fix mypy type logic errors change "is not None" to truthy checks where appropriate change "is None" to falsy checks were appropriate Add more debug logging when debug mode is on Change to using sanic.logger for debug logging rather than error_logger. * Fix long line lengths of debug messages Add some new debug messages when websocket IO is paused and unpaused for flow control Fix websocket example to use app.static() * remove unused import in websocket example app * re-run isort after Flake8 fixes * Some fixes to the new Websockets impl Will throw WebsocketClosed exception instead of ServerException now when attempting to read or write to closed websocket, this makes it easier to catch The various ws.recv() methods now have the ability to raise CancelledError into your websocket handler Fix a niche close-socket negotiation bug Fix bug where http protocol thought the websocket never sent any response. Allow data to still send in some cases after websocket enters CLOSING state. Fix some badly formatted and badly placed comments * allow eof_received to send back data too, if the connection is in CLOSING state Co-authored-by: Adam Hopkins <adam@amhopkins.com> Co-authored-by: Adam Hopkins <admhpkns@gmail.com>
265 lines
6.4 KiB
Python
265 lines
6.4 KiB
Python
from typing import Optional, Union
|
|
|
|
from sanic.helpers import STATUS_CODES
|
|
|
|
|
|
class SanicException(Exception):
|
|
message: str = ""
|
|
|
|
def __init__(
|
|
self,
|
|
message: Optional[Union[str, bytes]] = None,
|
|
status_code: Optional[int] = None,
|
|
quiet: Optional[bool] = None,
|
|
) -> None:
|
|
if message is None:
|
|
if self.message:
|
|
message = self.message
|
|
elif status_code is not None:
|
|
msg: bytes = STATUS_CODES.get(status_code, b"")
|
|
message = msg.decode("utf8")
|
|
|
|
super().__init__(message)
|
|
|
|
if status_code is not None:
|
|
self.status_code = status_code
|
|
|
|
# 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):
|
|
"""
|
|
**Status**: 404 Not Found
|
|
"""
|
|
|
|
status_code = 404
|
|
quiet = True
|
|
|
|
|
|
class InvalidUsage(SanicException):
|
|
"""
|
|
**Status**: 400 Bad Request
|
|
"""
|
|
|
|
status_code = 400
|
|
quiet = True
|
|
|
|
|
|
class MethodNotSupported(SanicException):
|
|
"""
|
|
**Status**: 405 Method Not Allowed
|
|
"""
|
|
|
|
status_code = 405
|
|
quiet = True
|
|
|
|
def __init__(self, message, method, allowed_methods):
|
|
super().__init__(message)
|
|
self.headers = {"Allow": ", ".join(allowed_methods)}
|
|
|
|
|
|
class ServerError(SanicException):
|
|
"""
|
|
**Status**: 500 Internal Server Error
|
|
"""
|
|
|
|
status_code = 500
|
|
|
|
|
|
class ServiceUnavailable(SanicException):
|
|
"""
|
|
**Status**: 503 Service Unavailable
|
|
|
|
The server is currently unavailable (because it is overloaded or
|
|
down for maintenance). Generally, this is a temporary state.
|
|
"""
|
|
|
|
status_code = 503
|
|
quiet = True
|
|
|
|
|
|
class URLBuildError(ServerError):
|
|
"""
|
|
**Status**: 500 Internal Server Error
|
|
"""
|
|
|
|
status_code = 500
|
|
|
|
|
|
class FileNotFound(NotFound):
|
|
"""
|
|
**Status**: 404 Not Found
|
|
"""
|
|
|
|
def __init__(self, message, path, relative_url):
|
|
super().__init__(message)
|
|
self.path = path
|
|
self.relative_url = relative_url
|
|
|
|
|
|
class RequestTimeout(SanicException):
|
|
"""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
|
|
connection (socket) between the client and the server and
|
|
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
|
|
server has 'timed out' on that particular socket connection.
|
|
"""
|
|
|
|
status_code = 408
|
|
quiet = True
|
|
|
|
|
|
class PayloadTooLarge(SanicException):
|
|
"""
|
|
**Status**: 413 Payload Too Large
|
|
"""
|
|
|
|
status_code = 413
|
|
quiet = True
|
|
|
|
|
|
class HeaderNotFound(InvalidUsage):
|
|
"""
|
|
**Status**: 400 Bad Request
|
|
"""
|
|
|
|
|
|
class InvalidHeader(InvalidUsage):
|
|
"""
|
|
**Status**: 400 Bad Request
|
|
"""
|
|
|
|
|
|
class ContentRangeError(SanicException):
|
|
"""
|
|
**Status**: 416 Range Not Satisfiable
|
|
"""
|
|
|
|
status_code = 416
|
|
quiet = True
|
|
|
|
def __init__(self, message, content_range):
|
|
super().__init__(message)
|
|
self.headers = {"Content-Range": f"bytes */{content_range.total}"}
|
|
|
|
|
|
class HeaderExpectationFailed(SanicException):
|
|
"""
|
|
**Status**: 417 Expectation Failed
|
|
"""
|
|
|
|
status_code = 417
|
|
quiet = True
|
|
|
|
|
|
class Forbidden(SanicException):
|
|
"""
|
|
**Status**: 403 Forbidden
|
|
"""
|
|
|
|
status_code = 403
|
|
quiet = True
|
|
|
|
|
|
class InvalidRangeType(ContentRangeError):
|
|
"""
|
|
**Status**: 416 Range Not Satisfiable
|
|
"""
|
|
|
|
status_code = 416
|
|
quiet = True
|
|
|
|
|
|
class PyFileError(Exception):
|
|
def __init__(self, file):
|
|
super().__init__("could not execute config file %s", file)
|
|
|
|
|
|
class Unauthorized(SanicException):
|
|
"""
|
|
**Status**: 401 Unauthorized
|
|
|
|
:param message: Message describing the exception.
|
|
:param status_code: HTTP Status code.
|
|
:param scheme: Name of the authentication scheme to be used.
|
|
|
|
When present, kwargs is used to complete the WWW-Authentication header.
|
|
|
|
Examples::
|
|
|
|
# With a Basic auth-scheme, realm MUST be present:
|
|
raise Unauthorized("Auth required.",
|
|
scheme="Basic",
|
|
realm="Restricted Area")
|
|
|
|
# With a Digest auth-scheme, things are a bit more complicated:
|
|
raise Unauthorized("Auth required.",
|
|
scheme="Digest",
|
|
realm="Restricted Area",
|
|
qop="auth, auth-int",
|
|
algorithm="MD5",
|
|
nonce="abcdef",
|
|
opaque="zyxwvu")
|
|
|
|
# With a Bearer auth-scheme, realm is optional so you can write:
|
|
raise Unauthorized("Auth required.", scheme="Bearer")
|
|
|
|
# or, if you want to specify the realm:
|
|
raise Unauthorized("Auth required.",
|
|
scheme="Bearer",
|
|
realm="Restricted Area")
|
|
"""
|
|
|
|
status_code = 401
|
|
quiet = True
|
|
|
|
def __init__(self, message, status_code=None, scheme=None, **kwargs):
|
|
super().__init__(message, status_code)
|
|
|
|
# if auth-scheme is specified, set "WWW-Authenticate" header
|
|
if scheme is not None:
|
|
values = ['{!s}="{!s}"'.format(k, v) for k, v in kwargs.items()]
|
|
challenge = ", ".join(values)
|
|
|
|
self.headers = {
|
|
"WWW-Authenticate": f"{scheme} {challenge}".rstrip()
|
|
}
|
|
|
|
|
|
class LoadFileException(SanicException):
|
|
pass
|
|
|
|
|
|
class InvalidSignal(SanicException):
|
|
pass
|
|
|
|
|
|
class WebsocketClosed(SanicException):
|
|
quiet = True
|
|
message = "Client has closed the websocket connection"
|
|
|
|
|
|
def abort(status_code: int, message: Optional[Union[str, bytes]] = None):
|
|
"""
|
|
Raise an exception based on SanicException. Returns the HTTP response
|
|
message appropriate for the given status code, unless provided.
|
|
|
|
STATUS_CODES from sanic.helpers for the given status code.
|
|
|
|
:param status_code: The HTTP status code to return.
|
|
:param message: The HTTP response body. Defaults to the messages in
|
|
"""
|
|
import warnings
|
|
|
|
warnings.warn(
|
|
"sanic.exceptions.abort has been marked as deprecated, and will be "
|
|
"removed in release 21.12.\n To migrate your code, simply replace "
|
|
"abort(status_code, msg) with raise SanicException(msg, status_code), "
|
|
"or even better, raise an appropriate SanicException subclass."
|
|
)
|
|
|
|
raise SanicException(message=message, status_code=status_code)
|