Skip to content
Closed
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
39 changes: 26 additions & 13 deletions rpcclient/infrastructure.go
Original file line number Diff line number Diff line change
Expand Up @@ -811,11 +811,13 @@ retryloop:
}

// Configure basic access authorization.
user, pass, authErr := config.getAuth()
if authErr != nil {
return nil, authErr
if !config.DisableAuth {
user, pass, authErr := config.getAuth()
if authErr != nil {
return nil, authErr
}
httpReq.SetBasicAuth(user, pass)
}
httpReq.SetBasicAuth(user, pass)

httpResponse, err = httpClient.Do(httpReq)

Expand Down Expand Up @@ -1264,6 +1266,12 @@ type ConnConfig struct {
// Pass is the passphrase to use to authenticate to the RPC server.
Pass string

// DisableAuth prevents the client from sending HTTP Basic Auth
// credentials. This is useful for RPC providers that authenticate
// out-of-band, for example with an API key in the request URL, and
// reject Authorization headers.
DisableAuth bool

// CookiePath is the path to a cookie file containing the username and
// passphrase to use to authenticate to the RPC server. It is used
// instead of User and Pass if non-empty.
Expand Down Expand Up @@ -1469,16 +1477,21 @@ func dial(config *ConnConfig) (*websocket.Conn, error) {
dialer.NetDial = proxy.Dial
}

// The RPC server requires basic authorization, so create a custom
// request header with the Authorization header set.
user, pass, err := config.getAuth()
if err != nil {
return nil, err
}
login := user + ":" + pass
auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(login))
// The RPC server requires basic authorization by default, so create a
// custom request header with the Authorization header set unless the
// caller explicitly disabled auth.
requestHeader := make(http.Header)
requestHeader.Add("Authorization", auth)
if !config.DisableAuth {
user, pass, err := config.getAuth()
if err != nil {
return nil, err
}
login := user + ":" + pass
auth := "Basic " + base64.StdEncoding.EncodeToString(
[]byte(login),
)
requestHeader.Add("Authorization", auth)
}
for key, value := range config.ExtraHeaders {
requestHeader.Add(key, value)
}
Expand Down
79 changes: 79 additions & 0 deletions rpcclient/infrastructure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,85 @@ func TestSendPostRequestWithRetrySuccess(t *testing.T) {
require.Equal(t, []byte("1"), result)
}

// TestSendPostRequestWithRetryUsesBasicAuthByDefault ensures the existing HTTP
// POST-mode auth behavior is preserved when DisableAuth is false.
func TestSendPostRequestWithRetryUsesBasicAuthByDefault(t *testing.T) {
t.Parallel()

authChecked := make(chan struct{}, 1)
client := newPostModeTestClient(postRoundTripFunc(
func(req *http.Request) (*http.Response, error) {
user, pass, ok := req.BasicAuth()
require.True(t, ok)
require.Equal(t, "user", user)
require.Equal(t, "pass", pass)
authChecked <- struct{}{}

return &http.Response{
StatusCode: http.StatusOK,
Header: make(http.Header),
Body: io.NopCloser(strings.NewReader(
`{"result":1,"error":null,"id":1}`,
)),
}, nil
},
))
jReq := newPostTestRequest()

result, err := sendPostRequestWithRetry(
context.Background(), jReq, 1, client.httpClient,
client.config, client.httpURL, false,
)
require.NoError(t, err)
require.Equal(t, []byte("1"), result)

select {
case <-authChecked:
case <-time.After(time.Second):
t.Fatal("timed out waiting for auth check")
}
}

// TestSendPostRequestWithRetryDisableAuth skips Basic Auth entirely for RPC
// providers that authenticate out-of-band and reject Authorization headers.
func TestSendPostRequestWithRetryDisableAuth(t *testing.T) {
t.Parallel()

authChecked := make(chan struct{}, 1)
client := newPostModeTestClient(postRoundTripFunc(
func(req *http.Request) (*http.Response, error) {
require.Empty(t, req.Header.Get("Authorization"))
authChecked <- struct{}{}

return &http.Response{
StatusCode: http.StatusOK,
Header: make(http.Header),
Body: io.NopCloser(strings.NewReader(
`{"result":1,"error":null,"id":1}`,
)),
}, nil
},
))
client.config.User = ""
client.config.Pass = ""
client.config.CookiePath = ""
client.config.DisableAuth = true
jReq := newPostTestRequest()

result, err := sendPostRequestWithRetry(
context.Background(), jReq, 1, client.httpClient,
client.config, client.httpURL, false,
)
require.NoError(t, err)
require.Equal(t, []byte("1"), result)

select {
case <-authChecked:
case <-time.After(time.Second):
t.Fatal("timed out waiting for auth check")
}
}

// TestSendPostRequestWithRetryShutdown keeps the shutdown regression cases in
// one table while preserving a distinct symptomatic failure for each path.
func TestSendPostRequestWithRetryShutdown(t *testing.T) {
Expand Down