Repository: nhooyr/websocket
Pull request #336 (2022-04-28 14:11:45)

Extend DialOptions to allow Host header override
The standard `http.Request.Host` field allows to override the `Host` header that is sent with the HTTP request (`URL.Host` is used by default). Such option can be useful in various scenarios, for example when running behind some proxy server.

This patch extends the `DialOptions` struct with `Host` field that basically does the same.

bendiscz: > Couldn't you use `Host` in `HTTPHeader`?

That does not work unfortunately. 'http.Client.Do` ignores `Host` in `HTTPHeader`, you need to set the `http.Request.Host` field if you want to override host from `http.Request.URL`.
nhooyr: Sounds good will get this in then after #256 
nhooyr: Or you know what we could actually check for `Host` in `HTTPHeader` and special case it to apply to `http.Request.Host`. The API in `net/http` isn't something we should follow to the tee anyway as it's hodgepodge.
bendiscz: Checking for `Host` in `HTTPHeader` works too, I just wanted to keep the API similar to net/http... But that is certainly not required :-)

Should I change the patch?
nhooyr: Go for it and thank you for your contribution. It's still going to take me a while to merge as I need to take care of #256 

Patch:
diff --git a/dial.go b/dial.go
index 4b2b7b6..510b94b 100644
--- a/dial.go
+++ b/dial.go
@@ -30,6 +30,10 @@ type DialOptions struct {
 	// HTTPHeader specifies the HTTP headers included in the handshake request.
 	HTTPHeader http.Header
 
+	// Host optionally overrides the Host HTTP header to send. If empty, the value
+	// of URL.Host will be used.
+	Host string
+
 	// Subprotocols lists the WebSocket subprotocols to negotiate with the server.
 	Subprotocols []string
 
@@ -168,6 +172,9 @@ func handshakeRequest(ctx context.Context, urls string, opts *DialOptions, copts
 	if err != nil {
 		return nil, fmt.Errorf("failed to create new http request: %w", err)
 	}
+	if len(opts.Host) > 0 {
+		req.Host = opts.Host
+	}
 	req.Header = opts.HTTPHeader.Clone()
 	req.Header.Set("Connection", "Upgrade")
 	req.Header.Set("Upgrade", "websocket")


Test patch:
diff --git a/dial_test.go b/dial_test.go
index 75d5954..8680147 100644
--- a/dial_test.go
+++ b/dial_test.go
@@ -4,6 +4,7 @@
 package websocket
 
 import (
+	"bytes"
 	"context"
 	"crypto/rand"
 	"io"
@@ -118,6 +119,65 @@ func TestBadDials(t *testing.T) {
 	})
 }
 
+func Test_verifyHostOverride(t *testing.T) {
+	testCases := []struct {
+		name string
+		host string
+		exp  string
+	}{
+		{
+			name: "noOverride",
+			host: "",
+			exp:  "example.com",
+		},
+		{
+			name: "hostOverride",
+			host: "example.net",
+			exp:  "example.net",
+		},
+	}
+
+	for _, tc := range testCases {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
+			ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
+			defer cancel()
+
+			rt := func(r *http.Request) (*http.Response, error) {
+				assert.Equal(t, "Host", tc.exp, r.Host)
+
+				h := http.Header{}
+				h.Set("Connection", "Upgrade")
+				h.Set("Upgrade", "websocket")
+				h.Set("Sec-WebSocket-Accept", secWebSocketAccept(r.Header.Get("Sec-WebSocket-Key")))
+
+				return &http.Response{
+					StatusCode: http.StatusSwitchingProtocols,
+					Header:     h,
+					Body:       mockBody{bytes.NewBufferString("hi")},
+				}, nil
+			}
+
+			_, _, err := Dial(ctx, "ws://example.com", &DialOptions{
+				HTTPClient: mockHTTPClient(rt),
+				Host:       tc.host,
+			})
+			assert.Success(t, err)
+		})
+	}
+
+}
+
+type mockBody struct {
+	*bytes.Buffer
+}
+
+func (mb mockBody) Close() error {
+	return nil
+}
+
 func Test_verifyServerHandshake(t *testing.T) {
 	t.Parallel()