diff --git a/changes/20250530152641.feature b/changes/20250530152641.feature new file mode 100644 index 0000000000..88ed6dfccb --- /dev/null +++ b/changes/20250530152641.feature @@ -0,0 +1 @@ +:sparkles: `http` Add support for HTTP client with headers diff --git a/changes/20250530153502.feature b/changes/20250530153502.feature new file mode 100644 index 0000000000..204f615a86 --- /dev/null +++ b/changes/20250530153502.feature @@ -0,0 +1 @@ +:sparkles: `http` Add utilities for headers diff --git a/utils/go.mod b/utils/go.mod index b16a355d27..b0dce56237 100644 --- a/utils/go.mod +++ b/utils/go.mod @@ -102,4 +102,7 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect ) -tool github.com/dmarkham/enumer +tool ( + github.com/dmarkham/enumer + go.uber.org/mock/mockgen +) diff --git a/utils/http/header_client.go b/utils/http/header_client.go new file mode 100644 index 0000000000..9172e2aa06 --- /dev/null +++ b/utils/http/header_client.go @@ -0,0 +1,138 @@ +package http + +import ( + "io" + "net/http" + "net/url" + "slices" + "strings" + + "github.com/ARM-software/golang-utils/utils/commonerrors" + "github.com/ARM-software/golang-utils/utils/http/headers" +) + +type ClientWithHeaders struct { + client IClient + headers headers.Headers +} + +func newClientWithHeaders(underlyingClient IClient, headerValues ...string) (c *ClientWithHeaders, err error) { + c = &ClientWithHeaders{ + headers: make(headers.Headers), + } + + if underlyingClient == nil { + c.client = NewPlainHTTPClient() + } else { + c.client = underlyingClient + } + + for header := range slices.Chunk(headerValues, 2) { + if len(header) != 2 { + err = commonerrors.New(commonerrors.ErrInvalid, "headers must be supplied in key-value pairs") + return + } + + c.headers.AppendHeader(header[0], header[1]) + } + + return +} + +func NewHTTPClientWithHeaders(headers ...string) (clientWithHeaders IClientWithHeaders, err error) { + return newClientWithHeaders(nil, headers...) +} + +func NewHTTPClientWithEmptyHeaders() (c IClientWithHeaders, err error) { + return NewHTTPClientWithHeaders() +} + +func NewHTTPClientWithUnderlyingClientWithHeaders(underlyingClient IClient, headers ...string) (c IClientWithHeaders, err error) { + return newClientWithHeaders(underlyingClient, headers...) +} + +func NewHTTPClientWithUnderlyingClientWithEmptyHeaders(underlyingClient IClient) (c IClientWithHeaders, err error) { + return newClientWithHeaders(underlyingClient) +} + +func (c *ClientWithHeaders) do(method string, url string, body io.Reader) (*http.Response, error) { + req, err := http.NewRequest(method, url, body) + if err != nil { + return nil, err + } + return c.Do(req) +} + +func (c *ClientWithHeaders) Head(url string) (*http.Response, error) { + return c.do(http.MethodHead, url, nil) +} + +func (c *ClientWithHeaders) Post(url, contentType string, rawBody interface{}) (*http.Response, error) { + b, err := determineBodyReader(rawBody) + if err != nil { + return nil, err + } + req, err := http.NewRequest(http.MethodPost, url, b) + if err != nil { + return nil, err + } + req.Header.Set(headers.HeaderContentType, contentType) // make sure to overrwrite any in the headers + return c.client.Do(req) +} + +func (c *ClientWithHeaders) PostForm(url string, data url.Values) (*http.Response, error) { + rawBody := strings.NewReader(data.Encode()) + return c.Post(url, headers.HeaderXWWWFormURLEncoded, rawBody) +} + +func (c *ClientWithHeaders) StandardClient() *http.Client { + return c.client.StandardClient() +} + +func (c *ClientWithHeaders) Get(url string) (*http.Response, error) { + return c.do(http.MethodGet, url, nil) +} + +func (c *ClientWithHeaders) Do(req *http.Request) (*http.Response, error) { + c.headers.AppendToRequest(req) + return c.client.Do(req) +} + +func (c *ClientWithHeaders) Delete(url string) (*http.Response, error) { + return c.do(http.MethodDelete, url, nil) +} + +func (c *ClientWithHeaders) Put(url string, rawBody interface{}) (*http.Response, error) { + b, err := determineBodyReader(rawBody) + if err != nil { + return nil, err + } + return c.do(http.MethodPut, url, b) +} + +func (c *ClientWithHeaders) Options(url string) (*http.Response, error) { + return c.do(http.MethodOptions, url, nil) +} + +func (c *ClientWithHeaders) Close() error { + c.client.StandardClient().CloseIdleConnections() + return nil +} + +func (c *ClientWithHeaders) AppendHeader(key, value string) { + if c.headers == nil { + c.headers = make(headers.Headers) + } + c.headers.AppendHeader(key, value) +} + +func (c *ClientWithHeaders) RemoveHeader(key string) { + if c.headers == nil { + return + } + delete(c.headers, key) +} + +func (c *ClientWithHeaders) ClearHeaders() { + c.headers = make(headers.Headers) +} diff --git a/utils/http/header_client_test.go b/utils/http/header_client_test.go new file mode 100644 index 0000000000..b3357ec24a --- /dev/null +++ b/utils/http/header_client_test.go @@ -0,0 +1,234 @@ +package http + +import ( + "bytes" + "context" + "fmt" + "net/http" + "strings" + "testing" + "time" + + "github.com/go-logr/logr" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/goleak" + + "github.com/ARM-software/golang-utils/utils/http/headers" + "github.com/ARM-software/golang-utils/utils/http/httptest" +) + +func TestClientWithHeadersWithDifferentBodies(t *testing.T) { + clientsToTest := []struct { + clientName string + client func() IClient + }{ + { + clientName: "default plain client", + client: NewPlainHTTPClient, + }, + { + clientName: "fast client", + client: NewFastPooledClient, + }, + { + clientName: "default pooled client", + client: NewDefaultPooledClient, + }, + { + clientName: "default retryable client", + client: func() IClient { + return NewRetryableClient() + }, + }, + { + clientName: "client with no retry", + client: func() IClient { + return NewConfigurableRetryableClient(DefaultHTTPClientConfiguration()) + }, + }, + { + clientName: "client with basic retry", + client: func() IClient { + return NewConfigurableRetryableClient(DefaultRobustHTTPClientConfiguration()) + }, + }, + { + clientName: "client with exponential backoff", + client: func() IClient { + return NewConfigurableRetryableClient(DefaultRobustHTTPClientConfigurationWithExponentialBackOff()) + }, + }, + { + clientName: "client with linear backoff", + client: func() IClient { + return NewConfigurableRetryableClient(DefaultRobustHTTPClientConfigurationWithLinearBackOff()) + }, + }, + { + clientName: "custom oauth client with retry after but no backoff using oauth2.Token (using custom client function with client == nil)", + client: func() IClient { + return NewConfigurableRetryableOauthClientWithLoggerAndCustomClient(DefaultRobustHTTPClientConfigurationWithRetryAfter(), nil, logr.Discard(), "test-token") + }, + }, + { + clientName: "custom oauth client with retry after but no backoff using oauth2.Token (using custom client function with client == NewPlainHTTPClient())", + client: func() IClient { + return NewConfigurableRetryableOauthClientWithLoggerAndCustomClient(DefaultRobustHTTPClientConfigurationWithRetryAfter(), NewPlainHTTPClient().StandardClient(), logr.Discard(), "test-token") + }, + }, + { + clientName: "nil", + client: func() IClient { + return nil + }, + }, + } + + tests := []struct { + bodyType string + uri string + headers map[string]string + body interface{} + }{ + { + bodyType: "nil", + uri: "/foo/bar", + headers: nil, + body: nil, + }, + { + bodyType: "string", + uri: "/foo/bar", + headers: nil, + body: "some kind of string body", + }, + { + bodyType: "string reader", + uri: "/foo/bar", + headers: nil, + body: strings.NewReader("some kind of string body"), + }, + { + bodyType: "bytes", + uri: "/foo/bar", + headers: nil, + body: []byte("some kind of byte body"), + }, + { + bodyType: "byte buffer", + uri: "/foo/bar", + headers: nil, + body: bytes.NewBuffer([]byte("some kind of byte body")), + }, + { + bodyType: "byte reader", + uri: "/foo/bar", + headers: nil, + body: bytes.NewReader([]byte("some kind of byte body")), + }, + { + bodyType: "nil + single Host", + uri: "/foo/bar", + headers: map[string]string{ + headers.HeaderHost: "example.com", + }, + body: nil, + }, + { + bodyType: "string + WebSocket headers", + uri: "/foo/bar", + headers: map[string]string{ + headers.HeaderConnection: "Upgrade", + headers.HeaderWebsocketVersion: "13", + headers.HeaderWebsocketKey: "dGhlIHNhbXBsZSBub25jZQ==", + headers.HeaderWebsocketProtocol: "chat, superchat", + headers.HeaderWebsocketExtensions: "permessage-deflate; client_max_window_bits", + }, + body: "hello websocket", + }, + { + bodyType: "bytes + Sunset/Deprecation", + uri: "/foo/bar", + headers: map[string]string{ + headers.HeaderSunset: "2025-12-31T23:59:59Z", + headers.HeaderDeprecation: "Tue, 01 Dec 2026 00:00:00 GMT", + }, + body: []byte("payload with deprecation headers"), + }, + { + bodyType: "byte buffer + Link", + uri: "/foo/bar", + headers: map[string]string{ + headers.HeaderLink: `; rel="next", ; rel="help"`, + }, + body: bytes.NewBuffer([]byte("link header test")), + }, + { + bodyType: "reader + TUS upload headers", + uri: "/foo/bar", + headers: map[string]string{ + headers.HeaderTusVersion: "1.0.0", + headers.HeaderUploadOffset: "1024", + headers.HeaderUploadLength: "2048", + headers.HeaderTusResumable: "1.0.0", + }, + body: strings.NewReader("resumable upload content"), + }, + } + + for i := range tests { + test := tests[i] + for j := range clientsToTest { + defer goleak.VerifyNone(t) + rawClient := clientsToTest[j] + var headersSlice []string + for k, v := range tests[i].headers { + headersSlice = append(headersSlice, k, v) + } + client, err := NewHTTPClientWithUnderlyingClientWithHeaders(rawClient.client(), headersSlice...) + require.NoError(t, err) + defer func() { _ = client.Close() }() + require.NotEmpty(t, client.StandardClient()) + t.Run(fmt.Sprintf("local host/Client %v/Body %v", rawClient.clientName, test.bodyType), func(t *testing.T) { + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + port := "28934" + + // Mock server which always responds 201. + httptest.NewTestServer(t, ctx, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }), port) + time.Sleep(100 * time.Millisecond) + url := fmt.Sprintf("http://127.0.0.1:%v/%v", port, test.uri) + resp, err := client.Put(url, test.body) + require.NoError(t, err) + _ = resp.Body.Close() + bodyReader, err := determineBodyReader(test.body) + require.NoError(t, err) + req, err := http.NewRequest("POST", url, bodyReader) + require.NoError(t, err) + resp, err = client.Do(req) + require.NoError(t, err) + _ = resp.Body.Close() + cancel() + time.Sleep(100 * time.Millisecond) + }) + clientStruct, ok := client.(*ClientWithHeaders) + require.True(t, ok) + + clientStruct.ClearHeaders() + assert.Empty(t, clientStruct.headers) + + clientStruct.AppendHeader("hello", "world") + require.NotEmpty(t, clientStruct.headers) + assert.Equal(t, headers.Header{Key: "hello", Value: "world"}, clientStruct.headers["hello"]) + + clientStruct.RemoveHeader("hello") + assert.Empty(t, clientStruct.headers) + + _ = client.Close() + } + } +} diff --git a/utils/http/headers/headers.go b/utils/http/headers/headers.go new file mode 100644 index 0000000000..3974d34429 --- /dev/null +++ b/utils/http/headers/headers.go @@ -0,0 +1,419 @@ +package headers + +import ( + "encoding/base64" + "fmt" + "net/http" + "strings" + + "github.com/go-http-utils/headers" + + "github.com/ARM-software/golang-utils/utils/collection" + "github.com/ARM-software/golang-utils/utils/commonerrors" + "github.com/ARM-software/golang-utils/utils/http/headers/useragent" + "github.com/ARM-software/golang-utils/utils/http/schemes" + "github.com/ARM-software/golang-utils/utils/reflection" +) + +const ( + HeaderWebsocketProtocol = "Sec-WebSocket-Protocol" //https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Sec-WebSocket-Protocol + HeaderWebsocketVersion = "Sec-WebSocket-Version" + HeaderWebsocketKey = "Sec-WebSocket-Key" + HeaderWebsocketAccept = "Sec-WebSocket-Accept" + HeaderWebsocketExtensions = "Sec-WebSocket-Extensions" + HeaderConnection = "Connection" + HeaderVersion = "Version" + HeaderAcceptVersion = "Accept-Version" + HeaderHost = "Host" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/host + // https://greenbytes.de/tech/webdav/draft-ietf-httpapi-deprecation-header-latest.html#sunset + HeaderSunset = "Sunset" // https://datatracker.ietf.org/doc/html/rfc8594 + HeaderDeprecation = "Deprecation" // https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-deprecation-header-02 + HeaderLink = headers.Link // https://datatracker.ietf.org/doc/html/rfc8288 + // TUS Headers https://tus.io/protocols/resumable-upload#headers + HeaderUploadOffset = "Upload-Offset" + HeaderTusVersion = "Tus-Version" + HeaderUploadLength = "Upload-Length" + HeaderTusResumable = "Tus-Resumable" + HeaderTusExtension = "Tus-Extension" + HeaderTusMaxSize = "Tus-Max-Size" + HeaderXHTTPMethodOverride = "X-HTTP-Method-Override" + HeaderXWWWFormURLEncoded = "application/x-www-form-urlencoded" + HeaderContentType = "Content-Type" +) + +var ( + // SafeHeaders corresponds to headers which do not store personal data. + SafeHeaders = []string{ + HeaderVersion, + HeaderAcceptVersion, + HeaderHost, + HeaderSunset, + HeaderDeprecation, + HeaderLink, + HeaderWebsocketVersion, + HeaderWebsocketAccept, + HeaderWebsocketExtensions, + HeaderConnection, + HeaderUploadOffset, + HeaderTusVersion, + HeaderUploadLength, + HeaderTusResumable, + HeaderTusExtension, + HeaderTusMaxSize, + HeaderXHTTPMethodOverride, + headers.Accept, + headers.AcceptCharset, + headers.AcceptEncoding, + headers.AcceptLanguage, + headers.CacheControl, + headers.ContentLength, + headers.ContentMD5, + headers.ContentType, + headers.DoNotTrack, + headers.IfMatch, + headers.IfModifiedSince, + headers.IfNoneMatch, + headers.IfRange, + headers.IfUnmodifiedSince, + headers.MaxForwards, + headers.Pragma, + headers.Range, + headers.Referer, + headers.UserAgent, + headers.TE, + headers.Via, + headers.Warning, + headers.AcceptDatetime, + headers.XRequestedWith, + headers.AccessControlAllowOrigin, + headers.AccessControlAllowMethods, + headers.AccessControlAllowHeaders, + headers.AccessControlAllowCredentials, + headers.AccessControlExposeHeaders, + headers.AccessControlMaxAge, + headers.AccessControlRequestMethod, + headers.AccessControlRequestHeaders, + headers.AcceptPatch, + headers.AcceptRanges, + headers.Allow, + headers.ContentEncoding, + headers.ContentLanguage, + headers.ContentLocation, + headers.ContentDisposition, + headers.ContentRange, + headers.ETag, + headers.Expires, + headers.LastModified, + headers.Link, + headers.Location, + headers.P3P, + headers.ProxyAuthenticate, + headers.Refresh, + headers.RetryAfter, + headers.Server, + headers.TransferEncoding, + headers.Upgrade, + headers.Vary, + headers.XPoweredBy, + headers.XHTTPMethodOverride, + headers.XRatelimitLimit, + headers.XRatelimitRemaining, + headers.XRatelimitReset, + } +) + +type Header struct { + Key string + Value string +} + +func (h *Header) String() string { + return fmt.Sprintf("%v: %v", h.Key, h.Value) +} + +type Headers map[string]Header + +func (hs Headers) AppendHeader(key, value string) { + hs.Append(&Header{ + Key: key, + Value: value, + }) +} + +func (hs Headers) Append(h *Header) { + hs[h.Key] = *h +} + +func (hs Headers) Has(h *Header) bool { + if h == nil { + return false + } + return hs.HasHeader(h.Key) +} + +func (hs Headers) HasHeader(key string) bool { + _, found := hs[key] + return found +} + +func (hs Headers) Empty() bool { + return len(hs) == 0 +} + +func (hs Headers) AppendToResponse(w http.ResponseWriter) { + if hs != nil && !hs.Empty() { + for k, v := range hs { + w.Header().Set(k, v.Value) + } + } +} + +func (hs Headers) AppendToRequest(r *http.Request) { + if hs != nil && !hs.Empty() { + for k, v := range hs { + r.Header.Set(k, v.Value) + } + } +} + +func NewHeaders() *Headers { + return &Headers{} +} + +// ParseAuthorizationHeader fetches the `Authorization` header and parses it. +func ParseAuthorizationHeader(r *http.Request) (string, string, error) { + return ParseAuthorisationValue(FetchWebsocketAuthorisation(r)) +} + +// ParseAuthorisationValue determines the different element of a `Authorization` header value. +// and makes sure it has 2 parts +func ParseAuthorisationValue(authHeader string) (scheme string, token string, err error) { + if reflection.IsEmpty(authHeader) { + err = commonerrors.New(commonerrors.ErrUndefined, "authorization header is not set") + return + } + parts := strings.Fields(authHeader) + if len(parts) != 2 { + err = commonerrors.New(commonerrors.ErrInvalid, "`Authorization` header contains incorrect number of parts") + return + } + scheme = parts[0] + token = parts[1] + err = checkSchemeSupport(scheme) + return +} + +func checkSchemeSupport(scheme string) (err error) { + schemeStr := strings.TrimSpace(scheme) + if schemeStr == "" { + err = commonerrors.UndefinedVariable("authorisation scheme") + return + } + _, found := collection.FindInSlice(false, schemes.HTTPAuthorisationSchemes, scheme) + if !found { + err = commonerrors.Newf(commonerrors.ErrUnsupported, "supported `Authorization` schemes are %v", schemes.HTTPAuthorisationSchemes) + } + return err +} + +// FetchAuthorisation fetches the value of `Authorization` header. +func FetchAuthorisation(r *http.Request) string { + if r == nil { + return "" + } + authHeader := r.Header.Get(headers.Authorization) + return authHeader +} + +// FetchWebSocketSubProtocols fetches the values of `Sec-WebSocket-Protocol` header https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Sec-WebSocket-Protocol. +func FetchWebSocketSubProtocols(r *http.Request) (subProtocols []string) { + if r == nil { + return + } + subProtocolsHeaders := r.Header.Values(HeaderWebsocketProtocol) + if len(subProtocolsHeaders) == 0 { + return + } + for i := range subProtocolsHeaders { + subProtocols = append(subProtocols, collection.ParseCommaSeparatedList(subProtocolsHeaders[i])...) + } + return +} + +// FetchWebsocketAuthorisation tries to find the authorisation header values in the case of websocket +// It will look in the `Authorization` header but will also look at some workaround suggested [here](https://ably.com/blog/websocket-authentication#:~:text=While%20the%20WebSocket%20browser%20API,token%20in%20the%20request%20header) and [there](https://github.com/kubernetes/kubernetes/pull/47740) +// If found using the workarounds, it will set the Authorization header with the determined value +func FetchWebsocketAuthorisation(r *http.Request) (authorisationHeader string) { + if r == nil { + return + } + authorisationHeader = FetchAuthorisation(r) + if !reflection.IsEmpty(authorisationHeader) { + return + } + subProtocols := FetchWebSocketSubProtocols(r) + if len(subProtocols) == 0 { + return + } + i, found := collection.FindInSlice(false, subProtocols, headers.Authorization) + if found { + if i < len(subProtocols)-1 { + authorisationHeader = subProtocols[i+1] + if decoded, err := decodeBase64Token(authorisationHeader); err == nil { + authorisationHeader = decoded + } + _ = SetAuthorisationIfNotPresent(r, authorisationHeader) + return + } + } + // see https://github.com/kubernetes/kubernetes/pull/47740 + _, found = collection.FindInSlice(false, subProtocols, "base64.binary.k8s.io") + if found { + for j := range subProtocols { + token := strings.TrimPrefix(subProtocols[j], "base64url.bearer.authorization.k8s.io.") + if token != subProtocols[j] { + data, err := decodeBase64Token(token) + if err == nil { + authorisationHeader = data + _ = SetAuthorisationIfNotPresent(r, authorisationHeader) + return + } + } + } + + } + return +} + +func decodeBase64Token(token string) (decoded string, err error) { + data, err := base64.URLEncoding.DecodeString(token) + if err == nil { + decoded = string(data) + return + } + data, err = base64.RawURLEncoding.DecodeString(token) + if err == nil { + decoded = string(data) + return + } + data, err = base64.StdEncoding.DecodeString(token) + if err == nil { + decoded = string(data) + return + } + data, err = base64.RawStdEncoding.DecodeString(token) + if err == nil { + decoded = string(data) + } + return +} + +// SetAuthorisationIfNotPresent sets the value of the `Authorization` header if not already set. +func SetAuthorisationIfNotPresent(r *http.Request, authorisation string) (err error) { + if strings.TrimSpace(FetchAuthorisation(r)) == "" { + err = SetAuthorisation(r, authorisation) + } + return +} + +// SetAuthorisation sets the value of the `Authorization` header. +func SetAuthorisation(r *http.Request, authorisation string) (err error) { + if r == nil { + err = commonerrors.UndefinedVariable("request") + return + } + if reflection.IsEmpty(authorisation) { + err = commonerrors.UndefinedVariable("authorisation value") + return + } + r.Header.Set(headers.Authorization, authorisation) + return +} + +// SetAuthorisationToken defines the `Authorization` header. +func SetAuthorisationToken(r *http.Request, scheme, token string) (err error) { + value, err := GenerateAuthorizationHeaderValue(scheme, token) + if err != nil { + return + } + err = SetAuthorisation(r, value) + return +} + +func GenerateAuthorizationHeaderValue(scheme string, token string) (value string, err error) { + err = checkSchemeSupport(scheme) + if err != nil { + return + } + if reflection.IsEmpty(token) { + err = commonerrors.UndefinedVariable("authorisation token") + return + } + value = fmt.Sprintf("%s %s", strings.TrimSpace(scheme), token) + return +} + +// AddToUserAgent adds some information to the `User Agent`. +func AddToUserAgent(r *http.Request, elements ...string) (err error) { + if r == nil { + err = fmt.Errorf("%w: missing request", commonerrors.ErrUndefined) + return + } + if reflection.IsEmpty(elements) { + err = fmt.Errorf("%w: empty elements to add", commonerrors.ErrUndefined) + return + } + r.Header.Set(headers.UserAgent, useragent.AddValuesToUserAgent(FetchUserAgent(r), elements...)) + return +} + +// AddProductInformationToUserAgent adds some product information to the `User Agent`. +func AddProductInformationToUserAgent(r *http.Request, product, productVersion, comment string) (err error) { + productStr, err := useragent.GenerateUserAgentValue(product, productVersion, comment) + if err != nil { + return + } + err = AddToUserAgent(r, productStr) + return +} + +// FetchUserAgent fetches the value of the `User-Agent` header. +func FetchUserAgent(r *http.Request) string { + authHeader := r.UserAgent() + return authHeader +} + +// SetLocationHeaders sets the location errors for `POST` requests. +func SetLocationHeaders(w http.ResponseWriter, location string) { + h := NewHeaders() + h.AppendHeader(headers.Location, location) + h.AppendHeader(headers.ContentLocation, location) + h.AppendToResponse(w) +} + +// SetContentLocationHeader sets the `Content-Location` header +func SetContentLocationHeader(w http.ResponseWriter, location string) { + w.Header().Set(headers.ContentLocation, location) +} + +// CreateLinkHeader creates a link header for a relation and mimetype +func CreateLinkHeader(link, relation, contentType string) string { + return fmt.Sprintf("<%v>; rel=\"%v\"; type=\"%v\"", link, relation, contentType) +} + +// SanitiseHeaders sanitises a collection of request headers not to include any with personal data +func SanitiseHeaders(requestHeader *http.Header) *Headers { + if requestHeader == nil { + return nil + } + aHeaders := NewHeaders() + for i := range SafeHeaders { + safeHeader := SafeHeaders[i] + rHeader := requestHeader.Get(safeHeader) + if !reflection.IsEmpty(rHeader) { + aHeaders.AppendHeader(safeHeader, rHeader) + } + } + + return aHeaders +} diff --git a/utils/http/headers/headers_test.go b/utils/http/headers/headers_test.go new file mode 100644 index 0000000000..e3149d1efa --- /dev/null +++ b/utils/http/headers/headers_test.go @@ -0,0 +1,201 @@ +package headers + +import ( + "encoding/base64" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/go-faker/faker/v4" + "github.com/go-http-utils/headers" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ARM-software/golang-utils/utils/commonerrors" + "github.com/ARM-software/golang-utils/utils/commonerrors/errortest" + "github.com/ARM-software/golang-utils/utils/http/schemes" +) + +func TestParseAuthorizationHeader(t *testing.T) { + random, err := faker.RandomInt(0, len(schemes.HTTPAuthorisationSchemes)-1, 1) + require.NoError(t, err) + fakescheme := schemes.HTTPAuthorisationSchemes[random[0]] + r, err := http.NewRequest(http.MethodGet, faker.URL(), nil) + require.NoError(t, err) + var scheme, token string + t.Run("empty authorization header", func(t *testing.T) { + scheme, token, err := ParseAuthorizationHeader(r) + require.Error(t, err) + errortest.RequireError(t, err, commonerrors.ErrUndefined) + assert.Empty(t, scheme) + assert.Empty(t, token) + }) + t.Run("invalid authorization header", func(t *testing.T) { + require.NoError(t, SetAuthorisation(r, faker.Word())) + scheme, token, err = ParseAuthorizationHeader(r) + require.Error(t, err) + assert.True(t, commonerrors.Any(err, commonerrors.ErrInvalid)) + assert.Empty(t, scheme) + assert.Empty(t, token) + require.NoError(t, SetAuthorisation(r, faker.Sentence())) + scheme, token, err = ParseAuthorizationHeader(r) + require.Error(t, err) + errortest.RequireError(t, err, commonerrors.ErrInvalid) + assert.Empty(t, scheme) + assert.Empty(t, token) + }) + faketoken := faker.Password() + t.Run("valid authorization header", func(t *testing.T) { + require.NoError(t, SetAuthorisationToken(r, fakescheme, faketoken)) + scheme, token, err = ParseAuthorizationHeader(r) + require.NoError(t, err) + assert.Equal(t, fakescheme, scheme) + assert.Equal(t, faketoken, token) + }) + t.Run("valid authorisation header for websocket (workaround1)", func(t *testing.T) { + value, err := GenerateAuthorizationHeaderValue(fakescheme, faketoken) + require.NoError(t, err) + tests := []struct { + encoded string + }{ + { + encoded: base64.StdEncoding.EncodeToString([]byte(value)), + }, + { + encoded: base64.URLEncoding.EncodeToString([]byte(value)), + }, + { + encoded: base64.RawStdEncoding.EncodeToString([]byte(value)), + }, + { + encoded: base64.RawURLEncoding.EncodeToString([]byte(value)), + }, + } + for i := range tests { + test := tests[i] + t.Run("base64 encoding", func(t *testing.T) { + r, err = http.NewRequest(http.MethodGet, faker.URL(), nil) + require.NoError(t, err) + r.Header.Add(HeaderWebsocketProtocol, "base64.binary.k8s.io") + r.Header.Add(HeaderWebsocketProtocol, fmt.Sprintf("base64url.bearer.authorization.k8s.io.%v", test.encoded)) + scheme, token, err = ParseAuthorizationHeader(r) + require.NoError(t, err) + assert.Equal(t, fakescheme, scheme) + assert.Equal(t, faketoken, token) + // now the value should also be set in the authorization header + scheme, token, err = ParseAuthorizationHeader(r) + require.NoError(t, err) + assert.Equal(t, fakescheme, scheme) + assert.Equal(t, faketoken, token) + }) + } + }) + t.Run("valid authorisation header for websocket (workaround2)", func(t *testing.T) { + r, err = http.NewRequest(http.MethodGet, faker.URL(), nil) + require.NoError(t, err) + r.Header.Add(HeaderWebsocketProtocol, fmt.Sprintf("%v, %v %v", headers.Authorization, fakescheme, faketoken)) + scheme, token, err = ParseAuthorizationHeader(r) + require.NoError(t, err) + assert.Equal(t, fakescheme, scheme) + assert.Equal(t, faketoken, token) + // now the value should also be set in the authorization header + scheme, token, err = ParseAuthorizationHeader(r) + require.NoError(t, err) + assert.Equal(t, fakescheme, scheme) + assert.Equal(t, faketoken, token) + }) + t.Run("valid authorisation header for websocket (workaround3)", func(t *testing.T) { + tokenString, err := GenerateAuthorizationHeaderValue(fakescheme, faketoken) + require.NoError(t, err) + tests := []struct { + encoded string + }{ + { + encoded: base64.StdEncoding.EncodeToString([]byte(tokenString)), + }, + { + encoded: base64.URLEncoding.EncodeToString([]byte(tokenString)), + }, + { + encoded: base64.RawStdEncoding.EncodeToString([]byte(tokenString)), + }, + { + encoded: base64.RawURLEncoding.EncodeToString([]byte(tokenString)), + }, + } + for i := range tests { + test := tests[i] + t.Run("base64 encoding", func(t *testing.T) { + r, err = http.NewRequest(http.MethodGet, faker.URL(), nil) + require.NoError(t, err) + r.Header.Add(HeaderWebsocketProtocol, fmt.Sprintf("%v, %v", headers.Authorization, test.encoded)) + scheme, token, err = ParseAuthorizationHeader(r) + require.NoError(t, err) + assert.Equal(t, fakescheme, scheme) + assert.Equal(t, faketoken, token) + // now the value should also be set in the authorization header + scheme, token, err = ParseAuthorizationHeader(r) + require.NoError(t, err) + assert.Equal(t, fakescheme, scheme) + assert.Equal(t, faketoken, token) + }) + } + }) +} + +func TestAddProductInformationToUserAgent(t *testing.T) { + r, err := http.NewRequest(http.MethodGet, faker.URL(), nil) + require.NoError(t, err) + assert.Empty(t, FetchUserAgent(r)) + require.NoError(t, AddProductInformationToUserAgent(r, faker.Word(), faker.IPv4(), "")) + assert.NotEmpty(t, FetchUserAgent(r)) + require.NoError(t, AddProductInformationToUserAgent(r, faker.Word(), faker.IPv4(), faker.Sentence())) + assert.NotEmpty(t, FetchUserAgent(r)) + fmt.Println(FetchUserAgent(r)) +} + +func TestSetLocationHeaders(t *testing.T) { + w := httptest.NewRecorder() + assert.Empty(t, w.Header().Get(headers.Location)) + assert.Empty(t, w.Header().Get(headers.ContentLocation)) + location := faker.URL() + SetLocationHeaders(w, location) + assert.Equal(t, location, w.Header().Get(headers.Location)) + assert.Equal(t, location, w.Header().Get(headers.ContentLocation)) +} + +func TestSanitiseHeaders(t *testing.T) { + header := &http.Header{} + t.Run("empty", func(t *testing.T) { + require.Empty(t, SanitiseHeaders(nil)) + require.Empty(t, SanitiseHeaders(header)) + }) + t.Run("valid", func(t *testing.T) { + header.Add(headers.AcceptEncoding, "gzip") + actual := SanitiseHeaders(header) + require.NotNil(t, actual) + assert.True(t, actual.HasHeader( + headers.AcceptEncoding)) + header.Add(headers.Accept, "1.0.0") + actual = SanitiseHeaders(header) + assert.True(t, actual.HasHeader( + headers.AcceptEncoding)) + assert.True(t, actual.HasHeader( + headers.Accept)) + }) + t.Run("redact headers", func(t *testing.T) { + header.Add(headers.Authorization, faker.Password()) + header.Add(HeaderWebsocketProtocol, faker.Password()) + actual := SanitiseHeaders(header) + assert.True(t, actual.HasHeader( + headers.AcceptEncoding)) + assert.True(t, actual.HasHeader( + headers.Accept)) + assert.False(t, actual.HasHeader( + headers.Authorization)) + assert.False(t, actual.HasHeader( + HeaderWebsocketProtocol)) + }) + +} diff --git a/utils/http/headers/interfaces.go b/utils/http/headers/interfaces.go new file mode 100644 index 0000000000..5b810cfde9 --- /dev/null +++ b/utils/http/headers/interfaces.go @@ -0,0 +1,16 @@ +package headers + +import "net/http" + +//nolint:goimport +//go:generate go tool mockgen -destination=../../mocks/mock_$GOPACKAGE.go -package=mocks github.com/ARM-software/golang-utils/utils/http/$GOPACKAGE IHTTPHeaders + +// IHTTPHeaders defines an HTTP header. +type IHTTPHeaders interface { + AppendHeader(key, value string) + Append(h *Header) + Has(h *Header) bool + HasHeader(key string) bool + Empty() bool + AppendToResponse(w http.ResponseWriter) +} diff --git a/utils/http/useragent/useragent.go b/utils/http/headers/useragent/useragent.go similarity index 100% rename from utils/http/useragent/useragent.go rename to utils/http/headers/useragent/useragent.go diff --git a/utils/http/headers/useragent/useragent/useragent.go b/utils/http/headers/useragent/useragent/useragent.go new file mode 100644 index 0000000000..7f4bcdd193 --- /dev/null +++ b/utils/http/headers/useragent/useragent/useragent.go @@ -0,0 +1,44 @@ +package useragent + +import ( + "fmt" + "strings" + + "github.com/ARM-software/golang-utils/utils/commonerrors" + "github.com/ARM-software/golang-utils/utils/reflection" +) + +// AddValuesToUserAgent extends a user agent string with new elements. See https://en.wikipedia.org/wiki/User-Agent_header#Format_for_human-operated_web_browsers +func AddValuesToUserAgent(userAgent string, elements ...string) (newUserAgent string) { + if len(elements) == 0 { + newUserAgent = userAgent + return + } + newUserAgent = strings.Join(elements, " ") + newUserAgent = strings.TrimSpace(newUserAgent) + if newUserAgent == "" { + newUserAgent = userAgent + return + } + if !reflection.IsEmpty(userAgent) { + newUserAgent = fmt.Sprintf("%v %v", userAgent, newUserAgent) + } + return +} + +// GenerateUserAgentValue generates a user agent value. See https://en.wikipedia.org/wiki/User-Agent_header#Format_for_human-operated_web_browsers +func GenerateUserAgentValue(product string, productVersion string, comment string) (userAgent string, err error) { + if reflection.IsEmpty(product) { + err = commonerrors.UndefinedVariable("product") + return + } + if reflection.IsEmpty(productVersion) { + err = commonerrors.UndefinedVariable("product version") + return + } + userAgent = fmt.Sprintf("%v/%v", product, productVersion) + if !reflection.IsEmpty(comment) { + userAgent = fmt.Sprintf("%v (%v)", userAgent, comment) + } + return +} diff --git a/utils/http/useragent/useragent_test.go b/utils/http/headers/useragent/useragent_test.go similarity index 100% rename from utils/http/useragent/useragent_test.go rename to utils/http/headers/useragent/useragent_test.go diff --git a/utils/http/interfaces.go b/utils/http/interfaces.go index a1c485d190..e843212639 100644 --- a/utils/http/interfaces.go +++ b/utils/http/interfaces.go @@ -26,7 +26,7 @@ import ( "github.com/hashicorp/go-retryablehttp" ) -//go:generate mockgen -destination=../mocks/mock_$GOPACKAGE.go -package=mocks github.com/ARM-software/golang-utils/utils/$GOPACKAGE IClient,IRetryWaitPolicy +//go:generate mockgen -destination=../mocks/mock_$GOPACKAGE.go -package=mocks github.com/ARM-software/golang-utils/utils/$GOPACKAGE IClient,IRetryWaitPolicy,IClientWithHeaders // IClient defines an HTTP client similar to http.Client but without shared state with other clients used in the same program. // See https://github.com/hashicorp/go-cleanhttp for more details. @@ -65,3 +65,11 @@ type IRetryableClient interface { IClient UnderlyingClient() *retryablehttp.Client } + +// IClientWithHeaders is a normal client that can have headers attached to it. These headers will be used in all requests +type IClientWithHeaders interface { + IClient + AppendHeader(key, value string) + RemoveHeader(key string) + ClearHeaders() +} diff --git a/utils/http/request.go b/utils/http/request.go index 385491d194..e56d0996e1 100644 --- a/utils/http/request.go +++ b/utils/http/request.go @@ -9,63 +9,31 @@ import ( "github.com/ARM-software/golang-utils/utils/commonerrors" configUtils "github.com/ARM-software/golang-utils/utils/config" + "github.com/ARM-software/golang-utils/utils/http/schemes" "github.com/ARM-software/golang-utils/utils/reflection" ) const ( - AuthorisationSchemeToken = "Token" - AuthorisationSchemeBasic = "Basic" - AuthorisationSchemeBearer = "Bearer" - AuthorisationSchemeConcealed = "Concealed" - AuthorisationSchemeDigest = "Digest" - AuthorisationSchemeDPoP = "DPoP" - AuthorisationSchemeGNAP = "GNAP" - AuthorisationSchemeHOBA = "HOBA" - AuthorisationSchemeMutual = "Mutual" - AuthorisationSchemeNegotiate = "Negotiate" - AuthorisationSchemeOAuth = "OAuth" - AuthorisationSchemePrivateToken = "PrivateToken" - AuthorisationSchemeSCRAMSSHA1 = "SCRAM-SHA-1" - AuthorisationSchemeSCRAMSHA256 = "SCRAM-SHA-256" - AuthorisationSchemeVapid = "vapid" + AuthorisationSchemeToken = schemes.AuthorisationSchemeToken + AuthorisationSchemeBasic = schemes.AuthorisationSchemeBasic + AuthorisationSchemeBearer = schemes.AuthorisationSchemeBearer + AuthorisationSchemeConcealed = schemes.AuthorisationSchemeConcealed + AuthorisationSchemeDigest = schemes.AuthorisationSchemeDigest + AuthorisationSchemeDPoP = schemes.AuthorisationSchemeDPoP + AuthorisationSchemeGNAP = schemes.AuthorisationSchemeGNAP + AuthorisationSchemeHOBA = schemes.AuthorisationSchemeHOBA + AuthorisationSchemeMutual = schemes.AuthorisationSchemeMutual + AuthorisationSchemeNegotiate = schemes.AuthorisationSchemeNegotiate + AuthorisationSchemeOAuth = schemes.AuthorisationSchemeOAuth + AuthorisationSchemePrivateToken = schemes.AuthorisationSchemePrivateToken + AuthorisationSchemeSCRAMSSHA1 = schemes.AuthorisationSchemeSCRAMSHA1 + AuthorisationSchemeSCRAMSHA256 = schemes.AuthorisationSchemeSCRAMSHA256 + AuthorisationSchemeVapid = schemes.AuthorisationSchemeVapid ) var ( - // HTTPAuthorisationSchemes lists all supported authorisation schemes. See https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml - HTTPAuthorisationSchemes = []string{ - AuthorisationSchemeToken, - AuthorisationSchemeBasic, - AuthorisationSchemeBearer, - AuthorisationSchemeConcealed, - AuthorisationSchemeDigest, - AuthorisationSchemeDPoP, - AuthorisationSchemeGNAP, - AuthorisationSchemeHOBA, - AuthorisationSchemeMutual, - AuthorisationSchemeNegotiate, - AuthorisationSchemeOAuth, - AuthorisationSchemePrivateToken, - AuthorisationSchemeSCRAMSSHA1, - AuthorisationSchemeSCRAMSHA256, - AuthorisationSchemeVapid, - } - inAuthSchemes = []any{ - AuthorisationSchemeToken, - AuthorisationSchemeBasic, - AuthorisationSchemeBearer, - AuthorisationSchemeConcealed, - AuthorisationSchemeDigest, - AuthorisationSchemeDPoP, - AuthorisationSchemeGNAP, - AuthorisationSchemeHOBA, - AuthorisationSchemeMutual, - AuthorisationSchemeNegotiate, - AuthorisationSchemeOAuth, - AuthorisationSchemePrivateToken, - AuthorisationSchemeSCRAMSSHA1, - AuthorisationSchemeSCRAMSHA256, - AuthorisationSchemeVapid, - } + HTTPAuthorisationSchemes = schemes.HTTPAuthorisationSchemes + inAuthSchemes = schemes.InAuthSchemes ) // Auth defines a typical HTTP client authentication/authorisation configuration diff --git a/utils/http/schemes/schemes.go b/utils/http/schemes/schemes.go new file mode 100644 index 0000000000..2aaa90657e --- /dev/null +++ b/utils/http/schemes/schemes.go @@ -0,0 +1,57 @@ +package schemes + +const ( + AuthorisationSchemeToken = "Token" + AuthorisationSchemeBasic = "Basic" + AuthorisationSchemeBearer = "Bearer" + AuthorisationSchemeConcealed = "Concealed" + AuthorisationSchemeDigest = "Digest" + AuthorisationSchemeDPoP = "DPoP" + AuthorisationSchemeGNAP = "GNAP" + AuthorisationSchemeHOBA = "HOBA" + AuthorisationSchemeMutual = "Mutual" + AuthorisationSchemeNegotiate = "Negotiate" + AuthorisationSchemeOAuth = "OAuth" + AuthorisationSchemePrivateToken = "PrivateToken" + AuthorisationSchemeSCRAMSHA1 = "SCRAM-SHA-1" + AuthorisationSchemeSCRAMSHA256 = "SCRAM-SHA-256" + AuthorisationSchemeVapid = "vapid" +) + +var ( + // HTTPAuthorisationSchemes lists all supported authorisation schemes. See https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml + HTTPAuthorisationSchemes = []string{ + AuthorisationSchemeToken, + AuthorisationSchemeBasic, + AuthorisationSchemeBearer, + AuthorisationSchemeConcealed, + AuthorisationSchemeDigest, + AuthorisationSchemeDPoP, + AuthorisationSchemeGNAP, + AuthorisationSchemeHOBA, + AuthorisationSchemeMutual, + AuthorisationSchemeNegotiate, + AuthorisationSchemeOAuth, + AuthorisationSchemePrivateToken, + AuthorisationSchemeSCRAMSHA1, + AuthorisationSchemeSCRAMSHA256, + AuthorisationSchemeVapid, + } + InAuthSchemes = []any{ + AuthorisationSchemeToken, + AuthorisationSchemeBasic, + AuthorisationSchemeBearer, + AuthorisationSchemeConcealed, + AuthorisationSchemeDigest, + AuthorisationSchemeDPoP, + AuthorisationSchemeGNAP, + AuthorisationSchemeHOBA, + AuthorisationSchemeMutual, + AuthorisationSchemeNegotiate, + AuthorisationSchemeOAuth, + AuthorisationSchemePrivateToken, + AuthorisationSchemeSCRAMSHA1, + AuthorisationSchemeSCRAMSHA256, + AuthorisationSchemeVapid, + } +) diff --git a/utils/mocks/mock_headers.go b/utils/mocks/mock_headers.go new file mode 100644 index 0000000000..576da89bf7 --- /dev/null +++ b/utils/mocks/mock_headers.go @@ -0,0 +1,120 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ARM-software/golang-utils/utils/http/headers (interfaces: IHTTPHeaders) +// +// Generated by this command: +// +// mockgen -destination=../../mocks/mock_headers.go -package=mocks github.com/ARM-software/golang-utils/utils/http/headers IHTTPHeaders +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + http "net/http" + reflect "reflect" + + headers "github.com/ARM-software/golang-utils/utils/http/headers" + gomock "go.uber.org/mock/gomock" +) + +// MockIHTTPHeaders is a mock of IHTTPHeaders interface. +type MockIHTTPHeaders struct { + ctrl *gomock.Controller + recorder *MockIHTTPHeadersMockRecorder + isgomock struct{} +} + +// MockIHTTPHeadersMockRecorder is the mock recorder for MockIHTTPHeaders. +type MockIHTTPHeadersMockRecorder struct { + mock *MockIHTTPHeaders +} + +// NewMockIHTTPHeaders creates a new mock instance. +func NewMockIHTTPHeaders(ctrl *gomock.Controller) *MockIHTTPHeaders { + mock := &MockIHTTPHeaders{ctrl: ctrl} + mock.recorder = &MockIHTTPHeadersMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockIHTTPHeaders) EXPECT() *MockIHTTPHeadersMockRecorder { + return m.recorder +} + +// Append mocks base method. +func (m *MockIHTTPHeaders) Append(h *headers.Header) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Append", h) +} + +// Append indicates an expected call of Append. +func (mr *MockIHTTPHeadersMockRecorder) Append(h any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Append", reflect.TypeOf((*MockIHTTPHeaders)(nil).Append), h) +} + +// AppendHeader mocks base method. +func (m *MockIHTTPHeaders) AppendHeader(key, value string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "AppendHeader", key, value) +} + +// AppendHeader indicates an expected call of AppendHeader. +func (mr *MockIHTTPHeadersMockRecorder) AppendHeader(key, value any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AppendHeader", reflect.TypeOf((*MockIHTTPHeaders)(nil).AppendHeader), key, value) +} + +// AppendToResponse mocks base method. +func (m *MockIHTTPHeaders) AppendToResponse(w http.ResponseWriter) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "AppendToResponse", w) +} + +// AppendToResponse indicates an expected call of AppendToResponse. +func (mr *MockIHTTPHeadersMockRecorder) AppendToResponse(w any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AppendToResponse", reflect.TypeOf((*MockIHTTPHeaders)(nil).AppendToResponse), w) +} + +// Empty mocks base method. +func (m *MockIHTTPHeaders) Empty() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Empty") + ret0, _ := ret[0].(bool) + return ret0 +} + +// Empty indicates an expected call of Empty. +func (mr *MockIHTTPHeadersMockRecorder) Empty() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Empty", reflect.TypeOf((*MockIHTTPHeaders)(nil).Empty)) +} + +// Has mocks base method. +func (m *MockIHTTPHeaders) Has(h *headers.Header) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Has", h) + ret0, _ := ret[0].(bool) + return ret0 +} + +// Has indicates an expected call of Has. +func (mr *MockIHTTPHeadersMockRecorder) Has(h any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Has", reflect.TypeOf((*MockIHTTPHeaders)(nil).Has), h) +} + +// HasHeader mocks base method. +func (m *MockIHTTPHeaders) HasHeader(key string) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HasHeader", key) + ret0, _ := ret[0].(bool) + return ret0 +} + +// HasHeader indicates an expected call of HasHeader. +func (mr *MockIHTTPHeadersMockRecorder) HasHeader(key any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasHeader", reflect.TypeOf((*MockIHTTPHeaders)(nil).HasHeader), key) +} diff --git a/utils/mocks/mock_http.go b/utils/mocks/mock_http.go index fe67077ad4..a47042fbc8 100644 --- a/utils/mocks/mock_http.go +++ b/utils/mocks/mock_http.go @@ -1,9 +1,9 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/ARM-software/golang-utils/utils/http (interfaces: IClient,IRetryWaitPolicy) +// Source: github.com/ARM-software/golang-utils/utils/http (interfaces: IClient,IRetryWaitPolicy,IClientWithHeaders) // // Generated by this command: // -// mockgen -destination=../mocks/mock_http.go -package=mocks github.com/ARM-software/golang-utils/utils/http IClient,IRetryWaitPolicy +// mockgen -destination=../mocks/mock_http.go -package=mocks github.com/ARM-software/golang-utils/utils/http IClient,IRetryWaitPolicy,IClientWithHeaders // // Package mocks is a generated GoMock package. @@ -227,3 +227,211 @@ func (mr *MockIRetryWaitPolicyMockRecorder) Apply(min, max, attemptNum, resp any mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Apply", reflect.TypeOf((*MockIRetryWaitPolicy)(nil).Apply), min, max, attemptNum, resp) } + +// MockIClientWithHeaders is a mock of IClientWithHeaders interface. +type MockIClientWithHeaders struct { + ctrl *gomock.Controller + recorder *MockIClientWithHeadersMockRecorder + isgomock struct{} +} + +// MockIClientWithHeadersMockRecorder is the mock recorder for MockIClientWithHeaders. +type MockIClientWithHeadersMockRecorder struct { + mock *MockIClientWithHeaders +} + +// NewMockIClientWithHeaders creates a new mock instance. +func NewMockIClientWithHeaders(ctrl *gomock.Controller) *MockIClientWithHeaders { + mock := &MockIClientWithHeaders{ctrl: ctrl} + mock.recorder = &MockIClientWithHeadersMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockIClientWithHeaders) EXPECT() *MockIClientWithHeadersMockRecorder { + return m.recorder +} + +// AppendHeader mocks base method. +func (m *MockIClientWithHeaders) AppendHeader(key, value string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "AppendHeader", key, value) +} + +// AppendHeader indicates an expected call of AppendHeader. +func (mr *MockIClientWithHeadersMockRecorder) AppendHeader(key, value any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AppendHeader", reflect.TypeOf((*MockIClientWithHeaders)(nil).AppendHeader), key, value) +} + +// ClearHeaders mocks base method. +func (m *MockIClientWithHeaders) ClearHeaders() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "ClearHeaders") +} + +// ClearHeaders indicates an expected call of ClearHeaders. +func (mr *MockIClientWithHeadersMockRecorder) ClearHeaders() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClearHeaders", reflect.TypeOf((*MockIClientWithHeaders)(nil).ClearHeaders)) +} + +// Close mocks base method. +func (m *MockIClientWithHeaders) Close() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Close") + ret0, _ := ret[0].(error) + return ret0 +} + +// Close indicates an expected call of Close. +func (mr *MockIClientWithHeadersMockRecorder) Close() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockIClientWithHeaders)(nil).Close)) +} + +// Delete mocks base method. +func (m *MockIClientWithHeaders) Delete(url string) (*http.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", url) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Delete indicates an expected call of Delete. +func (mr *MockIClientWithHeadersMockRecorder) Delete(url any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockIClientWithHeaders)(nil).Delete), url) +} + +// Do mocks base method. +func (m *MockIClientWithHeaders) Do(req *http.Request) (*http.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Do", req) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Do indicates an expected call of Do. +func (mr *MockIClientWithHeadersMockRecorder) Do(req any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockIClientWithHeaders)(nil).Do), req) +} + +// Get mocks base method. +func (m *MockIClientWithHeaders) Get(url string) (*http.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", url) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockIClientWithHeadersMockRecorder) Get(url any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockIClientWithHeaders)(nil).Get), url) +} + +// Head mocks base method. +func (m *MockIClientWithHeaders) Head(url string) (*http.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Head", url) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Head indicates an expected call of Head. +func (mr *MockIClientWithHeadersMockRecorder) Head(url any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Head", reflect.TypeOf((*MockIClientWithHeaders)(nil).Head), url) +} + +// Options mocks base method. +func (m *MockIClientWithHeaders) Options(url string) (*http.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Options", url) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Options indicates an expected call of Options. +func (mr *MockIClientWithHeadersMockRecorder) Options(url any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Options", reflect.TypeOf((*MockIClientWithHeaders)(nil).Options), url) +} + +// Post mocks base method. +func (m *MockIClientWithHeaders) Post(url, contentType string, body any) (*http.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Post", url, contentType, body) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Post indicates an expected call of Post. +func (mr *MockIClientWithHeadersMockRecorder) Post(url, contentType, body any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Post", reflect.TypeOf((*MockIClientWithHeaders)(nil).Post), url, contentType, body) +} + +// PostForm mocks base method. +func (m *MockIClientWithHeaders) PostForm(url string, data url.Values) (*http.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PostForm", url, data) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PostForm indicates an expected call of PostForm. +func (mr *MockIClientWithHeadersMockRecorder) PostForm(url, data any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostForm", reflect.TypeOf((*MockIClientWithHeaders)(nil).PostForm), url, data) +} + +// Put mocks base method. +func (m *MockIClientWithHeaders) Put(url string, body any) (*http.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Put", url, body) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Put indicates an expected call of Put. +func (mr *MockIClientWithHeadersMockRecorder) Put(url, body any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Put", reflect.TypeOf((*MockIClientWithHeaders)(nil).Put), url, body) +} + +// RemoveHeader mocks base method. +func (m *MockIClientWithHeaders) RemoveHeader(key string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "RemoveHeader", key) +} + +// RemoveHeader indicates an expected call of RemoveHeader. +func (mr *MockIClientWithHeadersMockRecorder) RemoveHeader(key any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveHeader", reflect.TypeOf((*MockIClientWithHeaders)(nil).RemoveHeader), key) +} + +// StandardClient mocks base method. +func (m *MockIClientWithHeaders) StandardClient() *http.Client { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StandardClient") + ret0, _ := ret[0].(*http.Client) + return ret0 +} + +// StandardClient indicates an expected call of StandardClient. +func (mr *MockIClientWithHeadersMockRecorder) StandardClient() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StandardClient", reflect.TypeOf((*MockIClientWithHeaders)(nil).StandardClient)) +}