dart-lang / sdk

The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.
https://dart.dev
BSD 3-Clause "New" or "Revised" License
10.24k stars 1.57k forks source link

dart:io WebSocket implementation sends headers in all lowercase format breaking compatibility with some servers #46599

Open jeffmikels opened 3 years ago

jeffmikels commented 3 years ago

When dart:io WebSocket attempts to upgrade a socket, it fails to connect to some servers because the dart websocket implementation sends all headers as lowercase.

The sample code below illustrates what I'm talking about.

import 'dart:io';

var server1 = 'ws://192.168.10.50:60157/stagedisplay';
// THIS SERVER RESPONDS WITH THE FOLLOWING HEADERS
// HTTP/1.1 101 Web Socket Protocol Handshake
// WebSocket-Location: ws://192.168.10.50:60157/stagedisplay
// Sec-WebSocket-Accept: cVJJw2Rwllr2QhiEkRWqEoqHfX0=
// Upgrade: WebSocket
// Connection: Upgrade
// WebSocket-Origin: chrome-extension://mefhakmgclhhfbdadeojlkbllmecialg

var server2 = 'ws://192.168.10.50:60777/stagedisplay';
// THIS SERVER RESPONDS WITH THE FOLLOWING HEADERS
// HTTP/1.1 101 Switching Protocols
// Upgrade: WebSocket
// Connection: Upgrade
// Sec-WebSocket-Accept: cnDz5Eny51ADskV2HSpc+jWUgJk=

void main() async {
  // ignore: close_sinks

  for (var server in [server1, server2]) {
    print('Testing server: $server');
    try {
      var ws = await WebSocket.connect(server);
      print('Successfully Connected');
      ws.close();
    } on HttpException catch (e) {
      print(e);
    }
  }
}

Running this code provides the following output:

$ dart wstest.dart

Testing server: ws://192.168.10.50:60157/stagedisplay
Successfully Connected
Testing server: ws://192.168.10.50:60777/stagedisplay
Connection closed before full header was received
http://192.168.10.50:60777/stagedisplay
HttpException: Connection closed before full header was received, uri = http://192.168.10.50:60777/stagedisplay

Using wireshark to scan the websocket traffic, I have confirmed that dart is sending the headers as all lowercase:

GET /stagedisplay HTTP/1.1
user-agent: Dart/2.13 (dart:io)
connection: Upgrade
cache-control: no-cache
accept-encoding: gzip
content-length: 0
sec-websocket-version: 13
host: 192.168.10.50:60777
sec-websocket-extensions: permessage-deflate; client_max_window_bits
sec-websocket-key: /8STpzZ6qXqaQvnHNehseA==
upgrade: websocket

This is a problem, because the server in question expects the http headers to be in camel case. Using curl, I can confirm that the case is the problem:

This successfully results in a connected session.

$ curl --verbose 'http://192.168.10.50:60777/stagedisplay' -H 'Sec-WebSocket-Key: /8STpzZ6qXqaQvnHNehseA=='  -H 'Upgrade: websocket'  -H 'Cache-Control: no-cache'   -H 'Connection: Upgrade'   -H 'Sec-WebSocket-Version: 13'   --compressed

But this fails:

curl --verbose 'http://192.168.10.50:60777/stagedisplay' -H 'sec-websocket-key: /8STpzZ6qXqaQvnHNehseA=='  -H 'Upgrade: websocket'  -H 'Cache-Control: no-cache'   -H 'Connection: Upgrade'   -H 'Sec-WebSocket-Version: 13'   --compressed

Clearly, this is a problem with the server failing to implement the HTTP spec correctly, but on the other hand, CamelCase headers are the preferred method for Google Chrome and are a reasonable expectation for servers and clients.

For Google:

HttpException: Connection closed before full header was received

MrSorcus commented 3 years ago

https://datatracker.ietf.org/doc/html/rfc2616#section-4.2

Each header field consists of a name followed by a colon (":") and the field value. Field names are case-insensitive.