Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/project/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ Improvements
* Replied with HTTP 405 Method Not Allowed when the handshake request doesn't
use the GET method, instead of closing the connection.

* Replied with HTTP 414 URI Too Long or 431 Request Header Fields Too Large
when the handshake request exceeds a security limit, instead of closing
the connection.

* Added the ``reconnect_delays`` argument for customizing the delays between
reconnection attempts in :func:`~asyncio.client.connect`, beyond existing
``WEBSOCKETS_BACKOFF_*`` environment variables.
Expand Down
8 changes: 8 additions & 0 deletions docs/reference/exceptions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ also reported by :func:`~websockets.asyncio.server.serve` in logs.

.. autoexception:: SecurityError

.. autoexception:: RequestLineTooLong

.. autoexception:: StatusLineTooLong

.. autoexception:: HeaderLineTooLong

.. autoexception:: TooManyHeaders

.. autoexception:: ProxyError

.. autoexception:: InvalidProxyMessage
Expand Down
7 changes: 7 additions & 0 deletions docs/topics/security.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ You may change these limits by setting the :envvar:`WEBSOCKETS_MAX_LINE_LENGTH`,
:envvar:`WEBSOCKETS_MAX_NUM_HEADERS`, and :envvar:`WEBSOCKETS_MAX_BODY_SIZE`
environment variables respectively.

When a handshake request exceeds one of these limits, the server replies with
HTTP 414 URI Too Long or 431 Request Header Fields Too Large, then closes the
connection.

When a handshake response exceeds one of these limits, the client closes the
connection and raises an exception.

Identification
--------------

Expand Down
12 changes: 12 additions & 0 deletions src/websockets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"ConnectionClosedError",
"ConnectionClosedOK",
"DuplicateParameter",
"HeaderLineTooLong",
"InvalidHandshake",
"InvalidHeader",
"InvalidHeaderFormat",
Expand All @@ -55,7 +56,10 @@
"PayloadTooBig",
"ProtocolError",
"ProxyError",
"RequestLineTooLong",
"SecurityError",
"StatusLineTooLong",
"TooManyHeaders",
"WebSocketException",
# .frames
"Close",
Expand Down Expand Up @@ -101,6 +105,7 @@
ConnectionClosedError,
ConnectionClosedOK,
DuplicateParameter,
HeaderLineTooLong,
InvalidHandshake,
InvalidHeader,
InvalidHeaderFormat,
Expand All @@ -121,7 +126,10 @@
PayloadTooBig,
ProtocolError,
ProxyError,
RequestLineTooLong,
SecurityError,
StatusLineTooLong,
TooManyHeaders,
WebSocketException,
)
from .frames import Close, CloseCode, Frame, Opcode
Expand Down Expand Up @@ -168,6 +176,7 @@
"ConnectionClosedError": ".exceptions",
"ConnectionClosedOK": ".exceptions",
"DuplicateParameter": ".exceptions",
"HeaderLineTooLong": ".exceptions",
"InvalidHandshake": ".exceptions",
"InvalidHeader": ".exceptions",
"InvalidHeaderFormat": ".exceptions",
Expand All @@ -188,7 +197,10 @@
"PayloadTooBig": ".exceptions",
"ProtocolError": ".exceptions",
"ProxyError": ".exceptions",
"RequestLineTooLong": ".exceptions",
"SecurityError": ".exceptions",
"StatusLineTooLong": ".exceptions",
"TooManyHeaders": ".exceptions",
"WebSocketException": ".exceptions",
# .frames
"Close": ".frames",
Expand Down
2 changes: 1 addition & 1 deletion src/websockets/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ async def interactive_client(uri: str, **kwargs: Any) -> None:
else:
print(f"Connected to {uri}.")

# Read messsages from stdin in a thread because Windows doesn't support
# Read messages from stdin in a thread because Windows doesn't support
# reading asynchronously (#1681), and a daemon thread to avoid blocking
# Ctrl-C because signals are only delivered to the main thread.
loop = asyncio.get_event_loop()
Expand Down
7 changes: 7 additions & 0 deletions src/websockets/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
InvalidStatus,
InvalidUpgrade,
NegotiationError,
StatusLineTooLong,
)
from .extensions import ClientExtensionFactory, Extension
from .headers import (
Expand Down Expand Up @@ -305,6 +306,12 @@ def parse(self) -> Generator[None]:
self.reader.read_exact,
self.reader.read_to_eof,
)
except StatusLineTooLong as exc:
self.handshake_exc = exc
self.send_eof()
self.parser = self.discard()
next(self.parser) # start coroutine
yield
except Exception as exc:
self.handshake_exc = InvalidMessage(
"did not receive a valid HTTP response"
Expand Down
36 changes: 36 additions & 0 deletions src/websockets/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
* :exc:`InvalidProxy`
* :exc:`InvalidHandshake`
* :exc:`SecurityError`
* :exc:`RequestLineTooLong`
* :exc:`StatusLineTooLong`
* :exc:`HeaderLineTooLong`
* :exc:`TooManyHeaders`
* :exc:`ProxyError`
* :exc:`InvalidProxyMessage`
* :exc:`InvalidProxyStatus`
Expand Down Expand Up @@ -50,6 +54,10 @@
"InvalidProxy",
"InvalidHandshake",
"SecurityError",
"RequestLineTooLong",
"StatusLineTooLong",
"HeaderLineTooLong",
"TooManyHeaders",
"ProxyError",
"InvalidProxyMessage",
"InvalidProxyStatus",
Expand Down Expand Up @@ -210,6 +218,34 @@ class SecurityError(InvalidHandshake):
"""


class RequestLineTooLong(SecurityError):
"""
Raised when the request line of a handshake request is too long.

"""


class StatusLineTooLong(SecurityError):
"""
Raised when the status line of a handshake response is too long.

"""


class HeaderLineTooLong(SecurityError):
"""
Raised when a header line of a handshake request or response is too long.

"""


class TooManyHeaders(SecurityError):
"""
Raised when a handshake request or response has too many headers.

"""


class ProxyError(InvalidHandshake):
"""
Raised when failing to connect to a proxy.
Expand Down
71 changes: 47 additions & 24 deletions src/websockets/http11.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@
from typing import Callable

from .datastructures import Headers
from .exceptions import SecurityError
from .exceptions import (
HeaderLineTooLong,
RequestLineTooLong,
SecurityError,
StatusLineTooLong,
TooManyHeaders,
)
from .version import version as websockets_version


Expand Down Expand Up @@ -106,7 +112,9 @@ def exception(self) -> Exception | None: # pragma: no cover
@classmethod
def parse(
cls,
read_line: Callable[[int], Generator[None, None, bytes | bytearray]],
read_line: Callable[
[int, type[Exception]], Generator[None, None, bytes | bytearray]
],
) -> Generator[None, None, Request]:
"""
Parse a WebSocket handshake request.
Expand All @@ -128,7 +136,9 @@ def parse(

Raises:
EOFError: If the connection is closed without a full HTTP request.
SecurityError: If the request exceeds a security limit.
RequestLineTooLong: If the request line is too long.
HeaderLineTooLong: If a header line is too long.
TooManyHeaders: If there are too many headers.
UnicodeDecodeError: If the request method or path isn't ASCII.
ValueError: If the request isn't well formatted.

Expand All @@ -140,7 +150,7 @@ def parse(
# implement HTTP/1.1 strictly, there's little need for lenient parsing.

try:
request_line = yield from parse_line(read_line)
request_line = yield from parse_line(read_line, RequestLineTooLong)
except EOFError as exc:
raise EOFError("connection closed while reading HTTP request line") from exc

Expand Down Expand Up @@ -217,9 +227,13 @@ def exception(self) -> Exception | None: # pragma: no cover
@classmethod
def parse(
cls,
read_line: Callable[[int], Generator[None, None, bytes | bytearray]],
read_line: Callable[
[int, type[Exception]], Generator[None, None, bytes | bytearray]
],
read_exact: Callable[[int], Generator[None, None, bytes | bytearray]],
read_to_eof: Callable[[int], Generator[None, None, bytes | bytearray]],
read_to_eof: Callable[
[int, type[Exception]], Generator[None, None, bytes | bytearray]
],
proxy: bool = False,
) -> Generator[None, None, Response]:
"""
Expand All @@ -241,15 +255,18 @@ def parse(

Raises:
EOFError: If the connection is closed without a full HTTP response.
SecurityError: If the response exceeds a security limit.
StatusLineTooLong: If the status line is too long.
HeaderLineTooLong: If a header line is too long.
TooManyHeaders: If there are too many headers.
SecurityError: If the response body exceeds a security limit.
LookupError: If the response isn't well formatted.
ValueError: If the response isn't well formatted.

"""
# https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2

try:
status_line = yield from parse_line(read_line)
status_line = yield from parse_line(read_line, StatusLineTooLong)
except EOFError as exc:
raise EOFError("connection closed while reading HTTP status line") from exc

Expand Down Expand Up @@ -311,7 +328,10 @@ def serialize(self) -> bytes:


def parse_line(
read_line: Callable[[int], Generator[None, None, bytes | bytearray]],
read_line: Callable[
[int, type[Exception]], Generator[None, None, bytes | bytearray]
],
too_long_exc_type: type[Exception] = SecurityError,
) -> Generator[None, None, bytes | bytearray]:
"""
Parse a single line.
Expand All @@ -321,24 +341,25 @@ def parse_line(
Args:
read_line: Generator-based coroutine that reads a LF-terminated line
or raises an exception if there isn't enough data.
too_long_exc_type: exception to raise if the line is too long;
defaults to :exc:`SecurityError`.

Raises:
EOFError: If the connection is closed without a CRLF.
SecurityError: If the response exceeds a security limit.

"""
try:
line = yield from read_line(MAX_LINE_LENGTH)
except RuntimeError:
raise SecurityError("line too long")
line = yield from read_line(MAX_LINE_LENGTH, too_long_exc_type)
# Not mandatory but safe - https://datatracker.ietf.org/doc/html/rfc7230#section-3.5
if not line.endswith(b"\r\n"):
raise EOFError("line without CRLF")
return line[:-2]


def parse_headers(
read_line: Callable[[int], Generator[None, None, bytes | bytearray]],
read_line: Callable[
[int, type[Exception]], Generator[None, None, bytes | bytearray]
],
) -> Generator[None, None, Headers]:
"""
Parse HTTP headers.
Expand All @@ -353,7 +374,8 @@ def parse_headers(

Raises:
EOFError: If the connection is closed without complete headers.
SecurityError: If the request exceeds a security limit.
HeaderLineTooLong: If a header line is too long.
TooManyHeaders: If there are too many headers.
ValueError: If the request isn't well formatted.

"""
Expand All @@ -364,7 +386,7 @@ def parse_headers(
headers = Headers()
for _ in range(MAX_NUM_HEADERS + 1):
try:
line = yield from parse_line(read_line)
line = yield from parse_line(read_line, HeaderLineTooLong)
except EOFError as exc:
raise EOFError("connection closed while reading HTTP headers") from exc
if line == b"":
Expand All @@ -390,17 +412,21 @@ def parse_headers(
headers.set_insecure(name, value)

else:
raise SecurityError("too many HTTP headers")
raise TooManyHeaders(f"expected no more than {MAX_NUM_HEADERS} headers")

return headers


def read_body(
status_code: int,
headers: Headers,
read_line: Callable[[int], Generator[None, None, bytes | bytearray]],
read_line: Callable[
[int, type[Exception]], Generator[None, None, bytes | bytearray]
],
read_exact: Callable[[int], Generator[None, None, bytes | bytearray]],
read_to_eof: Callable[[int], Generator[None, None, bytes | bytearray]],
read_to_eof: Callable[
[int, type[Exception]], Generator[None, None, bytes | bytearray]
],
) -> Generator[None, None, bytes | bytearray]:
# https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.3

Expand All @@ -420,7 +446,7 @@ def read_body(

body = b""
while True:
chunk_size_line = yield from parse_line(read_line)
chunk_size_line = yield from parse_line(read_line, SecurityError)
raw_chunk_size = chunk_size_line.split(b";", 1)[0]
# Set a lower limit than default_max_str_digits; 1 EB is plenty.
if len(raw_chunk_size) > 15:
Expand Down Expand Up @@ -450,7 +476,4 @@ def read_body(
return (yield from read_exact(content_length))

else:
try:
return (yield from read_to_eof(MAX_BODY_SIZE))
except RuntimeError:
raise SecurityError(f"body too large: over {MAX_BODY_SIZE} bytes")
return (yield from read_to_eof(MAX_BODY_SIZE, SecurityError))
Loading
Loading