apiclient: split auth_key, auth_retry, auth_jwt (#2743)
This commit is contained in:
parent
4df4e5b3bf
commit
d760b401e6
6 changed files with 189 additions and 163 deletions
|
@ -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"
|
||||
|
|
|
@ -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
73
pkg/apiclient/auth_key.go
Normal 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
|
||||
}
|
81
pkg/apiclient/auth_retry.go
Normal file
81
pkg/apiclient/auth_retry.go
Normal 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
32
pkg/apiclient/clone.go
Normal 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
|
||||
}
|
Loading…
Reference in a new issue