add configuration pkg and tests

This commit is contained in:
Max Lerebourg
2022-12-04 21:40:14 +01:00
parent 91c31e3fa1
commit 988f3ebeae
5 changed files with 410 additions and 402 deletions
+16 -202
View File
@@ -8,14 +8,10 @@ import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"reflect"
"text/template"
"time"
@@ -23,12 +19,10 @@ import (
ip "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/ip"
logger "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
simpleredis "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/simpleredis"
configuration "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/configuration"
)
const (
streamMode = "stream"
liveMode = "live"
noneMode = "none"
crowdsecLapiHeader = "X-Api-Key"
crowdsecLapiRoute = "v1/decisions"
crowdsecLapiStreamRoute = "v1/decisions/stream"
@@ -41,49 +35,9 @@ var (
ticker chan bool
)
// Config the plugin configuration.
type Config struct {
Enabled bool `json:"enabled,omitempty"`
LogLevel string `json:"logLevel,omitempty"`
CrowdsecMode string `json:"crowdsecMode,omitempty"`
CrowdsecLapiScheme string `json:"crowdsecLapiScheme,omitempty"`
CrowdsecLapiHost string `json:"crowdsecLapiHost,omitempty"`
CrowdsecLapiKey string `json:"crowdsecLapiKey,omitempty"`
CrowdsecLapiKeyFile string `json:"crowdsecLapiKeyFile,omitempty"`
CrowdsecLapiTLSInsecureVerify bool `json:"crowdsecLapiTlsInsecureVerify,omitempty"`
CrowdsecLapiTLSCertificateAuthority string `json:"crowdsecLapiTlsCertificateAuthority,omitempty"`
CrowdsecLapiTLSCertificateAuthorityFile string `json:"crowdsecLapiTlsCertificateAuthorityFile,omitempty"`
CrowdsecLapiTLSCertificateBouncer string `json:"crowdsecLapiTlsCertificateBouncer,omitempty"`
CrowdsecLapiTLSCertificateBouncerFile string `json:"crowdsecLapiTlsCertificateBouncerFile,omitempty"`
CrowdsecLapiTLSCertificateBouncerKey string `json:"crowdsecLapiTlsCertificateBouncerKey,omitempty"`
CrowdsecLapiTLSCertificateBouncerKeyFile string `json:"crowdsecLapiTlsCertificateBouncerKeyFile,omitempty"`
UpdateIntervalSeconds int64 `json:"updateIntervalSeconds,omitempty"`
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"`
ForwardedHeadersCustomName string `json:"forwardedheaderscustomheader,omitempty"`
ForwardedHeadersTrustedIPs []string `json:"forwardedHeadersTrustedIps,omitempty"`
ClientTrustedIPs []string `json:"clientTrustedIps,omitempty"`
RedisCacheEnabled bool `json:"redisCacheEnabled,omitempty"`
RedisCacheHost string `json:"redisCacheHost,omitempty"`
}
// CreateConfig creates the default plugin configuration.
func CreateConfig() *Config {
return &Config{
Enabled: false,
LogLevel: "INFO",
CrowdsecMode: liveMode,
CrowdsecLapiScheme: "http",
CrowdsecLapiHost: "crowdsec:8080",
CrowdsecLapiKey: "",
CrowdsecLapiTLSInsecureVerify: false,
UpdateIntervalSeconds: 60,
DefaultDecisionSeconds: 60,
ForwardedHeadersCustomName: "X-Forwarded-For",
ForwardedHeadersTrustedIPs: []string{},
ClientTrustedIPs: []string{},
RedisCacheEnabled: false,
RedisCacheHost: "redis:6379",
}
func CreateConfig() *configuration.Config {
return configuration.New()
}
// Bouncer a Bouncer struct.
@@ -106,9 +60,9 @@ type Bouncer struct {
}
// New creates the crowdsec bouncer plugin.
func New(ctx context.Context, next http.Handler, config *Config, name string) (http.Handler, error) {
func New(ctx context.Context, next http.Handler, config *configuration.Config, name string) (http.Handler, error) {
logger.Init(config.LogLevel)
err := validateParams(config)
err := configuration.ValidateParams(config)
if err != nil {
logger.Info(fmt.Sprintf("New:validateParams %s", err.Error()))
return nil, err
@@ -122,7 +76,7 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
logger.Error(fmt.Sprintf("New:getTLSConfigCrowdsec fail to get tlsConfig %s", err.Error()))
return nil, err
}
apiKey, err := getVariable(config, "CrowdsecLapiKey")
apiKey, err := configuration.GetVariable(config, "CrowdsecLapiKey")
if err != nil && len(tlsConfig.Certificates) == 0 {
logger.Error(fmt.Sprintf("New:crowdsecLapiKey fail to get CrowdsecLapiKey and no client certificate setup %s", err.Error()))
return nil, err
@@ -159,7 +113,7 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
if config.RedisCacheEnabled {
cache.InitRedisClient(config.RedisCacheHost)
}
if config.CrowdsecMode == streamMode && ticker == nil {
if config.CrowdsecMode == configuration.StreamMode && ticker == nil {
ticker = startTicker(config, func() {
handleStreamCache(bouncer)
})
@@ -199,7 +153,7 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
}
// TODO This should be simplified
if bouncer.crowdsecMode != noneMode {
if bouncer.crowdsecMode != configuration.NoneMode {
isBanned, erro := cache.GetDecision(remoteIP)
if erro != nil {
logger.Debug(fmt.Sprintf("ServeHTTP:getDecision ip:%s %s", remoteIP, erro.Error()))
@@ -219,7 +173,7 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
}
// Right here if we cannot join the stream we forbid the request to go on.
if bouncer.crowdsecMode == streamMode {
if bouncer.crowdsecMode == configuration.StreamMode {
if isCrowdsecStreamHealthy {
bouncer.next.ServeHTTP(rw, req)
} else {
@@ -258,16 +212,7 @@ type Stream struct {
New []Decision `json:"new"`
}
func contains(source []string, target string) bool {
for _, item := range source {
if item == target {
return true
}
}
return false
}
func startTicker(config *Config, work func()) chan bool {
func startTicker(config *configuration.Config, work func()) chan bool {
ticker := time.NewTicker(time.Duration(config.UpdateIntervalSeconds) * time.Second)
stop := make(chan bool, 1)
go func() {
@@ -286,7 +231,7 @@ func startTicker(config *Config, work func()) chan bool {
// We are now in none or live mode.
func handleNoStreamCache(bouncer *Bouncer, remoteIP string) error {
isLiveMode := bouncer.crowdsecMode == liveMode
isLiveMode := bouncer.crowdsecMode == configuration.LiveMode
routeURL := url.URL{
Scheme: bouncer.crowdsecScheme,
Host: bouncer.crowdsecHost,
@@ -392,7 +337,7 @@ func crowdsecQuery(bouncer *Bouncer, stringURL string) ([]byte, error) {
return body, nil
}
func getTLSConfigCrowdsec(config *Config) (*tls.Config, error) {
func getTLSConfigCrowdsec(config *configuration.Config) (*tls.Config, error) {
tlsConfig := new(tls.Config)
tlsConfig.RootCAs = x509.NewCertPool()
//nolint:gocritic
@@ -405,7 +350,7 @@ func getTLSConfigCrowdsec(config *Config) (*tls.Config, error) {
// If we return here and still want to use client auth this won't work
// return tlsConfig, nil
} else {
certAuthority, err := getVariable(config, "CrowdsecLapiTLSCertificateAuthority")
certAuthority, err := configuration.GetVariable(config, "CrowdsecLapiTLSCertificateAuthority")
if err != nil {
return nil, err
}
@@ -414,15 +359,15 @@ func getTLSConfigCrowdsec(config *Config) (*tls.Config, error) {
logger.Debug("getTLSConfigCrowdsec:CrowdsecLapiTLSCertificateAuthority read cert failed")
// here we return because if CrowdsecLapiTLSInsecureVerify is false
// and CA not load, we can't communicate with https
return nil, errors.New("getTLSConfigCrowdsec:cannot load CA and verify cert is enabled")
return nil, fmt.Errorf("getTLSConfigCrowdsec:cannot load CA and verify cert is enabled")
}
}
certBouncer, err := getVariable(config, "CrowdsecLapiTLSCertificateBouncer")
certBouncer, err := configuration.GetVariable(config, "CrowdsecLapiTLSCertificateBouncer")
if err != nil {
return nil, err
}
certBouncerKey, err := getVariable(config, "CrowdsecLapiTLSCertificateBouncerKey")
certBouncerKey, err := configuration.GetVariable(config, "CrowdsecLapiTLSCertificateBouncerKey")
if err != nil {
return nil, err
}
@@ -437,134 +382,3 @@ func getTLSConfigCrowdsec(config *Config) (*tls.Config, error) {
return tlsConfig, nil
}
func getVariable(config *Config, key string) (string, error) {
value := ""
object := reflect.Indirect(reflect.ValueOf(config))
field := object.FieldByName(fmt.Sprintf("%sFile", key))
// Here linter say you should simplify this code, but lets not, performance is important not clarity and complexity
fp := field.String()
if fp != "" {
file, err := os.Stat(fp)
if err != nil {
return value, fmt.Errorf("%s:%s invalid path %w", key, fp, err)
}
if file.IsDir() {
return value, fmt.Errorf("%s:%s path must be a file", key, fp)
}
fileValue, err := os.ReadFile(filepath.Clean(fp))
if err != nil {
return value, fmt.Errorf("%s:%s read file path failed %w", key, fp, err)
}
value = string(fileValue)
return value, nil
}
field = object.FieldByName(key)
value = field.String()
return value, nil
}
func validateParams(config *Config) error {
if err := validateParamsRequired(config); err != nil {
return err
}
// This only check that the format of the URL scheme:// is correct and do not make requests
testURL := url.URL{
Scheme: config.CrowdsecLapiScheme,
Host: config.CrowdsecLapiHost,
}
if _, err := http.NewRequest(http.MethodGet, testURL.String(), nil); err != nil {
return fmt.Errorf("CrowdsecLapiScheme://CrowdsecLapiHost: '%v://%v' must be an URL", config.CrowdsecLapiScheme, config.CrowdsecLapiHost)
}
if err := validateParamsIPs(config.ForwardedHeadersTrustedIPs, "ForwardedHeadersTrustedIPs"); err != nil {
return err
}
if err := validateParamsIPs(config.ClientTrustedIPs, "ClientTrustedIPs"); err != nil {
return err
}
lapiKey, err := getVariable(config, "CrowdsecLapiKey")
if err != nil {
return err
}
certBouncer, err := getVariable(config, "CrowdsecLapiTLSCertificateBouncer")
if err != nil {
return err
}
certBouncerKey, err := getVariable(config, "CrowdsecLapiTLSCertificateBouncerKey")
if err != nil {
return err
}
// We need to either have crowdsecLapiKey defined or the BouncerCert and Bouncerkey
if lapiKey == "" && (certBouncer == "" || certBouncerKey == "") {
return fmt.Errorf("CrowdsecLapiKey || (CrowdsecLapiTLSCertificateBouncer && CrowdsecLapiTLSCertificateBouncerKey): cannot be both empty")
}
// Case https to contact Crowdsec LAPI and certificate must be provided
if config.CrowdsecLapiScheme == "https" && !config.CrowdsecLapiTLSInsecureVerify {
err = validateParamsTLS(config)
if err != nil {
return err
}
}
return nil
}
func validateParamsTLS(config *Config) error {
certAuth, err := getVariable(config, "CrowdsecLapiTLSCertificateAuthority")
if err != nil {
return err
}
if certAuth == "" {
return fmt.Errorf("CrowdsecLapiTLSCertificateAuthority must be specified when CrowdsecLapiScheme='https' and CrowdsecLapiTLSInsecureVerify=false")
}
tlsConfig := new(tls.Config)
tlsConfig.RootCAs = x509.NewCertPool()
if !tlsConfig.RootCAs.AppendCertsFromPEM([]byte(certAuth)) {
return fmt.Errorf("failed parsing pem file")
}
return nil
}
func validateParamsIPs(listIP []string, key string) error {
if len(listIP) > 0 {
if _, err := ip.NewChecker(listIP); err != nil {
return fmt.Errorf("%s must be a list of IP/CIDR :%w", key, err)
}
} else {
logger.Debug(fmt.Sprintf("No IP provided for %s", key))
}
return nil
}
func validateParamsRequired(config *Config) error {
requiredStrings := map[string]string{
"CrowdsecLapiScheme": config.CrowdsecLapiScheme,
"CrowdsecLapiHost": config.CrowdsecLapiHost,
"CrowdsecMode": config.CrowdsecMode,
}
for key, val := range requiredStrings {
if len(val) == 0 {
return fmt.Errorf("%v: cannot be empty", key)
}
}
requiredInt := map[string]int64{
"UpdateIntervalSeconds": config.UpdateIntervalSeconds,
"DefaultDecisionSeconds": config.DefaultDecisionSeconds,
}
for key, val := range requiredInt {
if val < 1 {
return fmt.Errorf("%v: cannot be less than 1", key)
}
}
if !contains([]string{noneMode, liveMode, streamMode}, config.CrowdsecMode) {
return fmt.Errorf("CrowdsecMode: must be one of 'none', 'live' or 'stream'")
}
if !contains([]string{"http", "https"}, config.CrowdsecLapiScheme) {
return fmt.Errorf("CrowdsecLapiScheme: must be one of 'http' or 'https'")
}
return nil
}
-196
View File
@@ -12,12 +12,6 @@ import (
ip "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/ip"
)
func getMinimalConfig() *Config {
cfg := CreateConfig()
cfg.CrowdsecLapiKey = "test"
return cfg
}
func TestServeHTTP(t *testing.T) {
cfg := CreateConfig()
cfg.CrowdsecLapiKey = "test"
@@ -119,27 +113,6 @@ func TestBouncer_ServeHTTP(t *testing.T) {
}
}
func Test_contains(t *testing.T) {
type args struct {
source []string
target string
}
tests := []struct {
name string
args args
want bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := contains(tt.args.source, tt.args.target); got != tt.want {
t.Errorf("contains() = %v, want %v", got, tt.want)
}
})
}
}
func Test_handleNoStreamCache(t *testing.T) {
type args struct {
bouncer *Bouncer
@@ -230,172 +203,3 @@ func Test_getTLSConfigCrowdsec(t *testing.T) {
})
}
}
func Test_getVariable(t *testing.T) {
cfg1 := CreateConfig()
cfg1.CrowdsecLapiKey = "test"
cfg2 := CreateConfig()
cfg2.CrowdsecLapiKeyFile = "./tests/.keytest"
cfg3 := CreateConfig()
cfg3.CrowdsecLapiKeyFile = "./tests/.bad"
type args struct {
config *Config
key string
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{
name: "valid string",
args: args{config: cfg1, key: "CrowdsecLapiKey"},
want: "test",
wantErr: false,
},
{
name: "valid file",
args: args{config: cfg2, key: "CrowdsecLapiKey"},
want: "test",
wantErr: false,
},
{
name: "invalid file",
args: args{config: cfg3, key: "CrowdsecLapiKey"},
want: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := getVariable(tt.args.config, tt.args.key)
if (err != nil) != tt.wantErr {
t.Errorf("getVariable() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("getVariable() = %v, want %v", got, tt.want)
}
})
}
}
func Test_validateParams(t *testing.T) {
cfg2 := getMinimalConfig()
cfg2.CrowdsecLapiScheme = "bad"
cfg3 := getMinimalConfig()
cfg3.CrowdsecMode = "bad"
cfg4 := getMinimalConfig()
cfg4.UpdateIntervalSeconds = 0
cfg5 := getMinimalConfig()
cfg5.ClientTrustedIPs = []string{0: "bad"}
cfg6 := getMinimalConfig()
cfg6.CrowdsecLapiScheme = "https"
cfg6.CrowdsecLapiTLSInsecureVerify = true
cfg8 := getMinimalConfig()
cfg8.CrowdsecLapiScheme = "https"
type args struct {
config *Config
}
tests := []struct {
name string
args args
wantErr bool
}{
{name: "good minimal config", args: args{config: getMinimalConfig()}, wantErr: false},
{name: "bad crowdsec lapi key", args: args{config: CreateConfig()}, wantErr: true},
{name: "bad crowdsec scheme", args: args{config: cfg2}, wantErr: true},
{name: "bad crowdsec mode", args: args{config: cfg3}, wantErr: true},
{name: "bad update interval", args: args{config: cfg4}, wantErr: true},
{name: "bad clients ips", args: args{config: cfg5}, wantErr: true},
// HTTPS enabled
{name: "good https config with insecure verify", args: args{config: cfg6}, wantErr: false},
{name: "no cert authority", args: args{config: cfg8}, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := validateParams(tt.args.config); (err != nil) != tt.wantErr {
t.Errorf("validateParams() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func Test_validateParamsTLS(t *testing.T) {
type args struct {
config *Config
}
tests := []struct {
name string
args args
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := validateParamsTLS(tt.args.config); (err != nil) != tt.wantErr {
t.Errorf("validateParamsTLS() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func Test_validateParamsIPs(t *testing.T) {
type args struct {
listIP []string
key string
}
tests := []struct {
name string
args args
wantErr bool
}{
{name: "not an ip", args: args{listIP: []string{0: "bad"}}, wantErr: true},
{name: "weird ip", args: args{listIP: []string{0: "0.0.0.0/89"}}, wantErr: true},
{name: "localhost ?", args: args{listIP: []string{0: "localhost"}}, wantErr: true},
{name: "weird ip 2", args: args{listIP: []string{0: "0.0.0.256/12"}}, wantErr: true},
{name: "valid ip", args: args{listIP: []string{0: "0.0.0.0/12"}}, wantErr: false},
{name: "valid ip list", args: args{listIP: []string{0: "0.0.0.0/0", 1: "1.1.1.1/1"}}, wantErr: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := validateParamsIPs(tt.args.listIP, tt.args.key); (err != nil) != tt.wantErr {
t.Errorf("validateParamsIPs() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func Test_validateParamsRequired(t *testing.T) {
cfg2 := getMinimalConfig()
cfg2.CrowdsecLapiScheme = "bad"
cfg3 := getMinimalConfig()
cfg3.CrowdsecMode = "bad"
cfg4 := getMinimalConfig()
cfg4.UpdateIntervalSeconds = 0
cfg5 := getMinimalConfig()
cfg5.DefaultDecisionSeconds = 0
type args struct {
config *Config
}
tests := []struct {
name string
args args
wantErr bool
}{
{name: "good", args: args{config: getMinimalConfig()}, wantErr: false},
{name: "bad crowdsec scheme", args: args{config: cfg2}, wantErr: true},
{name: "bad crowdsec mode", args: args{config: cfg3}, wantErr: true},
{name: "bad update interval seconds", args: args{config: cfg4}, wantErr: true},
{name: "bad default decision seconds", args: args{config: cfg5}, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := validateParamsRequired(tt.args.config); (err != nil) != tt.wantErr {
t.Errorf("validateParamsRequired() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
@@ -14,13 +14,13 @@ services:
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
- "--experimental.plugins.bouncer.version=v1.1.4"
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
# - "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
# - "--experimental.plugins.bouncer.version=v1.1.4"
- "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- logs-redis:/var/log/traefik
# - ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
- ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
ports:
- 80:80
- 8080:8080
+206
View File
@@ -0,0 +1,206 @@
package configuration
import (
"crypto/tls"
"crypto/x509"
"fmt"
"net/http"
"net/url"
"os"
"path/filepath"
"reflect"
ip "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/ip"
logger "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
)
const (
StreamMode = "stream"
LiveMode = "live"
NoneMode = "none"
)
// Config the plugin configuration.
type Config struct {
Enabled bool `json:"enabled,omitempty"`
LogLevel string `json:"logLevel,omitempty"`
CrowdsecMode string `json:"crowdsecMode,omitempty"`
CrowdsecLapiScheme string `json:"crowdsecLapiScheme,omitempty"`
CrowdsecLapiHost string `json:"crowdsecLapiHost,omitempty"`
CrowdsecLapiKey string `json:"crowdsecLapiKey,omitempty"`
CrowdsecLapiKeyFile string `json:"crowdsecLapiKeyFile,omitempty"`
CrowdsecLapiTLSInsecureVerify bool `json:"crowdsecLapiTlsInsecureVerify,omitempty"`
CrowdsecLapiTLSCertificateAuthority string `json:"crowdsecLapiTlsCertificateAuthority,omitempty"`
CrowdsecLapiTLSCertificateAuthorityFile string `json:"crowdsecLapiTlsCertificateAuthorityFile,omitempty"`
CrowdsecLapiTLSCertificateBouncer string `json:"crowdsecLapiTlsCertificateBouncer,omitempty"`
CrowdsecLapiTLSCertificateBouncerFile string `json:"crowdsecLapiTlsCertificateBouncerFile,omitempty"`
CrowdsecLapiTLSCertificateBouncerKey string `json:"crowdsecLapiTlsCertificateBouncerKey,omitempty"`
CrowdsecLapiTLSCertificateBouncerKeyFile string `json:"crowdsecLapiTlsCertificateBouncerKeyFile,omitempty"`
UpdateIntervalSeconds int64 `json:"updateIntervalSeconds,omitempty"`
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"`
ForwardedHeadersCustomName string `json:"forwardedheaderscustomheader,omitempty"`
ForwardedHeadersTrustedIPs []string `json:"forwardedHeadersTrustedIps,omitempty"`
ClientTrustedIPs []string `json:"clientTrustedIps,omitempty"`
RedisCacheEnabled bool `json:"redisCacheEnabled,omitempty"`
RedisCacheHost string `json:"redisCacheHost,omitempty"`
}
func contains(source []string, target string) bool {
for _, item := range source {
if item == target {
return true
}
}
return false
}
// CreateConfig creates the default plugin configuration.
func New() *Config {
return &Config{
Enabled: false,
LogLevel: "INFO",
CrowdsecMode: LiveMode,
CrowdsecLapiScheme: "http",
CrowdsecLapiHost: "crowdsec:8080",
CrowdsecLapiKey: "",
CrowdsecLapiTLSInsecureVerify: false,
UpdateIntervalSeconds: 60,
DefaultDecisionSeconds: 60,
ForwardedHeadersCustomName: "X-Forwarded-For",
ForwardedHeadersTrustedIPs: []string{},
ClientTrustedIPs: []string{},
RedisCacheEnabled: false,
RedisCacheHost: "redis:6379",
}
}
func GetVariable(config *Config, key string) (string, error) {
value := ""
object := reflect.Indirect(reflect.ValueOf(config))
field := object.FieldByName(fmt.Sprintf("%sFile", key))
// Here linter say you should simplify this code, but lets not, performance is important not clarity and complexity
fp := field.String()
if fp != "" {
file, err := os.Stat(fp)
if err != nil {
return value, fmt.Errorf("%s:%s invalid path %w", key, fp, err)
}
if file.IsDir() {
return value, fmt.Errorf("%s:%s path must be a file", key, fp)
}
fileValue, err := os.ReadFile(filepath.Clean(fp))
if err != nil {
return value, fmt.Errorf("%s:%s read file path failed %w", key, fp, err)
}
value = string(fileValue)
return value, nil
}
field = object.FieldByName(key)
value = field.String()
return value, nil
}
func ValidateParams(config *Config) error {
if err := validateParamsRequired(config); err != nil {
return err
}
// This only check that the format of the URL scheme:// is correct and do not make requests
testURL := url.URL{
Scheme: config.CrowdsecLapiScheme,
Host: config.CrowdsecLapiHost,
}
if _, err := http.NewRequest(http.MethodGet, testURL.String(), nil); err != nil {
return fmt.Errorf("CrowdsecLapiScheme://CrowdsecLapiHost: '%v://%v' must be an URL", config.CrowdsecLapiScheme, config.CrowdsecLapiHost)
}
if err := validateParamsIPs(config.ForwardedHeadersTrustedIPs, "ForwardedHeadersTrustedIPs"); err != nil {
return err
}
if err := validateParamsIPs(config.ClientTrustedIPs, "ClientTrustedIPs"); err != nil {
return err
}
lapiKey, err := GetVariable(config, "CrowdsecLapiKey")
if err != nil {
return err
}
certBouncer, err := GetVariable(config, "CrowdsecLapiTLSCertificateBouncer")
if err != nil {
return err
}
certBouncerKey, err := GetVariable(config, "CrowdsecLapiTLSCertificateBouncerKey")
if err != nil {
return err
}
// We need to either have crowdsecLapiKey defined or the BouncerCert and Bouncerkey
if lapiKey == "" && (certBouncer == "" || certBouncerKey == "") {
return fmt.Errorf("CrowdsecLapiKey || (CrowdsecLapiTLSCertificateBouncer && CrowdsecLapiTLSCertificateBouncerKey): cannot be both empty")
}
// Case https to contact Crowdsec LAPI and certificate must be provided
if config.CrowdsecLapiScheme == "https" && !config.CrowdsecLapiTLSInsecureVerify {
err = validateParamsTLS(config)
if err != nil {
return err
}
}
return nil
}
func validateParamsTLS(config *Config) error {
certAuth, err := GetVariable(config, "CrowdsecLapiTLSCertificateAuthority")
if err != nil {
return err
}
if certAuth == "" {
return fmt.Errorf("CrowdsecLapiTLSCertificateAuthority must be specified when CrowdsecLapiScheme='https' and CrowdsecLapiTLSInsecureVerify=false")
}
tlsConfig := new(tls.Config)
tlsConfig.RootCAs = x509.NewCertPool()
if !tlsConfig.RootCAs.AppendCertsFromPEM([]byte(certAuth)) {
return fmt.Errorf("failed parsing pem file")
}
return nil
}
func validateParamsIPs(listIP []string, key string) error {
if len(listIP) > 0 {
if _, err := ip.NewChecker(listIP); err != nil {
return fmt.Errorf("%s must be a list of IP/CIDR :%w", key, err)
}
} else {
logger.Debug(fmt.Sprintf("No IP provided for %s", key))
}
return nil
}
func validateParamsRequired(config *Config) error {
requiredStrings := map[string]string{
"CrowdsecLapiScheme": config.CrowdsecLapiScheme,
"CrowdsecLapiHost": config.CrowdsecLapiHost,
"CrowdsecMode": config.CrowdsecMode,
}
for key, val := range requiredStrings {
if len(val) == 0 {
return fmt.Errorf("%v: cannot be empty", key)
}
}
requiredInt := map[string]int64{
"UpdateIntervalSeconds": config.UpdateIntervalSeconds,
"DefaultDecisionSeconds": config.DefaultDecisionSeconds,
}
for key, val := range requiredInt {
if val < 1 {
return fmt.Errorf("%v: cannot be less than 1", key)
}
}
if !contains([]string{NoneMode, LiveMode, StreamMode}, config.CrowdsecMode) {
return fmt.Errorf("CrowdsecMode: must be one of 'none', 'live' or 'stream'")
}
if !contains([]string{"http", "https"}, config.CrowdsecLapiScheme) {
return fmt.Errorf("CrowdsecLapiScheme: must be one of 'http' or 'https'")
}
return nil
}
+184
View File
@@ -0,0 +1,184 @@
package configuration
import (
"testing"
)
func getMinimalConfig() *Config {
cfg := New()
cfg.CrowdsecLapiKey = "test"
return cfg
}
func Test_contains(t *testing.T) {
type args struct {
source []string
target string
}
tests := []struct {
name string
args args
want bool
}{
{name: "Contain in the list", args: args{source: []string{"a", "b"}, target: "a"}, want: true},
{name: "Contain not in the list", args: args{source: []string{"a", "b"}, target: "c"}, want: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := contains(tt.args.source, tt.args.target); got != tt.want {
t.Errorf("contains() = %v, want %v", got, tt.want)
}
})
}
}
func Test_GetVariable(t *testing.T) {
cfg1 := New()
cfg1.CrowdsecLapiKey = "test"
cfg2 := New()
cfg2.CrowdsecLapiKeyFile = "../../tests/.keytest"
cfg3 := New()
cfg3.CrowdsecLapiKeyFile = "../../tests/.bad"
type args struct {
config *Config
key string
}
tests := []struct{
name string
args args
want string
wantErr bool
}{
{name: "Validate a key string", args: args{config: cfg1, key: "CrowdsecLapiKey"}, want: "test", wantErr: false},
{name: "Validate a key file", args: args{config: cfg2, key: "CrowdsecLapiKey"}, want: "test", wantErr: false},
{name: "Not validate an invalid file", args: args{config: cfg3, key: "CrowdsecLapiKey"}, want: "", wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := GetVariable(tt.args.config, tt.args.key)
if (err != nil) != tt.wantErr {
t.Errorf("getVariable() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("getVariable() = %v, want %v", got, tt.want)
}
})
}
}
func Test_ValidateParams(t *testing.T) {
cfg3 := getMinimalConfig()
cfg3.CrowdsecMode = "bad"
cfg4 := getMinimalConfig()
cfg4.UpdateIntervalSeconds = 0
cfg5 := getMinimalConfig()
cfg5.ClientTrustedIPs = []string{0: "bad"}
cfg6 := getMinimalConfig()
cfg6.CrowdsecLapiScheme = "https"
cfg6.CrowdsecLapiTLSInsecureVerify = true
cfg8 := getMinimalConfig()
cfg8.CrowdsecLapiScheme = "https"
type args struct {
config *Config
}
tests := []struct {
name string
args args
wantErr bool
}{
{name: "Validate minimal config", args: args{config: getMinimalConfig()}, wantErr: false},
{name: "Not validate an absent crowdsec lapi key", args: args{config: New()}, wantErr: true},
{name: "Not validate a not listed item", args: args{config: cfg3}, wantErr: true},
{name: "Not validate a bad number", args: args{config: cfg4}, wantErr: true},
{name: "Not validate a bad clients ips", args: args{config: cfg5}, wantErr: true},
// HTTPS enabled
{name: "Validate https config with insecure verify", args: args{config: cfg6}, wantErr: false},
{name: "Not validate https without cert authority", args: args{config: cfg8}, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := ValidateParams(tt.args.config); (err != nil) != tt.wantErr {
t.Errorf("validateParams() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func Test_validateParamsTLS(t *testing.T) {
type args struct {
config *Config
}
tests := []struct {
name string
args args
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := validateParamsTLS(tt.args.config); (err != nil) != tt.wantErr {
t.Errorf("validateParamsTLS() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func Test_validateParamsIPs(t *testing.T) {
type args struct {
listIP []string
key string
}
tests := []struct {
name string
args args
wantErr bool
}{
{name: "Not validate a non ip", args: args{listIP: []string{0: "bad"}}, wantErr: true},
{name: "Not validate localhost", args: args{listIP: []string{0: "localhost"}}, wantErr: true},
{name: "Not validate a weird ip", args: args{listIP: []string{0: "0.0.0.0/89"}}, wantErr: true},
{name: "Not validate a weird ip 2", args: args{listIP: []string{0: "0.0.0.256/12"}}, wantErr: true},
{name: "Validate an ip", args: args{listIP: []string{0: "0.0.0.0/12"}}, wantErr: false},
{name: "Validate a ip list", args: args{listIP: []string{0: "0.0.0.0/0", 1: "1.1.1.1/1"}}, wantErr: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := validateParamsIPs(tt.args.listIP, tt.args.key); (err != nil) != tt.wantErr {
t.Errorf("validateParamsIPs() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func Test_validateParamsRequired(t *testing.T) {
cfg2 := getMinimalConfig()
cfg2.CrowdsecLapiScheme = "bad"
cfg3 := getMinimalConfig()
cfg3.CrowdsecMode = "bad"
cfg4 := getMinimalConfig()
cfg4.UpdateIntervalSeconds = 0
cfg5 := getMinimalConfig()
cfg5.DefaultDecisionSeconds = 0
type args struct {
config *Config
}
tests := []struct {
name string
args args
wantErr bool
}{
{name: "Validate minimal config", args: args{config: getMinimalConfig()}, wantErr: false},
{name: "Not validate a bad crowdsec scheme", args: args{config: cfg2}, wantErr: true},
{name: "Not validate a bad crowdsec mode", args: args{config: cfg3}, wantErr: true},
{name: "Not validate a bad update interval seconds", args: args{config: cfg4}, wantErr: true},
{name: "Not validate a bad default decision seconds", args: args{config: cfg5}, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := validateParamsRequired(tt.args.config); (err != nil) != tt.wantErr {
t.Errorf("validateParamsRequired() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}