Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(tm2): Implement Basic Authentication in HTTP Client #2590

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
50 changes: 40 additions & 10 deletions tm2/pkg/bft/rpc/lib/client/http/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"io"
"net"
"net/http"
"net/url"
"strings"

types "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types"
Expand All @@ -28,24 +29,31 @@
ErrInvalidBatchResponse = errors.New("invalid http batch response size")
)

type AuthInfo struct {
Username string
Password string
}

// Client is an HTTP client implementation
type Client struct {
rpcURL string // the remote RPC URL of the node
authInfo AuthInfo // the basic authentication info
rpcURL string // the remote RPC URL of the node

client *http.Client
}

// NewClient initializes and creates a new HTTP RPC client
func NewClient(rpcURL string) (*Client, error) {
// Parse the RPC URL
address, err := toClientAddress(rpcURL)
// Parse the RPC URL and extract auth info
address, authInfo, err := toClientAddressAndAuth(rpcURL)
if err != nil {
return nil, fmt.Errorf("invalid RPC URL, %w", err)
}

c := &Client{
rpcURL: address,
client: defaultHTTPClient(rpcURL),
rpcURL: address,
authInfo: authInfo,
client: defaultHTTPClient(rpcURL),
}

return c, nil
Expand All @@ -54,7 +62,7 @@
// SendRequest sends a single RPC request to the server
func (c *Client) SendRequest(ctx context.Context, request types.RPCRequest) (*types.RPCResponse, error) {
// Send the request
response, err := sendRequestCommon[types.RPCRequest, *types.RPCResponse](ctx, c.client, c.rpcURL, request)
response, err := sendRequestCommon[types.RPCRequest, *types.RPCResponse](ctx, c.client, c.rpcURL, c.authInfo, request)
if err != nil {
return nil, err
}
Expand All @@ -70,7 +78,7 @@
// SendBatch sends a single RPC batch request to the server
func (c *Client) SendBatch(ctx context.Context, requests types.RPCRequests) (types.RPCResponses, error) {
// Send the batch
responses, err := sendRequestCommon[types.RPCRequests, types.RPCResponses](ctx, c.client, c.rpcURL, requests)
responses, err := sendRequestCommon[types.RPCRequests, types.RPCResponses](ctx, c.client, c.rpcURL, c.authInfo, requests)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -110,6 +118,7 @@
ctx context.Context,
client *http.Client,
rpcURL string,
authInfo AuthInfo,
request T,
) (R, error) {
// Marshal the request
Expand All @@ -131,6 +140,11 @@
// Set the header content type
req.Header.Set("Content-Type", "application/json")

// Set the basic authentication if provided
if authInfo.Username != "" {
req.SetBasicAuth(authInfo.Username, authInfo.Password)

Check warning on line 145 in tm2/pkg/bft/rpc/lib/client/http/client.go

View check run for this annotation

Codecov / codecov/patch

tm2/pkg/bft/rpc/lib/client/http/client.go#L145

Added line #L145 was not covered by tests
}

// Execute the request
httpResponse, err := client.Do(req.WithContext(ctx))
if err != nil {
Expand Down Expand Up @@ -208,10 +222,26 @@
return clientProtocol, trimmedAddress
}

func toClientAddress(remoteAddr string) (string, error) {
clientProtocol, trimmedAddress := toClientAddrAndParse(remoteAddr)
func toClientAddressAndAuth(remoteAddr string) (string, AuthInfo, error) {
parsedURL, err := url.Parse(remoteAddr)
if err != nil {
return "", AuthInfo{}, err

Check warning on line 228 in tm2/pkg/bft/rpc/lib/client/http/client.go

View check run for this annotation

Codecov / codecov/patch

tm2/pkg/bft/rpc/lib/client/http/client.go#L228

Added line #L228 was not covered by tests
}

username := parsedURL.User.Username()
password, _ := parsedURL.User.Password()

authInfo := AuthInfo{
Username: username,
Password: password,
}

// Remove the user info from the parsed URL
parsedURL.User = nil

clientProtocol, trimmedAddress := toClientAddrAndParse(parsedURL.String())

return clientProtocol + "://" + trimmedAddress, nil
return clientProtocol + "://" + trimmedAddress, authInfo, nil
}

// network - name of the network (for example, "tcp", "unix")
Expand Down
103 changes: 103 additions & 0 deletions tm2/pkg/bft/rpc/lib/client/http/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,55 @@ import (
"github.com/stretchr/testify/require"
)

func TestNewClient(t *testing.T) {
t.Parallel()

testCases := []struct {
rpcURL string
expectedAuth AuthInfo
expectedURL string
}{
{
"http://user:pass@localhost:26657",
AuthInfo{Username: "user", Password: "pass"},
"http://localhost:26657",
},
{
"http://user@localhost:26657",
AuthInfo{Username: "user"},
"http://localhost:26657",
},
{
"https://localhost:26657",
AuthInfo{},
"https://localhost:26657",
},
{
"tcp://localhost:26657",
AuthInfo{},
"http://localhost:26657",
},
{
"localhost",
AuthInfo{},
"http://localhost:80",
},
}

for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.rpcURL, func(t *testing.T) {
t.Parallel()

client, err := NewClient(testCase.rpcURL)

require.NoError(t, err)
assert.Equal(t, testCase.expectedURL, client.rpcURL)
assert.Equal(t, testCase.expectedAuth, client.authInfo)
})
}
}

func TestClient_parseRemoteAddr(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -274,3 +323,57 @@ func TestClient_SendBatchRequest(t *testing.T) {
assert.Nil(t, resp.Error)
}
}

func Test_toClientAddressAndAuth(t *testing.T) {
t.Parallel()

testCases := []struct {
remoteAddr string
expectedAddress string
expectedAuth AuthInfo
}{
{
"http://user:pass@example.com",
"http://example.com:80",
AuthInfo{Username: "user", Password: "pass"},
},
{
"http://user@example.com",
"http://example.com:80",
AuthInfo{Username: "user"},
},
{
"https://user:pass@example.com:8080",
"https://example.com:8080",
AuthInfo{Username: "user", Password: "pass"},
},
{
"http://example.com",
"http://example.com:80",
AuthInfo{},
},
{
"example.com",
"http://example.com:80",
AuthInfo{},
},
{
"localhost",
"http://localhost:80",
AuthInfo{},
},
}

for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.remoteAddr, func(t *testing.T) {
t.Parallel()

address, authInfo, err := toClientAddressAndAuth(testCase.remoteAddr)

require.NoError(t, err)
assert.Equal(t, testCase.expectedAddress, address)
assert.Equal(t, testCase.expectedAuth, authInfo)
})
}
}
Loading