Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Release History
1.4.0 (unreleased)
------------------

- Accept any sequence of ``(name, value)`` header pairs, matching ``h11``.

- Require ``event_hint`` when constructing ``RemoteProtocolError``.
This is an API-breaking change.

Expand Down
20 changes: 12 additions & 8 deletions src/wsproto/handshake.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,15 +81,17 @@ def initiate_upgrade_connection(
This should be used if the request has already be received and
parsed.

:param list headers: HTTP headers represented as a list of 2-tuples.
:param headers: HTTP headers as a sequence of ``(name, value)`` pairs.
:param str path: A URL path.
"""
if self.client:
msg = "Cannot initiate an upgrade connection when acting as the client"
raise LocalProtocolError(
msg,
)
upgrade_request = h11.Request(method=b"GET", target=path, headers=headers)
upgrade_request = h11.Request(
method=b"GET", target=path, headers=list(headers),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is explicitly converting to a list here necessary? From what I see in h11 Request and h11, all sequences are normalized and validated anyway.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was not. h11 already walks any sequence of pairs in normalize_and_validate, so the extra list() was just a copy.

375ecbf passes headers through, and concatenates extra headers with unpacking. _reject still copies to a list because it appends content-length.

)
h11_client = h11.Connection(h11.CLIENT)
self.receive_data(h11_client.send(upgrade_request))

Expand Down Expand Up @@ -204,7 +206,7 @@ def _process_connection_request(
subprotocols: list[str] = []
upgrade = b""
version = None
headers: Headers = []
collected: list[tuple[bytes, bytes]] = []
for name, value in event.headers:
name = name.lower()
if name == b"connection":
Expand All @@ -224,7 +226,8 @@ def _process_connection_request(
version = value
elif name == b"upgrade":
upgrade = value
headers.append((name, value))
collected.append((name, value))
headers: Headers = collected
if connection_tokens is None or not any(
token.lower() == "upgrade" for token in connection_tokens
):
Expand Down Expand Up @@ -299,7 +302,7 @@ def _accept(self, event: AcceptConnection) -> bytes:

response = h11.InformationalResponse(
status_code=101,
headers=headers + event.extra_headers,
headers=headers + list(event.extra_headers),
reason=b"Switching Protocols",
)
self._connection = Connection(
Expand Down Expand Up @@ -381,7 +384,7 @@ def _initiate_connection(self, request: Request) -> bytes:
upgrade = h11.Request(
method=b"GET",
target=request.target.encode("ascii"),
headers=headers + request.extra_headers,
headers=headers + list(request.extra_headers),
)
return self._h11_connection.send(upgrade) or b""

Expand All @@ -397,7 +400,7 @@ def _establish_client_connection(
accepts: list[str] = []
subprotocol = None
upgrade = b""
headers: Headers = []
collected: list[tuple[bytes, bytes]] = []
for name, value in event.headers:
name = name.lower()
if name == b"connection":
Expand All @@ -415,7 +418,8 @@ def _establish_client_connection(
if name == b"upgrade":
upgrade = value
continue # Skip appending to headers
headers.append((name, value))
collected.append((name, value))
headers: Headers = collected

if connection_tokens is None or not any(
token.lower() == "upgrade" for token in connection_tokens
Expand Down
4 changes: 2 additions & 2 deletions src/wsproto/typing.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from __future__ import annotations
from collections.abc import Sequence

Headers = list[tuple[bytes, bytes]]
Headers = Sequence[tuple[bytes, bytes]]
15 changes: 15 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,21 @@ def test_connection_request_additional_headers() -> None:
assert headers[b"x-bar"] == b"Foo"


def test_connection_request_tuple_extra_headers() -> None:
# Headers accepts any sequence of pairs, not only list.
request = _make_connection_request(
Request(
host="localhost",
target="/",
extra_headers=((b"X-Foo", b"Bar"), (b"X-Bar", b"Foo")),
),
)

headers = normed_header_dict(request.headers)
assert headers[b"x-foo"] == b"Bar"
assert headers[b"x-bar"] == b"Foo"


def test_connection_request_simple_extension() -> None:
extension = FakeExtension(offer_response=True)
request = _make_connection_request(
Expand Down
35 changes: 30 additions & 5 deletions tests/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,9 @@ def test_connection_request_key_header() -> None:
assert str(excinfo.value) == "Missing header, 'Sec-WebSocket-Key'"


def test_upgrade_request() -> None:
server = WSConnection(SERVER)
server.initiate_upgrade_connection(
@pytest.mark.parametrize(
"headers",
[
[
(b"Host", b"localhost"),
(b"Connection", b"Keep-Alive, Upgrade"),
Expand All @@ -142,8 +142,21 @@ def test_upgrade_request() -> None:
(b"Sec-WebSocket-Key", generate_nonce()),
(b"X-Foo", b"bar"),
],
"/",
)
# Sequence that is not a list (the point of Headers = Sequence[...]).
(
(b"Host", b"localhost"),
(b"Connection", b"Keep-Alive, Upgrade"),
(b"Upgrade", b"websocket"),
(b"Sec-WebSocket-Version", b"13"),
(b"Sec-WebSocket-Key", generate_nonce()),
(b"X-Foo", b"bar"),
),
],
ids=["list", "tuple"],
)
def test_upgrade_request(headers: Headers) -> None:
server = WSConnection(SERVER)
server.initiate_upgrade_connection(headers, "/")
event = next(server.events())
event = cast("Request", event)

Expand Down Expand Up @@ -222,6 +235,18 @@ def test_handshake_extra_headers() -> None:
]


def test_handshake_tuple_extra_headers() -> None:
response, nonce = _make_handshake([], accept_headers=((b"X-Foo", b"bar"),))

assert response.status_code == 101
assert sorted(response.headers) == [
(b"connection", b"Upgrade"),
(b"sec-websocket-accept", generate_accept_token(nonce)),
(b"upgrade", b"websocket"),
(b"x-foo", b"bar"),
]


@pytest.mark.parametrize("accept_subprotocol", ["one", "two"])
def test_handshake_with_subprotocol(accept_subprotocol: str) -> None:
response, _ = _make_handshake(
Expand Down