apiclient: split auth_key, auth_retry, auth_jwt (#2743)

This commit is contained in:
mmetc 2024-01-17 15:08:41 +01:00 committed by GitHub
parent 4df4e5b3bf
commit d760b401e6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 189 additions and 163 deletions

View file

@ -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"

View file

@ -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
}

73
pkg/apiclient/auth_key.go Normal file
View file

@ -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
}

View file

@ -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
}

32
pkg/apiclient/clone.go Normal file
View file

@ -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
}