Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,12 @@ func (p *Server) forwardRequest(conn net.Conn, req *http.Request, https bool, se
resp.ProtoMajor = 1
resp.ProtoMinor = 1

// We serve one request per connection and close it in the caller's defer,
// so the client has to be told, or a pooled socket is reused after we are
// gone (RFC 9112 section 9.6).
resp.Header.Del("Connection")
resp.Close = true

// Copy response back to client
err = resp.Write(conn)
if err != nil {
Expand Down Expand Up @@ -492,6 +498,7 @@ For more help: https://github.com/coder/boundary

resp.Body = io.NopCloser(strings.NewReader(body))
resp.ContentLength = int64(len(body))
resp.Close = true

// Copy response back to client
err := resp.Write(conn)
Expand Down
45 changes: 45 additions & 0 deletions proxy/proxy_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package proxy

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/require"
)

// TestProxyServerBasicHTTP tests basic HTTP request handling
Expand Down Expand Up @@ -63,3 +67,44 @@ func TestProxyServerBasicHTTPS(t *testing.T) {
pt.ExpectDeny("https://localhost:8080/", "example.com")
})
}

// The proxy serves one request per connection and then closes it, so both the
// forwarded and the blocked response have to say so. Without the signal a
// client that pools sockets reuses one the proxy has already closed.
func TestProxyResponsesSignalConnectionClose(t *testing.T) {
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
}))
defer backend.Close()

pt := NewProxyTest(t, WithAllowedDomain("127.0.0.1")).Start()
defer pt.Stop()

t.Run("allowed request", func(t *testing.T) {
resp, err := pt.proxyClient.Get(backend.URL)
require.NoError(t, err)
defer resp.Body.Close() //nolint:errcheck

require.Equal(t, http.StatusOK, resp.StatusCode)
require.True(t, resp.Close, "response must tell the client the connection is closing")
})

t.Run("blocked request", func(t *testing.T) {
blocked := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer blocked.Close()

req, err := http.NewRequest(http.MethodGet, blocked.URL, nil)
require.NoError(t, err)
req.Host = "example.com"

resp, err := pt.proxyClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close() //nolint:errcheck

require.Equal(t, http.StatusForbidden, resp.StatusCode)
require.True(t, resp.Close, "blocked response must tell the client the connection is closing")
})
}