Skip to content

Commit 14ed61f

Browse files
committed
raise on misdirected websocket handshake
Responding "HTTP/1.1 101 Switching Protocols" to a HEAD or POST request is unlikely intended. Spec says **"MUST stop"** for non-GET requests: https://datatracker.ietf.org/doc/html/rfc6455#section-4.2.1 There is precedent for being strict about 1xx informational responses that might confuse a proxy, see expect-100 processing.
1 parent d912123 commit 14ed61f

File tree

4 files changed

+61
-11
lines changed

4 files changed

+61
-11
lines changed

CHANGES/10729.breaking.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Stopped processing non-``GET`` method in WebSocket connection handshake (this might confuse proxies) as demanded by :rfc:`6455#section-4.2.1` -- by :user:`pajod`.

aiohttp/web_ws.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
from .log import ws_logger
3737
from .streams import EofStream
3838
from .typedefs import JSONDecoder, JSONEncoder
39-
from .web_exceptions import HTTPBadRequest, HTTPException
39+
from .web_exceptions import HTTPBadRequest, HTTPException, HTTPMethodNotAllowed
4040
from .web_request import BaseRequest
4141
from .web_response import StreamResponse
4242

@@ -226,6 +226,9 @@ def _handshake(
226226
self, request: BaseRequest
227227
) -> Tuple["CIMultiDict[str]", Optional[str], int, bool]:
228228
headers = request.headers
229+
if request.method != hdrs.METH_GET:
230+
raise HTTPMethodNotAllowed(request.method, {hdrs.METH_GET})
231+
229232
if "websocket" != headers.get(hdrs.UPGRADE, "").lower().strip():
230233
raise HTTPBadRequest(
231234
text=(

tests/test_web_websocket.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,12 @@ def test_can_prepare_unknown_protocol(make_request: _RequestMaker) -> None:
215215
assert WebSocketReady(True, None) == ws.can_prepare(req)
216216

217217

218+
def test_can_prepare_invalid_method(make_request: _RequestMaker) -> None:
219+
req = make_request("POST", "/")
220+
ws = web.WebSocketResponse()
221+
assert WebSocketReady(False, None) == ws.can_prepare(req)
222+
223+
218224
def test_can_prepare_without_upgrade(make_request: _RequestMaker) -> None:
219225
req = make_request("GET", "/", headers=CIMultiDict({}))
220226
ws = web.WebSocketResponse()
@@ -369,11 +375,11 @@ async def test_close_idempotent(make_request: _RequestMaker) -> None:
369375
assert close_code == 0
370376

371377

372-
async def test_prepare_post_method_ok(make_request: _RequestMaker) -> None:
378+
async def test_prepare_invalid_method(make_request: _RequestMaker) -> None:
373379
req = make_request("POST", "/")
374380
ws = web.WebSocketResponse()
375-
await ws.prepare(req)
376-
assert ws.prepared
381+
with pytest.raises(web.HTTPMethodNotAllowed):
382+
await ws.prepare(req)
377383

378384

379385
async def test_prepare_without_upgrade(make_request: _RequestMaker) -> None:

tests/test_websocket_handshake.py

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,29 +40,61 @@ def gen_ws_headers(
4040
return hdrs, key
4141

4242

43+
async def test_not_get() -> None:
44+
ws = web.WebSocketResponse()
45+
req = make_mocked_request("POST", "/")
46+
with pytest.raises(web.HTTPMethodNotAllowed):
47+
await ws.prepare(req)
48+
49+
50+
async def test_inappropriate_method() -> None:
51+
ws = web.WebSocketResponse()
52+
req = make_mocked_request(
53+
"HEAD",
54+
"/",
55+
headers=[
56+
("Upgrade", "websocket"),
57+
# expect refusal; not a 1xx status (or two)
58+
("Expect", "100-continue"),
59+
("Expect", "100-continue"),
60+
],
61+
)
62+
with pytest.raises(web.HTTPMethodNotAllowed) as ctx:
63+
await ws.prepare(req)
64+
assert ctx.value.method == "HEAD"
65+
assert ctx.value.allowed_methods == {"GET"}
66+
assert ctx.value.status == 405
67+
68+
4369
async def test_no_upgrade() -> None:
4470
ws = web.WebSocketResponse()
4571
req = make_mocked_request("GET", "/")
46-
with pytest.raises(web.HTTPBadRequest):
72+
with pytest.raises(web.HTTPBadRequest) as ctx:
4773
await ws.prepare(req)
74+
assert ctx.value.text and "UPGRADE" in ctx.value.text
75+
assert ctx.value.status == 400
4876

4977

5078
async def test_no_connection() -> None:
5179
ws = web.WebSocketResponse()
5280
req = make_mocked_request(
5381
"GET", "/", headers={"Upgrade": "websocket", "Connection": "keep-alive"}
5482
)
55-
with pytest.raises(web.HTTPBadRequest):
83+
with pytest.raises(web.HTTPBadRequest) as ctx:
5684
await ws.prepare(req)
85+
assert ctx.value.text and "CONNECTION" in ctx.value.text
86+
assert ctx.value.status == 400
5787

5888

5989
async def test_protocol_version_unset() -> None:
6090
ws = web.WebSocketResponse()
6191
req = make_mocked_request(
6292
"GET", "/", headers={"Upgrade": "websocket", "Connection": "upgrade"}
6393
)
64-
with pytest.raises(web.HTTPBadRequest):
94+
with pytest.raises(web.HTTPBadRequest) as ctx:
6595
await ws.prepare(req)
96+
assert ctx.value.text and "version" in ctx.value.text
97+
assert ctx.value.status == 400
6698

6799

68100
async def test_protocol_version_not_supported() -> None:
@@ -76,8 +108,10 @@ async def test_protocol_version_not_supported() -> None:
76108
"Sec-Websocket-Version": "1",
77109
},
78110
)
79-
with pytest.raises(web.HTTPBadRequest):
111+
with pytest.raises(web.HTTPBadRequest) as ctx:
80112
await ws.prepare(req)
113+
assert ctx.value.text and "version" in ctx.value.text
114+
assert ctx.value.status == 400
81115

82116

83117
async def test_protocol_key_not_present() -> None:
@@ -91,8 +125,10 @@ async def test_protocol_key_not_present() -> None:
91125
"Sec-Websocket-Version": "13",
92126
},
93127
)
94-
with pytest.raises(web.HTTPBadRequest):
128+
with pytest.raises(web.HTTPBadRequest) as ctx:
95129
await ws.prepare(req)
130+
assert ctx.value.text and "Handshake" in ctx.value.text
131+
assert ctx.value.status == 400
96132

97133

98134
async def test_protocol_key_invalid() -> None:
@@ -107,8 +143,10 @@ async def test_protocol_key_invalid() -> None:
107143
"Sec-Websocket-Key": "123",
108144
},
109145
)
110-
with pytest.raises(web.HTTPBadRequest):
146+
with pytest.raises(web.HTTPBadRequest) as ctx:
111147
await ws.prepare(req)
148+
assert ctx.value.text and "Handshake" in ctx.value.text
149+
assert ctx.value.status == 400
112150

113151

114152
async def test_protocol_key_bad_size() -> None:
@@ -125,8 +163,10 @@ async def test_protocol_key_bad_size() -> None:
125163
"Sec-Websocket-Key": val,
126164
},
127165
)
128-
with pytest.raises(web.HTTPBadRequest):
166+
with pytest.raises(web.HTTPBadRequest) as ctx:
129167
await ws.prepare(req)
168+
assert ctx.value.text and "Handshake" in ctx.value.text
169+
assert ctx.value.status == 400
130170

131171

132172
async def test_handshake_ok() -> None:

0 commit comments

Comments
 (0)