Skip to content
Merged
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
35 changes: 34 additions & 1 deletion transport/httpcommon/httpcommon.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package httpcommon

import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"io"
Expand All @@ -43,6 +44,9 @@ type HTTPTransportSettings struct {
// TLS provides ssl/tls setup settings
TLS *tlscommon.Config `config:"ssl" yaml:"ssl,omitempty" json:"ssl,omitempty"`

// Auth provides option authorization header settings.
Auth *HTTPAuthorization `config:"auth" yaml:"auth,omitempty" json:"auth,omitempty"`

// Timeout configures the `(http.Transport).Timeout`.
Timeout time.Duration `config:"timeout" yaml:"timeout,omitempty" json:"timeout,omitempty"`

Expand All @@ -57,6 +61,31 @@ type HTTPTransportSettings struct {
// - ConnectionTimeout (currently 'Timeout' is used for both)
}

// HTTPAuthorization provides authorization settings for HTTP clients.
type HTTPAuthorization struct {
Headers []struct {
Key string `config:"key" yaml:"key,omitempty" json:"key,omitempty"`
Value string `config:"value" yaml:"value,omitempty" json:"value,omitempty"`
} `config:"headers" yaml:"headers,omitempty" json:"headers,omitempty"`
Username string `config:"username" yaml:"username,omitempty" json:"username,omitempty"`
Password string `config:"password" yaml:"password,omitempty" json:"password,omitempty"`
APIKey string `config:"api_key" yaml:"api_key,omitempty" json:"api_key,omitempty"`
}

// ToMap transforms returns a map representation of the HTTPAuthorization to use as headers.
func (h *HTTPAuthorization) ToMap() map[string]string {
mp := make(map[string]string)
for _, header := range h.Headers {
mp[header.Key] = header.Value
}
if h.APIKey != "" {
mp["Authorization"] = "ApiKey " + h.APIKey
} else if h.Username != "" && h.Password != "" {
mp["Authorization"] = "Basic " + base64.StdEncoding.EncodeToString([]byte(h.Username+":"+h.Password))
}
return mp
}

// WithKeepaliveSettings options can be used to modify the Keepalive
type WithKeepaliveSettings struct {
Disable bool
Expand Down Expand Up @@ -257,6 +286,10 @@ func (settings *HTTPTransportSettings) RoundTripper(opts ...TransportOption) (ht
rt = settings.httpRoundTripper(tls, dialer, tlsDialer, opts...)
}

if settings.Auth != nil {
opts = append(opts, WithHeaderRoundTripper(settings.Auth.ToMap()))
}

for _, opt := range opts {
if rtOpt, ok := opt.(roundTripperOption); ok {
rt = rtOpt.applyRoundTripper(settings, rt)
Expand All @@ -270,7 +303,7 @@ func (settings *HTTPTransportSettings) httpRoundTripper(
dialer, tlsDialer transport.Dialer,
opts ...TransportOption,
) *http.Transport {
t := http.DefaultTransport.(*http.Transport).Clone()
t := http.DefaultTransport.(*http.Transport).Clone() //nolint:errcheck // always an *http.Transport
t.DialContext = dialer.DialContext
t.DialTLSContext = tlsDialer.DialContext
t.TLSClientConfig = tls.ToConfig()
Expand Down
195 changes: 195 additions & 0 deletions transport/httpcommon/httpcommon_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"testing"
"time"

Expand Down Expand Up @@ -243,3 +244,197 @@ func BenchmarkReadAll(b *testing.B) {
})
}
}

func Test_HTTPTransportSettings_RoundTripper(t *testing.T) {
tests := []struct {
name string
settings *HTTPTransportSettings
handler http.Handler
}{{
name: "with basic auth",
settings: &HTTPTransportSettings{
Auth: &HTTPAuthorization{
Username: "test-user",
Password: "test-password",
},
},
handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
username, password, ok := req.BasicAuth()
if !ok || username != "test-user" || password != "test-password" {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusOK)
}),
}, {
name: "with api key",
settings: &HTTPTransportSettings{
Auth: &HTTPAuthorization{
APIKey: "test-key",
},
},
handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if req.Header.Get("Authorization") != "ApiKey test-key" {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusOK)
}),
}, {
name: "with additional headers",
settings: &HTTPTransportSettings{
Auth: &HTTPAuthorization{
Headers: []struct {
Key string `config:"key" yaml:"key,omitempty" json:"key,omitempty"`
Value string `config:"value" yaml:"value,omitempty" json:"value,omitempty"`
}{{
Key: "X-Authorization",
Value: "test-extra",
}, {
Key: "Other-Header",
Value: "test-value",
}},
},
},
handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if req.Header.Get("X-Authorization") != "test-extra" || req.Header.Get("Other-Header") != "test-value" {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusOK)
}),
}}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
rt, err := tc.settings.RoundTripper()
require.NoError(t, err)

server := httptest.NewServer(tc.handler)
defer server.Close()
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, server.URL, nil)
require.NoError(t, err)

resp, err := rt.RoundTrip(req)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode)
})
}
}

func Test_HTTPAuthorization_ToMap(t *testing.T) {
tests := []struct {
name string
auth *HTTPAuthorization
expect map[string]string
}{{
name: "headers only",
auth: &HTTPAuthorization{
Headers: []struct {
Key string `config:"key" yaml:"key,omitempty" json:"key,omitempty"`
Value string `config:"value" yaml:"value,omitempty" json:"value,omitempty"`
}{{
Key: "header1",
Value: "val1",
}, {
Key: "header2",
Value: "val2",
}},
},
expect: map[string]string{
"header1": "val1",
"header2": "val2",
},
}, {
name: "basic only",
auth: &HTTPAuthorization{
Username: "user",
Password: "pass",
},
expect: map[string]string{
"Authorization": "Basic dXNlcjpwYXNz",
},
}, {
name: "basic with headers",
auth: &HTTPAuthorization{
Headers: []struct {
Key string `config:"key" yaml:"key,omitempty" json:"key,omitempty"`
Value string `config:"value" yaml:"value,omitempty" json:"value,omitempty"`
}{{
Key: "header1",
Value: "val1",
}, {
Key: "header2",
Value: "val2",
}},
Username: "user",
Password: "pass",
},
expect: map[string]string{
"header1": "val1",
"header2": "val2",
"Authorization": "Basic dXNlcjpwYXNz",
},
}, {
name: "api_key only",
auth: &HTTPAuthorization{
APIKey: "apiKeyVal",
},
expect: map[string]string{
"Authorization": "ApiKey apiKeyVal",
},
}, {
name: "api_key with headers",
auth: &HTTPAuthorization{
Headers: []struct {
Key string `config:"key" yaml:"key,omitempty" json:"key,omitempty"`
Value string `config:"value" yaml:"value,omitempty" json:"value,omitempty"`
}{{
Key: "header1",
Value: "val1",
}, {
Key: "header2",
Value: "val2",
}},
APIKey: "apiKeyVal",
},
expect: map[string]string{
"header1": "val1",
"header2": "val2",
"Authorization": "ApiKey apiKeyVal",
},
}, {
name: "api_key preffered over basic",
auth: &HTTPAuthorization{
APIKey: "apiKeyVal",
Username: "user",
Password: "pass",
},
expect: map[string]string{
"Authorization": "ApiKey apiKeyVal",
},
}, {
name: "api_key replaces Authorization custom header",
Comment thread
michel-laterman marked this conversation as resolved.
auth: &HTTPAuthorization{
Headers: []struct {
Key string `config:"key" yaml:"key,omitempty" json:"key,omitempty"`
Value string `config:"value" yaml:"value,omitempty" json:"value,omitempty"`
}{{
Key: "Authorization",
Value: "val1",
}},
APIKey: "apiKeyVal",
},
expect: map[string]string{
"Authorization": "ApiKey apiKeyVal",
},
}}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
results := tc.auth.ToMap()
require.EqualValues(t, tc.expect, results)
})
}
}
Loading