What changed, and why it matters
This commit only changes tests and clarifies a public comment. It does not alter the actual authentication behavior of the btcd RPC client. The code already only suppresses the internally generated Basic auth header when DisableAuth is true; the commit makes the tests check that real requests succeed and confirms caller-provided Authorization headers are still sent. There is no security fix here.
No action required. This is a test-hardening and documentation commit with no security vulnerability or fix.
Security signals we found
No functional code change; only tests and comments
Comment clarification that DisableAuth only suppresses generated Basic auth, not caller-provided Authorization headers
Tests now cover WebSocket handshake, cookie bypass, and caller-provided headers
Evidence from the diff
The diff updates rpcclient/disableauth_test.go to use a fake HTTP round-tripper and a real WebSocket test server, checking that DisableAuth omits only the generated Basic Authorization header while preserving ExtraHeaders (including a caller-supplied Authorization). It also rewords comments in rpcclient/infrastructure.go to state that DisableAuth skips generated Basic auth but caller-provided Authorization values in ExtraHeaders are still sent. No functional code path is changed.
Changed components
rpcclient/disableauth_test.gorpcclient/infrastructure.go (comments only)Inspect captured patch +186 / −127
diff --git a/rpcclient/disableauth_test.go b/rpcclient/disableauth_test.go
index e35a888..7bba3a5 100644
--- a/rpcclient/disableauth_test.go
+++ b/rpcclient/disableauth_test.go
@@ -1,140 +1,202 @@
package rpcclient
import (
+ "context"
"encoding/base64"
+ "io"
"net/http"
"net/http/httptest"
+ "path/filepath"
"strings"
"testing"
+ "time"
+ "github.com/gorilla/websocket"
"github.com/stretchr/testify/require"
)
-// TestDisableAuth verifies that the DisableAuth field correctly controls
-// whether the Authorization header is sent on RPC requests.
-func TestDisableAuth(t *testing.T) {
- t.Parallel()
-
- t.Run("DisableAuth true omits Authorization header", func(t *testing.T) {
- t.Parallel()
-
- var gotAuth string
- handler := http.HandlerFunc(
- func(w http.ResponseWriter, r *http.Request) {
- gotAuth = r.Header.Get("Authorization")
-
- // Return a valid JSON-RPC response so the client
- // doesn't retry.
- w.Header().Set("Content-Type", "application/json")
- w.Write([]byte(
- `{"result":null,"error":null,"id":1}`,
- ))
+const (
+ testRPCUser = "testuser"
+ testRPCPass = "testpass"
+ testCallerAuth = "Bearer test-api-key"
+ testExtraHeader = "X-Test-API-Key"
+ testExtraValue = "test-api-key"
+)
+
+// disableAuthTestCase describes one authentication header configuration that
+// must behave the same for HTTP POST and WebSocket transports.
+type disableAuthTestCase struct {
+ name string
+ configure func(*ConnConfig)
+ wantAuthorization string
+}
+
+// disableAuthTestCases returns the shared transport authentication cases.
+func disableAuthTestCases(missingCookie string) []disableAuthTestCase {
+ basicAuth := "Basic " + base64.StdEncoding.EncodeToString(
+ []byte(testRPCUser+":"+testRPCPass),
+ )
+
+ return []disableAuthTestCase{
+ {
+ name: "disabled omits generated authorization",
+ configure: func(config *ConnConfig) {
+ config.User = ""
+ config.Pass = ""
+ config.CookiePath = missingCookie
+ config.DisableAuth = true
},
- )
- srv := httptest.NewServer(handler)
- defer srv.Close()
-
- addr := strings.TrimPrefix(srv.URL, "http://")
- client, err := New(&ConnConfig{
- Host: addr,
- HTTPPostMode: true,
- DisableAuth: true,
- DisableTLS: true,
- }, nil)
- require.NoError(t, err)
- defer client.Shutdown()
-
- // The client is now connected; issue a simple request to trigger
- // handleSendPostMessage.
- _, err = client.RawRequest("getblockchaininfo", nil)
- // We don't care if the RPC itself errors. We only care about
- // the Authorization header.
- _ = err
-
- require.Empty(
- t, gotAuth,
- "Authorization header should be empty when DisableAuth is true",
- )
- })
-
- t.Run("DisableAuth false includes Authorization header", func(t *testing.T) {
- t.Parallel()
-
- var gotAuth string
- handler := http.HandlerFunc(
- func(w http.ResponseWriter, r *http.Request) {
- gotAuth = r.Header.Get("Authorization")
- w.Header().Set("Content-Type", "application/json")
- w.Write([]byte(
- `{"result":null,"error":null,"id":1}`,
- ))
+ },
+ {
+ name: "disabled preserves caller authorization",
+ configure: func(config *ConnConfig) {
+ config.User = ""
+ config.Pass = ""
+ config.CookiePath = missingCookie
+ config.DisableAuth = true
+ config.ExtraHeaders["Authorization"] =
+ testCallerAuth
},
- )
- srv := httptest.NewServer(handler)
- defer srv.Close()
-
- addr := strings.TrimPrefix(srv.URL, "http://")
- client, err := New(&ConnConfig{
- Host: addr,
- HTTPPostMode: true,
- DisableAuth: false,
- DisableTLS: true,
- User: "testuser",
- Pass: "testpass",
- }, nil)
- require.NoError(t, err)
- defer client.Shutdown()
-
- _, err = client.RawRequest("getblockchaininfo", nil)
- _ = err
-
- login := []byte("testuser:testpass")
- expected := "Basic " + base64.StdEncoding.EncodeToString(login)
- require.Equal(
- t, expected, gotAuth,
- "Authorization header should be set when DisableAuth is false",
- )
- })
-
- t.Run(
- "DisableAuth default (zero value) includes Authorization header",
- func(t *testing.T) {
- t.Parallel()
-
- var gotAuth string
- handler := http.HandlerFunc(
- func(w http.ResponseWriter, r *http.Request) {
- gotAuth = r.Header.Get("Authorization")
- w.Header().Set("Content-Type", "application/json")
- w.Write([]byte(
- `{"result":null,"error":null,"id":1}`,
- ))
+ wantAuthorization: testCallerAuth,
+ },
+ {
+ name: "explicit false includes basic authorization",
+ configure: func(config *ConnConfig) {
+ config.DisableAuth = false
+ },
+ wantAuthorization: basicAuth,
+ },
+ {
+ name: "zero value includes basic authorization",
+ configure: func(*ConnConfig) {
+ // Leave DisableAuth at its zero value.
+ },
+ wantAuthorization: basicAuth,
+ },
+ }
+}
+
+// newDisableAuthConfig creates the common configuration for the transport
+// authentication cases.
+func newDisableAuthConfig() *ConnConfig {
+ return &ConnConfig{
+ User: testRPCUser,
+ Pass: testRPCPass,
+ ExtraHeaders: map[string]string{
+ testExtraHeader: testExtraValue,
+ },
+ }
+}
+
+// assertAuthHeaders verifies both generated or caller-supplied authorization
+// and the independent extra header.
+func assertAuthHeaders(t *testing.T, header http.Header,
+ wantAuthorization string) {
+
+ t.Helper()
+
+ require.Equal(t, wantAuthorization, header.Get("Authorization"))
+ require.Equal(t, testExtraValue, header.Get(testExtraHeader))
+}
+
+// TestDisableAuthHTTPPost verifies that DisableAuth controls generated Basic
+// Auth headers on HTTP POST requests without suppressing caller headers.
+func TestDisableAuthHTTPPost(t *testing.T) {
+ missingCookie := filepath.Join(t.TempDir(), "missing-cookie")
+
+ for _, tc := range disableAuthTestCases(missingCookie) {
+ t.Run(tc.name, func(t *testing.T) {
+ requestHeader := make(chan http.Header, 1)
+ client := newPostModeTestClient(postRoundTripFunc(
+ func(req *http.Request) (*http.Response, error) {
+ requestHeader <- req.Header.Clone()
+
+ return &http.Response{
+ StatusCode: http.StatusOK,
+ Header: make(http.Header),
+ Body: io.NopCloser(strings.NewReader(
+ `{"result":1,"error":null,"id":1}`,
+ )),
+ }, nil
},
+ ))
+ client.config = newDisableAuthConfig()
+ client.config.Host = "127.0.0.1:8332"
+ client.config.DisableTLS = true
+ client.config.HTTPPostMode = true
+ tc.configure(client.config)
+
+ result, err := sendPostRequestWithRetry(
+ context.Background(), newPostTestRequest(), 1,
+ client.httpClient, client.config, client.httpURL,
+ false,
)
- srv := httptest.NewServer(handler)
- defer srv.Close()
-
- addr := strings.TrimPrefix(srv.URL, "http://")
- client, err := New(&ConnConfig{
- Host: addr,
- HTTPPostMode: true,
- DisableTLS: true,
- User: "myuser",
- Pass: "mypass",
- }, nil)
require.NoError(t, err)
- defer client.Shutdown()
+ require.Equal(t, []byte("1"), result)
- _, err = client.RawRequest("getblockchaininfo", nil)
- _ = err
+ select {
+ case header := <-requestHeader:
+ assertAuthHeaders(t, header, tc.wantAuthorization)
- login := []byte("myuser:mypass")
- expected := "Basic " +
- base64.StdEncoding.EncodeToString(login)
- require.Equal(
- t, expected, gotAuth,
- "Authorization header should be set by default",
- )
+ case <-time.After(time.Second):
+ t.Fatal("timed out waiting for HTTP POST request")
+ }
+ })
+ }
+}
+
+// newWebsocketAuthServer creates a server that records the WebSocket handshake
+// headers before upgrading the connection.
+func newWebsocketAuthServer(t *testing.T) (string, <-chan http.Header) {
+ t.Helper()
+
+ requestHeader := make(chan http.Header, 1)
+ upgrader := websocket.Upgrader{}
+ handler := http.HandlerFunc(
+ func(w http.ResponseWriter, req *http.Request) {
+ requestHeader <- req.Header.Clone()
+
+ conn, err := upgrader.Upgrade(w, req, nil)
+ if err != nil {
+ return
+ }
+ defer func() {
+ _ = conn.Close()
+ }()
},
)
+ server := httptest.NewServer(handler)
+ t.Cleanup(server.Close)
+
+ return strings.TrimPrefix(server.URL, "http://"), requestHeader
+}
+
+// TestDisableAuthWebsocket verifies that DisableAuth controls generated Basic
+// Auth headers on WebSocket handshakes without suppressing caller headers.
+func TestDisableAuthWebsocket(t *testing.T) {
+ missingCookie := filepath.Join(t.TempDir(), "missing-cookie")
+
+ for _, tc := range disableAuthTestCases(missingCookie) {
+ t.Run(tc.name, func(t *testing.T) {
+ host, requestHeader := newWebsocketAuthServer(t)
+ config := newDisableAuthConfig()
+ config.Host = host
+ config.DisableTLS = true
+ tc.configure(config)
+
+ conn, err := dial(config)
+ require.NoError(t, err)
+ t.Cleanup(func() {
+ require.NoError(t, conn.Close())
+ })
+
+ select {
+ case header := <-requestHeader:
+ assertAuthHeaders(t, header, tc.wantAuthorization)
+
+ case <-time.After(time.Second):
+ t.Fatal("timed out waiting for WebSocket handshake")
+ }
+ })
+ }
}
diff --git a/rpcclient/infrastructure.go b/rpcclient/infrastructure.go
index 5d2d3b2..a454849 100644
--- a/rpcclient/infrastructure.go
+++ b/rpcclient/infrastructure.go
@@ -810,7 +810,7 @@ retryloop:
httpReq.Header.Set(key, value)
}
- // Configure basic access authorization.
+ // Configure generated basic access authorization.
if !config.DisableAuth {
user, pass, authErr := config.getAuth()
if authErr != nil {
@@ -1333,10 +1333,9 @@ type ConnConfig struct {
// when connecting to blockchain.info RPC server
EnableBCInfoHacks bool
- // DisableAuth instructs the client to skip setting the Authorization
- // header on RPC requests. This is useful when connecting to third-party
- // RPC providers that authenticate via API key in the URL path and
- // reject requests containing an Authorization header with 401 errors.
+ // DisableAuth instructs the client to skip generating a Basic
+ // Authorization header for RPC requests. Caller-provided Authorization
+ // values in ExtraHeaders are still sent.
DisableAuth bool
}
@@ -1477,10 +1476,8 @@ func dial(config *ConnConfig) (*websocket.Conn, error) {
dialer.NetDial = proxy.Dial
}
- // Configure basic access authorization. When DisableAuth is set, skip
- // setting the Authorization header entirely. This is useful for
- // third-party RPC providers that authenticate via API key in the URL
- // path and reject requests containing an Authorization header.
+ // Configure generated basic access authorization. Caller-provided
+ // headers are added independently below.
requestHeader := make(http.Header)
if !config.DisableAuth {
user, pass, err := config.getAuth()
Why this scored 12/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.