Repository: nhooyr/websocket
Pull request #273 (2020-12-21 21:36:37)

Preserve client case for selected subprotocol
WebSocket implementation in chromium rejects
ws connections in case server side responses with
`Sec-WebSocket-Protocol` header which value does
not match one of the values sent in the same header by client

In case you use subprotocols to send case-sensitive text
(for example, auth tokens) you end up with inability
to establish websocket connection from chromium.

#### How to reproduce:

Initialize websocket like this:

```go
websocketOptions := &websocket.AcceptOptions{}
if subProtocol := r.Header.Get("Sec-Websocket-Protocol"); subProtocol != "" {
	websocketOptions.Subprotocols = []string{subProtocol}
}
...
ws, err := websocket.Accept(w, r, websocketOptions)
...
```

After that try to establish connection from javascript in chromium (my current version is `87.0.4280.88`):

```js
var ws = new WebSocket(url, "ToKeN")
```

You will get next error:

```
WebSocket connection to '...' failed: Error during WebSocket handshake: 'Sec-WebSocket-Protocol' header value 'token' in response does not match any of sent values
```

So this PR fixes that behavior.


Patch:
diff --git a/accept.go b/accept.go
index 66379b5..18536bd 100644
--- a/accept.go
+++ b/accept.go
@@ -159,13 +159,13 @@ func verifyClientRequest(w http.ResponseWriter, r *http.Request) (errCode int, _
 		return http.StatusUpgradeRequired, fmt.Errorf("WebSocket protocol violation: handshake request must be at least HTTP/1.1: %q", r.Proto)
 	}
 
-	if !headerContainsToken(r.Header, "Connection", "Upgrade") {
+	if !headerContainsTokenIgnoreCase(r.Header, "Connection", "Upgrade") {
 		w.Header().Set("Connection", "Upgrade")
 		w.Header().Set("Upgrade", "websocket")
 		return http.StatusUpgradeRequired, fmt.Errorf("WebSocket protocol violation: Connection header %q does not contain Upgrade", r.Header.Get("Connection"))
 	}
 
-	if !headerContainsToken(r.Header, "Upgrade", "websocket") {
+	if !headerContainsTokenIgnoreCase(r.Header, "Upgrade", "websocket") {
 		w.Header().Set("Connection", "Upgrade")
 		w.Header().Set("Upgrade", "websocket")
 		return http.StatusUpgradeRequired, fmt.Errorf("WebSocket protocol violation: Upgrade header %q does not contain websocket", r.Header.Get("Upgrade"))
@@ -309,11 +309,9 @@ func acceptWebkitDeflate(w http.ResponseWriter, ext websocketExtension, mode Com
 	return copts, nil
 }
 
-func headerContainsToken(h http.Header, key, token string) bool {
-	token = strings.ToLower(token)
-
+func headerContainsTokenIgnoreCase(h http.Header, key, token string) bool {
 	for _, t := range headerTokens(h, key) {
-		if t == token {
+		if strings.EqualFold(t, token) {
 			return true
 		}
 	}
@@ -354,7 +352,6 @@ func headerTokens(h http.Header, key string) []string {
 	for _, v := range h[key] {
 		v = strings.TrimSpace(v)
 		for _, t := range strings.Split(v, ",") {
-			t = strings.ToLower(t)
 			t = strings.TrimSpace(t)
 			tokens = append(tokens, t)
 		}
diff --git a/dial.go b/dial.go
index 509882e..7a7787f 100644
--- a/dial.go
+++ b/dial.go
@@ -194,11 +194,11 @@ func verifyServerResponse(opts *DialOptions, copts *compressionOptions, secWebSo
 		return nil, fmt.Errorf("expected handshake response status code %v but got %v", http.StatusSwitchingProtocols, resp.StatusCode)
 	}
 
-	if !headerContainsToken(resp.Header, "Connection", "Upgrade") {
+	if !headerContainsTokenIgnoreCase(resp.Header, "Connection", "Upgrade") {
 		return nil, fmt.Errorf("WebSocket protocol violation: Connection header %q does not contain Upgrade", resp.Header.Get("Connection"))
 	}
 
-	if !headerContainsToken(resp.Header, "Upgrade", "WebSocket") {
+	if !headerContainsTokenIgnoreCase(resp.Header, "Upgrade", "WebSocket") {
 		return nil, fmt.Errorf("WebSocket protocol violation: Upgrade header %q does not contain websocket", resp.Header.Get("Upgrade"))
 	}
 


Test patch:
diff --git a/accept_test.go b/accept_test.go
index 9b18d8e..e114d1a 100644
--- a/accept_test.go
+++ b/accept_test.go
@@ -224,6 +224,12 @@ func Test_selectSubprotocol(t *testing.T) {
 			serverProtocols: []string{"echo2", "echo3"},
 			negotiated:      "echo3",
 		},
+		{
+			name:            "clientCasePresered",
+			clientProtocols: []string{"Echo1"},
+			serverProtocols: []string{"echo1"},
+			negotiated:      "Echo1",
+		},
 	}
 
 	for _, tc := range testCases {