Skip to content
Open
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
15 changes: 15 additions & 0 deletions src/websockets/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
* :exc:`ProxyError`
* :exc:`InvalidProxyMessage`
* :exc:`InvalidProxyStatus`
* :exc:`InvalidMethod`
* :exc:`InvalidMessage`
* :exc:`InvalidStatus`
* :exc:`InvalidStatusCode` (legacy)
Expand Down Expand Up @@ -52,6 +53,7 @@
"ProxyError",
"InvalidProxyMessage",
"InvalidProxyStatus",
"InvalidMethod",
"InvalidMessage",
"InvalidStatus",
"InvalidHeader",
Expand Down Expand Up @@ -235,6 +237,19 @@ def __str__(self) -> str:
return f"proxy rejected connection: HTTP {self.response.status_code:d}"


class InvalidMethod(InvalidHandshake):
"""
Raised when the HTTP method isn't GET.

"""

def __init__(self, method: str) -> None:
self.method = method

def __str__(self) -> str:
return f"invalid HTTP method; expected GET; got {self.method}"


class InvalidMessage(InvalidHandshake):
"""
Raised when a handshake request or response is malformed.
Expand Down
4 changes: 2 additions & 2 deletions src/websockets/http11.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from typing import Callable

from .datastructures import Headers
from .exceptions import SecurityError
from .exceptions import InvalidMethod, SecurityError
from .version import version as websockets_version


Expand Down Expand Up @@ -148,7 +148,7 @@ def parse(
f"unsupported protocol; expected HTTP/1.1: {d(request_line)}"
)
if method != b"GET":
raise ValueError(f"unsupported HTTP method; expected GET; got {d(method)}")
raise InvalidMethod(d(method))
path = raw_path.decode("ascii", "surrogateescape")

headers = yield from parse_headers(read_line)
Expand Down
14 changes: 14 additions & 0 deletions src/websockets/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
InvalidHeader,
InvalidHeaderValue,
InvalidMessage,
InvalidMethod,
InvalidOrigin,
InvalidUpgrade,
NegotiationError,
Expand Down Expand Up @@ -547,6 +548,18 @@ def parse(self) -> Generator[None]:
request = yield from Request.parse(
self.reader.read_line,
)
except InvalidMethod as exc:
self.handshake_exc = exc
response = self.reject(
http.HTTPStatus.METHOD_NOT_ALLOWED,
f"Failed to open a WebSocket connection: {exc}.\n",
)
response.headers["Allow"] = "GET"
self.send_response(response)
self.parser = self.discard()
next(self.parser) # start coroutine
yield
return
except Exception as exc:
self.handshake_exc = InvalidMessage(
"did not receive a valid HTTP request"
Expand All @@ -556,6 +569,7 @@ def parse(self) -> Generator[None]:
self.parser = self.discard()
next(self.parser) # start coroutine
yield
return

if self.debug:
self.logger.debug("< GET %s HTTP/1.1", request.path)
Expand Down
6 changes: 3 additions & 3 deletions tests/test_http11.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from websockets.datastructures import Headers
from websockets.exceptions import SecurityError
from websockets.exceptions import InvalidMethod, SecurityError
from websockets.http11 import *
from websockets.http11 import parse_headers
from websockets.streams import StreamReader
Expand Down Expand Up @@ -61,11 +61,11 @@ def test_parse_unsupported_protocol(self):

def test_parse_unsupported_method(self):
self.reader.feed_data(b"OPTIONS * HTTP/1.1\r\n\r\n")
with self.assertRaises(ValueError) as raised:
with self.assertRaises(InvalidMethod) as raised:
next(self.parse())
self.assertEqual(
str(raised.exception),
"unsupported HTTP method; expected GET; got OPTIONS",
"invalid HTTP method; expected GET; got OPTIONS",
)

def test_parse_invalid_header(self):
Expand Down
65 changes: 65 additions & 0 deletions tests/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from websockets.exceptions import (
InvalidHeader,
InvalidMessage,
InvalidMethod,
InvalidOrigin,
InvalidUpgrade,
NegotiationError,
Expand Down Expand Up @@ -257,6 +258,70 @@ def test_receive_junk_request(self):
"invalid HTTP request line: HELO relay.invalid",
)

@patch("email.utils.formatdate", return_value=DATE)
def test_receive_head_request(self, _formatdate):
"""Server receives a HEAD request and returns 405 Method Not Allowed."""
server = ServerProtocol()
server.receive_data(
(
f"HEAD /test HTTP/1.1\r\n"
f"Host: example.com\r\n"
f"\r\n"
).encode(),
)

self.assertEqual(server.events_received(), [])
self.assertIsInstance(server.handshake_exc, InvalidMethod)
self.assertEqual(str(server.handshake_exc), "invalid HTTP method; expected GET; got HEAD")
self.assertEqual(
server.data_to_send(),
[
f"HTTP/1.1 405 Method Not Allowed\r\n"
f"Date: {DATE}\r\n"
f"Connection: close\r\n"
f"Content-Length: 84\r\n"
f"Content-Type: text/plain; charset=utf-8\r\n"
f"Allow: GET\r\n"
f"\r\n"
f"Failed to open a WebSocket connection: "
f"invalid HTTP method; expected GET; got HEAD.\n".encode(),
b"",
],
)
self.assertTrue(server.close_expected())

@patch("email.utils.formatdate", return_value=DATE)
def test_receive_post_request(self, _formatdate):
"""Server receives a POST request and returns 405 Method Not Allowed."""
server = ServerProtocol()
server.receive_data(
(
f"POST /test HTTP/1.1\r\n"
f"Host: example.com\r\n"
f"\r\n"
).encode(),
)

self.assertEqual(server.events_received(), [])
self.assertIsInstance(server.handshake_exc, InvalidMethod)
self.assertEqual(str(server.handshake_exc), "invalid HTTP method; expected GET; got POST")
self.assertEqual(
server.data_to_send(),
[
f"HTTP/1.1 405 Method Not Allowed\r\n"
f"Date: {DATE}\r\n"
f"Connection: close\r\n"
f"Content-Length: 84\r\n"
f"Content-Type: text/plain; charset=utf-8\r\n"
f"Allow: GET\r\n"
f"\r\n"
f"Failed to open a WebSocket connection: "
f"invalid HTTP method; expected GET; got POST.\n".encode(),
b"",
],
)
self.assertTrue(server.close_expected())


class ResponseTests(unittest.TestCase):
"""Test generating opening handshake responses."""
Expand Down