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

Allow case-insensitive HTTP Upgrade header #2097

Merged
merged 7 commits into from
Apr 5, 2021
Merged

Allow case-insensitive HTTP Upgrade header #2097

merged 7 commits into from
Apr 5, 2021

Conversation

ENT8R
Copy link
Contributor

@ENT8R ENT8R commented Apr 4, 2021

As far as I understand Section 11.2 of RFC 6455 ("The WebSocket Protocol"), the HTTP Upgrade header should actually be WebSocket. At least Firefox uses the header like that, which leads to a wrong detection of the underlying protocol in Sanic (e.g. timeouts after a minute because Sanic thinks the request is a normal HTTP request). This is why I propose to make the check for the Upgrade header case-insensitive in order to correctly handle the incoming request.

@ENT8R ENT8R requested a review from a team as a code owner April 4, 2021 13:07
@codecov
Copy link

codecov bot commented Apr 4, 2021

Codecov Report

Merging #2097 (1fa4832) into main (93a0246) will not change coverage.
The diff coverage is 100.000%.

Impacted file tree graph

@@            Coverage Diff            @@
##              main     #2097   +/-   ##
=========================================
  Coverage   92.253%   92.253%           
=========================================
  Files           38        38           
  Lines         3485      3485           
  Branches       583       583           
=========================================
  Hits          3215      3215           
  Misses         183       183           
  Partials        87        87           
Impacted Files Coverage Δ
sanic/request.py 97.712% <ø> (ø)
sanic/http.py 77.491% <100.000%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 93a0246...1fa4832. Read the comment docs.

Copy link
Member

@ashleysommer ashleysommer left a comment

Choose a reason for hiding this comment

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

The scheme check: headers is not defined, you should use self.headers.

@ashleysommer
Copy link
Member

ashleysommer commented Apr 4, 2021

Looks great. I think it might've been done this way (with just lowercase "websockets") because the Headers Class is a CIMultidict, meaning we don't normally need to worry about case. But of course that only applies to the key names, not the values. So when checking for specific values, we do need to convert case.

Thanks for this fix. I'm surprised we havent had reports of broken websockets in Sanic v21.3 due to this.

ashleysommer
ashleysommer previously approved these changes Apr 4, 2021
@ashleysommer
Copy link
Member

Ah, type checking has picked up a valid error case.

headers.get('upgrade') could return None, and you can't do .lower() on None.

I know it's a pain and an extra step, but you probably need to put a and headers.get('upgrade', None) is not None in there too.

@ahopkins
Copy link
Member

ahopkins commented Apr 4, 2021

I agree. I am surprised as well since this has certainly been tested and out in usage already. Which kind of makes me curious as to why we haven't seen this already. Ideally I'd also like to have a test coverage for it, but that might be hard to do with our current test client.

@ENT8R
Copy link
Contributor Author

ENT8R commented Apr 4, 2021

Thank you for the fast response and quick review.
At least in my case, the code continued to work, but I was seeing lots of timeouts and websocket reconnections every 60 seconds. The behavior was definitely not wanted but didn't seem to cause other effects besides the reconnections. Also, if I wouldn't have checked the logs, I would have probably never seen this bug...

@harshanarayana
Copy link
Contributor

harshanarayana commented Apr 4, 2021

Interesting. I did a bit of digging.

  1. Mozilla Dev
  2. RFC

In the RFC the sample always shows

        GET /chat HTTP/1.1
        Host: server.example.com
        Upgrade: websocket
        Connection: Upgrade
        Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
        Origin: http://example.com
        Sec-WebSocket-Protocol: chat, superchat
        Sec-WebSocket-Version: 13

So does the documentation of the Mozilla Dev page.

Running a quick inspect on Websocket using Firefox indicates the same.

Screenshot 2021-04-04 at 9 11 52 PM

This is the same behavior on chrome as well.

Screenshot 2021-04-04 at 9 13 40 PM

sanic/http.py Outdated
@@ -218,7 +218,7 @@ async def http1_request_header(self):
raise InvalidUsage("Bad Request")

headers_instance = Header(headers)
self.upgrade_websocket = headers_instance.get("upgrade") == "websocket"
self.upgrade_websocket = "upgrade" in headers_instance and headers_instance.get("upgrade", None) is not None and headers_instance.get("upgrade").lower() == "websocket"
Copy link
Contributor

@harshanarayana harshanarayana Apr 4, 2021

Choose a reason for hiding this comment

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

If we really want to do this check the easier option might be

Suggested change
self.upgrade_websocket = "upgrade" in headers_instance and headers_instance.get("upgrade", None) is not None and headers_instance.get("upgrade").lower() == "websocket"
self.upgrade_websocket = headers_instance.get("upgrade", "").lower() == "websocket"

Copy link
Member

Choose a reason for hiding this comment

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

I like this better

@ashleysommer
Copy link
Member

I don't know whats going on with those travis tests. It should be passing now. I'll try to restart them.

@ashleysommer
Copy link
Member

ashleysommer commented Apr 5, 2021

Nope, it won't let me restart it. (Error: "Oops, this Job cannot be Restarted.")

I think we can just merge this regardless.

On a different note, for @sanic-org/core-devs
I'm wondering if we should try to avoid using headers.get('key') in our codebase.
In the CIMultidict that holds our headers, each key can have multiple values. Thats why the .getone() and .getall() methods exist. From the Multidict Docs, the .get() method simply gets the first value, and has implicit None as default. Doing .get('key') is the same as .getone('key', None). I wonder if we should be more explicit with the intention in the code and change all occurrences of .get('key') to .getone('key', None).

@harshanarayana
Copy link
Contributor

I don't know whats going on with those travis tests. It should be passing now. I'll try to restart them.

Those tests I've been really flaky lately. Especially the py3{8,9}-no-ext and like you noticed, Travis restart tests aren't working anymore. 😥

harshanarayana
harshanarayana previously approved these changes Apr 5, 2021
Copy link
Contributor

@harshanarayana harshanarayana left a comment

Choose a reason for hiding this comment

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

Approving this change. But I do like the idea of using getone as suggested by @ashleysommer.. May be it can be a total cleanup work across the board using a different issue item?

Copy link
Member

@ahopkins ahopkins left a comment

Choose a reason for hiding this comment

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

Looks like there is no new line at the end of the files. It should be failing linting. Can you run make pretty to make sure that it complies with black. You can also run tox -e lint to make sure that everything is formatted properly.

@ahopkins
Copy link
Member

ahopkins commented Apr 5, 2021

+1 the explicit usage of getone and getall as a separate PR across the board.

@ENT8R
Copy link
Contributor Author

ENT8R commented Apr 5, 2021

Looks like there is no new line at the end of the files

Are you sure that this is the actual cause of the failing tests? As far as I see, there is generally no new line at the end of files in this project. I feel like this is more related to #2092 because it's only py39-no-ext which fails to build... I ran make pretty and the only complaint was related to the line length. Let's see what Travis thinks about it 🙃

@ENT8R
Copy link
Contributor Author

ENT8R commented Apr 5, 2021

Looks like Travis has no complaints this time 🎉

@ahopkins ahopkins merged commit eba7821 into sanic-org:main Apr 5, 2021
@harshanarayana
Copy link
Contributor

+1 the explicit usage of getone and getall as a separate PR across the board.

Added a tracker item for this

@ahopkins
Copy link
Member

ahopkins commented Apr 5, 2021

+1 the explicit usage of getone and getall as a separate PR across the board.

Added a tracker item for this

#2098

@ENT8R ENT8R deleted the patch-1 branch April 5, 2021 11:44
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.

None yet

4 participants