From d760b401e6047a4643fed035c3a93e4c64d3bab9 Mon Sep 17 00:00:00 2001 From: mmetc <92726601+mmetc@users.noreply.github.com> Date: Wed, 17 Jan 2024 15:08:41 +0100 Subject: [PATCH] apiclient: split auth_key, auth_retry, auth_jwt (#2743) --- .golangci.yml | 5 +- pkg/apiclient/{auth.go => auth_jwt.go} | 161 ------------------ pkg/apiclient/auth_key.go | 73 ++++++++ .../{auth_test.go => auth_key_test.go} | 0 pkg/apiclient/auth_retry.go | 81 +++++++++ pkg/apiclient/clone.go | 32 ++++ 6 files changed, 189 insertions(+), 163 deletions(-) rename pkg/apiclient/{auth.go => auth_jwt.go} (62%) create mode 100644 pkg/apiclient/auth_key.go rename pkg/apiclient/{auth_test.go => auth_key_test.go} (100%) create mode 100644 pkg/apiclient/auth_retry.go create mode 100644 pkg/apiclient/clone.go diff --git a/.golangci.yml b/.golangci.yml index 5c0bab58c..571ee7a93 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -54,7 +54,7 @@ linters-settings: main: deny: - pkg: "github.com/pkg/errors" - desc: "errors.New() is deprecated in favor of fmt.Errorf()" + desc: "errors.Wrap() is deprecated in favor of fmt.Errorf()" linters: enable-all: true @@ -287,7 +287,8 @@ issues: - bodyclose text: "response body must be closed" - # named/naked returns are evil + # named/naked returns are evil, with a single exception + # https://go.dev/wiki/CodeReviewComments#named-result-parameters - linters: - nonamedreturns text: "named return .* with type .* found" diff --git a/pkg/apiclient/auth.go b/pkg/apiclient/auth_jwt.go similarity index 62% rename from pkg/apiclient/auth.go rename to pkg/apiclient/auth_jwt.go index 02a2f5ada..71b0e2731 100644 --- a/pkg/apiclient/auth.go +++ b/pkg/apiclient/auth_jwt.go @@ -3,10 +3,8 @@ package apiclient import ( "bytes" "encoding/json" - "errors" "fmt" "io" - "math/rand" "net/http" "net/http/httputil" "net/url" @@ -16,143 +14,9 @@ import ( "github.com/go-openapi/strfmt" log "github.com/sirupsen/logrus" - "github.com/crowdsecurity/crowdsec/pkg/fflag" "github.com/crowdsecurity/crowdsec/pkg/models" ) -type APIKeyTransport struct { - APIKey string - // Transport is the underlying HTTP transport to use when making requests. - // It will default to http.DefaultTransport if nil. - Transport http.RoundTripper - URL *url.URL - VersionPrefix string - UserAgent string -} - -// RoundTrip implements the RoundTripper interface. -func (t *APIKeyTransport) RoundTrip(req *http.Request) (*http.Response, error) { - if t.APIKey == "" { - return nil, errors.New("APIKey is empty") - } - - // We must make a copy of the Request so - // that we don't modify the Request we were given. This is required by the - // specification of http.RoundTripper. - req = cloneRequest(req) - req.Header.Add("X-Api-Key", t.APIKey) - - if t.UserAgent != "" { - req.Header.Add("User-Agent", t.UserAgent) - } - - log.Debugf("req-api: %s %s", req.Method, req.URL.String()) - - if log.GetLevel() >= log.TraceLevel { - dump, _ := httputil.DumpRequest(req, true) - log.Tracef("auth-api request: %s", string(dump)) - } - - // Make the HTTP request. - resp, err := t.transport().RoundTrip(req) - if err != nil { - log.Errorf("auth-api: auth with api key failed return nil response, error: %s", err) - - return resp, err - } - - if log.GetLevel() >= log.TraceLevel { - dump, _ := httputil.DumpResponse(resp, true) - log.Tracef("auth-api response: %s", string(dump)) - } - - log.Debugf("resp-api: http %d", resp.StatusCode) - - return resp, err -} - -func (t *APIKeyTransport) Client() *http.Client { - return &http.Client{Transport: t} -} - -func (t *APIKeyTransport) transport() http.RoundTripper { - if t.Transport != nil { - return t.Transport - } - - return http.DefaultTransport -} - -type retryRoundTripper struct { - next http.RoundTripper - maxAttempts int - retryStatusCodes []int - withBackOff bool - onBeforeRequest func(attempt int) -} - -func (r retryRoundTripper) ShouldRetry(statusCode int) bool { - for _, code := range r.retryStatusCodes { - if code == statusCode { - return true - } - } - - return false -} - -func (r retryRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - var ( - resp *http.Response - err error - ) - - backoff := 0 - maxAttempts := r.maxAttempts - - if fflag.DisableHttpRetryBackoff.IsEnabled() { - maxAttempts = 1 - } - - for i := 0; i < maxAttempts; i++ { - if i > 0 { - if r.withBackOff { - //nolint:gosec - backoff += 10 + rand.Intn(20) - } - - log.Infof("retrying in %d seconds (attempt %d of %d)", backoff, i+1, r.maxAttempts) - - select { - case <-req.Context().Done(): - return nil, req.Context().Err() - case <-time.After(time.Duration(backoff) * time.Second): - } - } - - if r.onBeforeRequest != nil { - r.onBeforeRequest(i) - } - - clonedReq := cloneRequest(req) - - resp, err = r.next.RoundTrip(clonedReq) - if err != nil { - if left := maxAttempts - i - 1; left > 0 { - log.Errorf("error while performing request: %s; %d retries left", err, left) - } - - continue - } - - if !r.ShouldRetry(resp.StatusCode) { - return resp, nil - } - } - - return resp, err -} - type JWTTransport struct { MachineID *string Password *strfmt.Password @@ -351,28 +215,3 @@ func (t *JWTTransport) transport() http.RoundTripper { }, } } - -// cloneRequest returns a clone of the provided *http.Request. The clone is a -// shallow copy of the struct and its Header map. -func cloneRequest(r *http.Request) *http.Request { - // shallow copy of the struct - r2 := new(http.Request) - *r2 = *r - // deep copy of the Header - r2.Header = make(http.Header, len(r.Header)) - - for k, s := range r.Header { - r2.Header[k] = append([]string(nil), s...) - } - - if r.Body != nil { - var b bytes.Buffer - - b.ReadFrom(r.Body) - - r.Body = io.NopCloser(&b) - r2.Body = io.NopCloser(bytes.NewReader(b.Bytes())) - } - - return r2 -} diff --git a/pkg/apiclient/auth_key.go b/pkg/apiclient/auth_key.go new file mode 100644 index 000000000..e2213aca2 --- /dev/null +++ b/pkg/apiclient/auth_key.go @@ -0,0 +1,73 @@ +package apiclient + +import ( + "errors" + "net/http" + "net/http/httputil" + "net/url" + + log "github.com/sirupsen/logrus" +) + +type APIKeyTransport struct { + APIKey string + // Transport is the underlying HTTP transport to use when making requests. + // It will default to http.DefaultTransport if nil. + Transport http.RoundTripper + URL *url.URL + VersionPrefix string + UserAgent string +} + +// RoundTrip implements the RoundTripper interface. +func (t *APIKeyTransport) RoundTrip(req *http.Request) (*http.Response, error) { + if t.APIKey == "" { + return nil, errors.New("APIKey is empty") + } + + // We must make a copy of the Request so + // that we don't modify the Request we were given. This is required by the + // specification of http.RoundTripper. + req = cloneRequest(req) + req.Header.Add("X-Api-Key", t.APIKey) + + if t.UserAgent != "" { + req.Header.Add("User-Agent", t.UserAgent) + } + + log.Debugf("req-api: %s %s", req.Method, req.URL.String()) + + if log.GetLevel() >= log.TraceLevel { + dump, _ := httputil.DumpRequest(req, true) + log.Tracef("auth-api request: %s", string(dump)) + } + + // Make the HTTP request. + resp, err := t.transport().RoundTrip(req) + if err != nil { + log.Errorf("auth-api: auth with api key failed return nil response, error: %s", err) + + return resp, err + } + + if log.GetLevel() >= log.TraceLevel { + dump, _ := httputil.DumpResponse(resp, true) + log.Tracef("auth-api response: %s", string(dump)) + } + + log.Debugf("resp-api: http %d", resp.StatusCode) + + return resp, err +} + +func (t *APIKeyTransport) Client() *http.Client { + return &http.Client{Transport: t} +} + +func (t *APIKeyTransport) transport() http.RoundTripper { + if t.Transport != nil { + return t.Transport + } + + return http.DefaultTransport +} diff --git a/pkg/apiclient/auth_test.go b/pkg/apiclient/auth_key_test.go similarity index 100% rename from pkg/apiclient/auth_test.go rename to pkg/apiclient/auth_key_test.go diff --git a/pkg/apiclient/auth_retry.go b/pkg/apiclient/auth_retry.go new file mode 100644 index 000000000..8ec8823f6 --- /dev/null +++ b/pkg/apiclient/auth_retry.go @@ -0,0 +1,81 @@ +package apiclient + +import ( + "math/rand" + "net/http" + "time" + + log "github.com/sirupsen/logrus" + + "github.com/crowdsecurity/crowdsec/pkg/fflag" +) + +type retryRoundTripper struct { + next http.RoundTripper + maxAttempts int + retryStatusCodes []int + withBackOff bool + onBeforeRequest func(attempt int) +} + +func (r retryRoundTripper) ShouldRetry(statusCode int) bool { + for _, code := range r.retryStatusCodes { + if code == statusCode { + return true + } + } + + return false +} + +func (r retryRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + var ( + resp *http.Response + err error + ) + + backoff := 0 + maxAttempts := r.maxAttempts + + if fflag.DisableHttpRetryBackoff.IsEnabled() { + maxAttempts = 1 + } + + for i := 0; i < maxAttempts; i++ { + if i > 0 { + if r.withBackOff { + //nolint:gosec + backoff += 10 + rand.Intn(20) + } + + log.Infof("retrying in %d seconds (attempt %d of %d)", backoff, i+1, r.maxAttempts) + + select { + case <-req.Context().Done(): + return nil, req.Context().Err() + case <-time.After(time.Duration(backoff) * time.Second): + } + } + + if r.onBeforeRequest != nil { + r.onBeforeRequest(i) + } + + clonedReq := cloneRequest(req) + + resp, err = r.next.RoundTrip(clonedReq) + if err != nil { + if left := maxAttempts - i - 1; left > 0 { + log.Errorf("error while performing request: %s; %d retries left", err, left) + } + + continue + } + + if !r.ShouldRetry(resp.StatusCode) { + return resp, nil + } + } + + return resp, err +} diff --git a/pkg/apiclient/clone.go b/pkg/apiclient/clone.go new file mode 100644 index 000000000..e8f474296 --- /dev/null +++ b/pkg/apiclient/clone.go @@ -0,0 +1,32 @@ +package apiclient + +import ( + "bytes" + "io" + "net/http" +) + +// cloneRequest returns a clone of the provided *http.Request. The clone is a +// shallow copy of the struct and its Header map. +func cloneRequest(r *http.Request) *http.Request { + // shallow copy of the struct + r2 := new(http.Request) + *r2 = *r + // deep copy of the Header + r2.Header = make(http.Header, len(r.Header)) + + for k, s := range r.Header { + r2.Header[k] = append([]string(nil), s...) + } + + if r.Body != nil { + var b bytes.Buffer + + b.ReadFrom(r.Body) + + r.Body = io.NopCloser(&b) + r2.Body = io.NopCloser(bytes.NewReader(b.Bytes())) + } + + return r2 +}