apiclient: split auth_key, auth_retry, auth_jwt (#2743)
This commit is contained in:
parent
4df4e5b3bf
commit
d760b401e6
|
@ -54,7 +54,7 @@ linters-settings:
|
||||||
main:
|
main:
|
||||||
deny:
|
deny:
|
||||||
- pkg: "github.com/pkg/errors"
|
- 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:
|
linters:
|
||||||
enable-all: true
|
enable-all: true
|
||||||
|
@ -287,7 +287,8 @@ issues:
|
||||||
- bodyclose
|
- bodyclose
|
||||||
text: "response body must be closed"
|
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:
|
- linters:
|
||||||
- nonamedreturns
|
- nonamedreturns
|
||||||
text: "named return .* with type .* found"
|
text: "named return .* with type .* found"
|
||||||
|
|
|
@ -3,10 +3,8 @@ package apiclient
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math/rand"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
@ -16,143 +14,9 @@ import (
|
||||||
"github.com/go-openapi/strfmt"
|
"github.com/go-openapi/strfmt"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/crowdsecurity/crowdsec/pkg/fflag"
|
|
||||||
"github.com/crowdsecurity/crowdsec/pkg/models"
|
"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 {
|
type JWTTransport struct {
|
||||||
MachineID *string
|
MachineID *string
|
||||||
Password *strfmt.Password
|
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