python-hyper / wsproto

Sans-IO WebSocket protocol implementation
https://wsproto.readthedocs.io/
MIT License
261 stars 38 forks source link

The plain HTTP request was sent to HTTPS port #165

Closed Bluenix2 closed 2 years ago

Bluenix2 commented 2 years ago

When trying to use WSProto I ran into issues with connecting to Discord using a Secure WebSocket as Cloudflare kept returning a 400 Bad Request rejection.

What appeared to be happening was that WSProto included the wss part of the URL as in the HTTP request as shown here:

GET /?v=9&encoding=json HTTP/1.1
Host: wss://gateway.discord.gg
Upgrade: WebSocket
Connection: Upgrade
Sec-WebSocket-Key: ch6rqRZdOku8udke8Q6pqQ==
Sec-WebSocket-Version: 13

I could fix this by changing Request('wss://gateway.discord.gg', target='/?v=9$encoding=json') to Request('gateway.discord.gg', target='/?v=9$encoding=json').

Now I am having issues with WSProto trying to send a HTTP request instead of a HTTPS one as sen from this Cloudflare response:

RejectData(data=bytearray(b'<html>\r\n<head><title>400 The plain HTTP request was sent to HTTPS port</title></head>\r\n<body>\r\n<center><h1>400 Bad Request</h1></center>\r\n<center>The plain HTTP request was sent to HTTPS port</center>\r\n<hr><center>cloudflare</center>\r\n</body>\r\n</html>\r\n'), body_finished=False)
RejectData(data=b'', body_finished=True)
Kriechi commented 2 years ago

@Bluenix2 would you mind pasting your code of how you are creating the socket and related parts?

wsproto is a sans-io library, meaning that it does not handle your TLS connection or socket -- you have to take care of that yourself. wsproto only received and emits bytes, but putting them into a network connection (either plain or with TLS) is up to you. In the general protocol stack, the operating system takes care of the TCP socket, then you probably want to use Python's ssl module or similar to give you a secure trusted TLS socket. And then you shuffle bytes in and out with wsproto.

You should only pass a valid hostname (without any scheme prefix) to the Request event.

Bluenix2 commented 2 years ago

Hello, the code below should be able to reproduce the issue @Kriechi

import socket

from wsproto.events import Request
from wsproto import WSConnection, ConnectionType

RECV_BUFFER = 4096

conn = WSConnection(ConnectionType.CLIENT)

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

sock.connect(('gateway.discord.gg', 443))

sock.send(conn.send(Request('gateway.discord.gg', target='/?v=9&encoding=json')))

while True:
    data = conn.receive_data(sock.recv(RECV_BUFFER))

    for event in conn.events():
        print(event)

The output of this code can be seen below:

RejectConnection(status_code=400, headers=<Headers([(b'server', b'cloudflare'), (b'date', b'Fri, 22 Oct 2021 07:56:35 GMT'), (b'content-type', b'text/html'), (b'content-length', b'253'), (b'connection', b'close'), (b'cf-ray', b'-')])>, has_body=True)
RejectData(data=bytearray(b'<html>\r\n<head><title>400 The plain HTTP request was sent to HTTPS port</title></head>\r\n<body>\r\n<center><h1>400 Bad Request</h1></center>\r\n<center>The plain HTTP request was sent to HTTPS port</center>\r\n<hr><center>cloudflare</center>\r\n</body>\r\n</html>\r\n'), body_finished=False)
RejectData(data=b'', body_finished=True)
Kriechi commented 2 years ago

@Bluenix2 you are missing the TLS layer in your connection. The error message clearly tells you: you are not sending TLS encrypted data to port 443.

This snippet should get you a working socket with TLS (untested):

import socket
import ssl
import certifi

SERVER_NAME = 'gateway.discord.gg'
SERVER_PORT = 443

# generic socket and ssl configuration
socket.setdefaulttimeout(15)
ctx = ssl.create_default_context(cafile=certifi.where())

# open a socket to the server and initiate TLS
sock = socket.create_connection((SERVER_NAME, SERVER_PORT))
sock = ctx.wrap_socket(sock, server_hostname=SERVER_NAME)

# wsproto
conn = WSConnection(ConnectionType.CLIENT)
sock.send(conn.send(Request(SERVER_NAME, target='/?v=9&encoding=json')))