diff --git a/lib/mint/web_socket.ex b/lib/mint/web_socket.ex index f1a01c45..01839afb 100644 --- a/lib/mint/web_socket.ex +++ b/lib/mint/web_socket.ex @@ -208,6 +208,11 @@ defmodule Mint.WebSocket do connections established by passing `:http` to `Mint.HTTP.connect/4` and `:wss` corresponding to `:https`. + The WebSocket handshake headers are subject to the `:case_sensitive_headers` + option of `Mint.HTTP.connect/4`: they are lowercased by default and sent in + their conventional casing (`Sec-WebSocket-Key` and friends) when that option + is enabled. + ## Options * `:extensions` - a list of extensions to negotiate. See the extensions diff --git a/lib/mint/web_socket/utils.ex b/lib/mint/web_socket/utils.ex index 3a8eb913..42cdc0b8 100644 --- a/lib/mint/web_socket/utils.ex +++ b/lib/mint/web_socket/utils.ex @@ -9,21 +9,23 @@ defmodule Mint.WebSocket.Utils do :crypto.strong_rand_bytes(16) |> Base.encode64() end + # header names are written in their conventional casing: Mint lowercases them + # unless the connection was opened with `case_sensitive_headers: true` def headers({:http1, nonce}, extensions) when is_binary(nonce) do [ - {"upgrade", "websocket"}, - {"connection", "upgrade"}, - {"sec-websocket-version", "13"}, - {"sec-websocket-key", nonce}, - {"sec-websocket-extensions", extension_string(extensions)} + {"Upgrade", "websocket"}, + {"Connection", "upgrade"}, + {"Sec-WebSocket-Version", "13"}, + {"Sec-WebSocket-Key", nonce}, + {"Sec-WebSocket-Extensions", extension_string(extensions)} ] |> Enum.reject(fn {_k, v} -> v == "" end) end def headers(:http2, extensions) do [ - {"sec-websocket-version", "13"}, - {"sec-websocket-extensions", extension_string(extensions)} + {"Sec-WebSocket-Version", "13"}, + {"Sec-WebSocket-Extensions", extension_string(extensions)} ] |> Enum.reject(fn {_k, v} -> v == "" end) end diff --git a/test/mint/web_socket_test.exs b/test/mint/web_socket_test.exs index 12f72167..fdc9ed9f 100644 --- a/test/mint/web_socket_test.exs +++ b/test/mint/web_socket_test.exs @@ -119,6 +119,44 @@ defmodule Mint.WebSocketTest do end end + describe "given an HTTP/1 connection to a socket which captures the request" do + setup do + {:ok, listen_socket} = :gen_tcp.listen(0, [:binary, active: false, reuseaddr: true]) + on_exit(fn -> :gen_tcp.close(listen_socket) end) + {:ok, port} = :inet.port(listen_socket) + + [listen_socket: listen_socket, port: port] + end + + test "the upgrade headers are lowercased by default", c do + {:ok, conn} = HTTP1.connect(:http, "localhost", c.port) + {:ok, _conn, _ref} = WebSocket.upgrade(:ws, conn, "/", []) + + request = capture_request(c.listen_socket) + assert request =~ "upgrade: websocket" + assert request =~ "sec-websocket-key: " + end + + test "the upgrade headers keep their casing with case_sensitive_headers: true", c do + {:ok, conn} = HTTP1.connect(:http, "localhost", c.port, case_sensitive_headers: true) + {:ok, _conn, _ref} = WebSocket.upgrade(:ws, conn, "/", []) + + request = capture_request(c.listen_socket) + assert request =~ "Upgrade: websocket" + assert request =~ "Connection: upgrade" + assert request =~ "Sec-WebSocket-Version: 13" + assert request =~ "Sec-WebSocket-Key: " + end + end + + defp capture_request(listen_socket) do + {:ok, socket} = :gen_tcp.accept(listen_socket, 5_000) + {:ok, request} = :gen_tcp.recv(socket, 0, 5_000) + :gen_tcp.close(socket) + + request + end + @doc !""" In Mint 1.5.0+, Mint handles the SETTINGS frame from the server asynchronously and returns default values for server settings until it is received. So we must