Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check if handshake is completed before sending frame on wsproto shutdown #1737

Merged
merged 6 commits into from
Oct 31, 2022

Conversation

Kludex
Copy link
Member

@Kludex Kludex commented Oct 28, 2022

I'm extremely happy about this PR. I understand WebSocket things now. 😅

How to test it?

Application:

import asyncio


async def app(scope, receive, send):
    assert scope["type"] == "websocket"
    event = await receive()
    assert event["type"] == "websocket.connect"

    print("Go ahead, stop the server...")
    await asyncio.sleep(10)
    print("Too late!")

    await send({"type": "websocket.accept"})

Client (I've used curl):

curl --include \
     --no-buffer \
     --header "Connection: Upgrade" \
     --header "Upgrade: websocket" \
     --header "Host: example.com:80" \
     --header "Origin: http://example.com:80" \
     --header "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" \
     --header "Sec-WebSocket-Version: 13" \
     http://localhost:8000/

Checklist

  • Include every behavior introduced here on websockets implementation.
  • Tests

Comment on lines +128 to +131
if self.handshake_complete:
self.queue.put_nowait({"type": "websocket.disconnect", "code": 1012})
output = self.conn.send(wsproto.events.CloseConnection(code=1012))
self.transport.write(output)
Copy link
Member Author

Choose a reason for hiding this comment

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

This is the core here.

We want to check if the handshake was completed, because CloseConnection is not a valid event for wsproto.WSConnection.send() when the handshake is not completed.

This is the error these lines solve:

wsproto.utilities.LocalProtocolError: Event CloseConnection(code=1012, reason=None) cannot be sent during the handshake

More about it on #596.

Copy link
Member

Choose a reason for hiding this comment

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

Makes sense to me. 👍

Comment on lines +132 to +133
else:
self.send_500_response()
Copy link
Member Author

Choose a reason for hiding this comment

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

The thing is... If the above is False, we still need to send something to the client, otherwise we are being rude. 😞

Without these lines, the client will have a:

curl: (52) Empty reply from server

With these lines:

HTTP/1.1 500 
content-length: 0

The curl command used was:

curl --include \
     --no-buffer \
     --header "Connection: Upgrade" \
     --header "Upgrade: websocket" \
     --header "Host: example.com:80" \
     --header "Origin: http://example.com:80" \
     --header "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" \
     --header "Sec-WebSocket-Version: 13" \
     http://localhost:8000/

Copy link
Member

Choose a reason for hiding this comment

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

Much neater. 😌

I did spend some time trying to figure out if we should we also include a textual description here, but I think it's probably okay as it currently stands.

Comment on lines +228 to +229
except BaseException:
self.logger.exception("Exception in ASGI application\n")
Copy link
Member Author

@Kludex Kludex Oct 28, 2022

Choose a reason for hiding this comment

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

These are analogous. Just making it more readable.

Comment on lines -260 to +270
output = self.conn.send(
wsproto.events.AcceptConnection(
subprotocol=subprotocol,
extensions=extensions,
extra_headers=extra_headers,
if not self.transport.is_closing():
output = self.conn.send(
wsproto.events.AcceptConnection(
subprotocol=subprotocol,
extensions=extensions,
extra_headers=extra_headers,
)
)
)
self.transport.write(output)
self.transport.write(output)
Copy link
Member Author

@Kludex Kludex Oct 28, 2022

Choose a reason for hiding this comment

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

This is necessary on this scenario, because the transport was closed on the shutdown(), it can also be that the client disconnected before accepting the connection...

@Kludex Kludex mentioned this pull request Oct 28, 2022
13 tasks
Comment on lines +348 to +349
if self.ws_server.closing:
return {"type": "websocket.disconnect", "code": 1012}
Copy link
Member Author

Choose a reason for hiding this comment

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

This is to match the behavior on both implementations.

If we close the connection on shutdown, we should be sending 1012.

@Kludex Kludex requested a review from a team October 29, 2022 13:05
setup.cfg Outdated Show resolved Hide resolved
@Kludex Kludex added this to the Version 0.20.0 milestone Oct 29, 2022
@@ -82,7 +82,7 @@ plugins =

[coverage:report]
precision = 2
fail_under = 97.82
fail_under = 97.92
Copy link
Member Author

Choose a reason for hiding this comment

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

Yey! 😎 👍

Copy link
Contributor

@iudeen iudeen left a comment

Choose a reason for hiding this comment

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

Handles everything that was attempted in #1738 😄

tests/utils.py Outdated Show resolved Hide resolved
tests/utils.py Outdated Show resolved Hide resolved
Copy link
Member

@tomchristie tomchristie left a comment

Choose a reason for hiding this comment

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

I'm extremely happy about this PR.

😊

@Kludex
Copy link
Member Author

Kludex commented Oct 31, 2022

Thanks @tomchristie 🙏

@Kludex Kludex merged commit ec3aac3 into master Oct 31, 2022
@Kludex Kludex deleted the fix/596 branch October 31, 2022 11:06
Comment on lines +553 to +578
@pytest.mark.anyio
@pytest.mark.parametrize("ws_protocol_cls", WS_PROTOCOLS)
@pytest.mark.parametrize("http_protocol_cls", HTTP_PROTOCOLS)
async def test_not_accept_on_connection_lost(ws_protocol_cls, http_protocol_cls):
send_accept_task = asyncio.Event()

async def app(scope, receive, send):
while True:
message = await receive()
if message["type"] == "websocket.connect":
await send_accept_task.wait()
await send({"type": "websocket.accept"})
elif message["type"] == "websocket.disconnect":
break

async def websocket_session(uri):
async with websockets.client.connect(uri):
while True:
await asyncio.sleep(0.1)

config = Config(app=app, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off")
async with run_server(config):
task = asyncio.create_task(websocket_session("ws://127.0.0.1:8000"))
await asyncio.sleep(0.1)
task.cancel()
send_accept_task.set()
Copy link
Member Author

Choose a reason for hiding this comment

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

This test actually fails on the websockets implementation.

This fails before and after my changes, so it is actually not this PR who broke it.

There's a single small change on the websockets implementation on this PR, which actually handles the next test.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Handshaking may not be completed yet at shutdown in wsproto impl
3 participants