diff --git a/transport/httpcommon/httpcommon.go b/transport/httpcommon/httpcommon.go index c703e962..e1bfdc6e 100644 --- a/transport/httpcommon/httpcommon.go +++ b/transport/httpcommon/httpcommon.go @@ -19,6 +19,7 @@ package httpcommon import ( "bytes" + "encoding/base64" "errors" "fmt" "io" @@ -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"` @@ -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 @@ -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) @@ -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() diff --git a/transport/httpcommon/httpcommon_test.go b/transport/httpcommon/httpcommon_test.go index a36308cd..54c598bc 100644 --- a/transport/httpcommon/httpcommon_test.go +++ b/transport/httpcommon/httpcommon_test.go @@ -22,6 +22,7 @@ import ( "fmt" "io" "net/http" + "net/http/httptest" "testing" "time" @@ -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", + 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) + }) + } +}