Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve configuration loading and fix issues related to configuration (re)load #36

Merged
merged 9 commits into from
Jan 13, 2024
46 changes: 31 additions & 15 deletions crowdsec/caddyfile.go
Original file line number Diff line number Diff line change
@@ -1,27 +1,33 @@
package crowdsec

import (
"fmt"
"net/url"
"strings"
"time"

"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
)

func parseCaddyfileGlobalOption(d *caddyfile.Dispenser, existingVal interface{}) (interface{}, error) {

// TODO: make this work similar to the handler? Or doesn't that work for this
// app level module, because of shared config etc.

cfg = &config{
TickerInterval: defaultTickerInterval,
EnableStreaming: defaultStreamingEnabled,
EnableHardFails: defaultHardFailsEnabled,
func parseCrowdSec(d *caddyfile.Dispenser, existingVal any) (any, error) {
tv := true
fv := false
cs := &CrowdSec{
TickerInterval: "60s",
EnableStreaming: &tv,
EnableHardFails: &fv,
}

if !d.Next() {
return nil, d.Err("expected tokens")
}

if d.Val() != "crowdsec" {
return nil, d.Err(fmt.Sprintf(`expected "crowdsec"; got %q`, d.Val()))
}

for d.NextBlock(0) {
switch d.Val() {
case "api_url":
Expand All @@ -32,12 +38,19 @@ func parseCaddyfileGlobalOption(d *caddyfile.Dispenser, existingVal interface{})
if err != nil {
return nil, d.Errf("invalid URL %s: %v", d.Val(), err)
}
cfg.APIUrl = u.String()
if u.Scheme == "" {
return nil, d.Errf("URL %q does not have a scheme (i.e https)", u.String())
}
s := u.String()
if !strings.HasSuffix(s, "/") {
s = s + "/"
}
cs.APIUrl = s
case "api_key":
if !d.NextArg() {
return nil, d.ArgErr()
}
cfg.APIKey = d.Val()
cs.APIKey = d.Val()
case "ticker_interval":
if !d.NextArg() {
return nil, d.ArgErr()
Expand All @@ -46,21 +59,24 @@ func parseCaddyfileGlobalOption(d *caddyfile.Dispenser, existingVal interface{})
if err != nil {
return nil, d.Errf("invalid duration %s: %v", d.Val(), err)
}
cfg.TickerInterval = interval.String()
cs.TickerInterval = interval.String()
case "disable_streaming":
if d.NextArg() {
return nil, d.ArgErr()
}
cfg.EnableStreaming = false
cs.EnableStreaming = &fv
case "enable_hard_fails":
if d.NextArg() {
return nil, d.ArgErr()
}
cfg.EnableHardFails = true
cs.EnableHardFails = &tv
default:
return nil, d.Errf("invalid configuration token provided: %s", d.Val())
}
}

return nil, nil
return httpcaddyfile.App{
Name: "crowdsec",
Value: caddyconfig.JSON(cs, nil),
}, nil
}
250 changes: 164 additions & 86 deletions crowdsec/caddyfile_test.go
Original file line number Diff line number Diff line change
@@ -1,133 +1,211 @@
package crowdsec

import (
"encoding/json"
"testing"

"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestUnmarshalCaddyfile(t *testing.T) {
trueValue := true
falseValue := false
type args struct {
d *caddyfile.Dispenser
}
tv := true
fv := false
tests := []struct {
name string
expected *CrowdSec
args args
wantParseErr bool
wantConfigureErr bool
name string
input string
env map[string]string
expected *CrowdSec
wantParseErr bool
}{
{
name: "fail/no-args",
name: "fail/missing tokens",
expected: &CrowdSec{},
input: ``,
wantParseErr: true,
},
{
name: "fail/not-crowdsec",
expected: &CrowdSec{},
input: `not-crowdsec`,
wantParseErr: true,
},
{
name: "fail/invalid-duration",
expected: &CrowdSec{},
args: args{
d: caddyfile.NewTestDispenser(`crowdsec`),
},
wantParseErr: false,
wantConfigureErr: true,
input: `crowdsec {
api_url http://127.0.0.1:8080
api_key some_random_key
ticker_interval 30x
}`,
wantParseErr: true,
},
{
name: "fail/no-api-url",
expected: &CrowdSec{},
input: `
crowdsec {
api_url
api_key some_random_key
ticker_interval 30x
}`,
wantParseErr: true,
},
{
name: "fail/invalid-api-url",
expected: &CrowdSec{},
input: `crowdsec {
api_url http://\x00/
api_key some_random_key
ticker_interval 30x
}`,
wantParseErr: true,
},
{
name: "fail/invalid-api-url-no-scheme",
expected: &CrowdSec{},
input: `crowdsec {
api_url example.com
api_key some_random_key
ticker_interval 30x
}`,
wantParseErr: true,
},
{
name: "fail/missing-api-key",
expected: &CrowdSec{},
input: `crowdsec {
api_url http://127.0.0.1:8080
api_key
}`,
wantParseErr: true,
},
{
name: "fail/missing-ticker-interval",
expected: &CrowdSec{},
input: `crowdsec {
api_url http://127.0.0.1:8080
api_key test-key
ticker_interval
}`,
wantParseErr: true,
},
{
name: "fail/invalid-streaming",
expected: &CrowdSec{},
input: `crowdsec {
api_url http://127.0.0.1:8080
api_key test-key
ticker_interval 30s
disable_streaming absolutely
}`,
wantParseErr: true,
},
{
name: "fail/invalid-streaming",
expected: &CrowdSec{},
input: `crowdsec {
api_url http://127.0.0.1:8080
api_key test-key
ticker_interval 30s
disable_streaming
enable_hard_fails yo
}`,
wantParseErr: true,
},
{
name: "fail/unknown-token",
expected: &CrowdSec{},
input: `crowdsec {
api_url http://127.0.0.1:8080
api_key some_random_key
unknown_token 42
}`,
wantParseErr: true,
},
{
name: "ok/basic",
expected: &CrowdSec{
APIUrl: "http://127.0.0.1:8080/",
APIKey: "some_random_key",
APIUrl: "http://127.0.0.1:8080/",
APIKey: "some_random_key",
TickerInterval: "60s",
EnableStreaming: &tv,
EnableHardFails: &fv,
},
args: args{
d: caddyfile.NewTestDispenser(`crowdsec {
input: `crowdsec {
api_url http://127.0.0.1:8080
api_key some_random_key
}`),
},
wantParseErr: false,
wantConfigureErr: false,
}`,
wantParseErr: false,
},
{
name: "ok/full",
expected: &CrowdSec{
APIUrl: "http://127.0.0.1:8080/",
APIKey: "some_random_key",
TickerInterval: "33s",
EnableStreaming: &falseValue,
EnableHardFails: &trueValue,
EnableStreaming: &fv,
EnableHardFails: &tv,
},
args: args{
d: caddyfile.NewTestDispenser(`crowdsec {
input: `crowdsec {
api_url http://127.0.0.1:8080
api_key some_random_key
ticker_interval 33s
disable_streaming
enable_hard_fails
}`),
},
wantParseErr: false,
wantConfigureErr: false,
}`,
wantParseErr: false,
},
{
name: "fail/invalid-duration",
expected: &CrowdSec{},
args: args{
d: caddyfile.NewTestDispenser(`crowdsec {
api_url http://127.0.0.1:8080
api_key some_random_key
ticker_interval 30x
}`),
name: "ok/env-vars",
expected: &CrowdSec{
APIUrl: "http://127.0.0.2:8080/",
APIKey: "env-test-key",
TickerInterval: "25s",
EnableStreaming: &tv,
EnableHardFails: &fv,
},
wantParseErr: true,
wantConfigureErr: false,
},
{
name: "fail/unknown-token",
expected: &CrowdSec{},
args: args{
d: caddyfile.NewTestDispenser(`crowdsec {
api_url http://127.0.0.1:8080
api_key some_random_key
unknown_token 42
}`),
env: map[string]string{
"CROWDSEC_TEST_API_URL": "http://127.0.0.2:8080/",
"CROWDSEC_TEST_API_KEY": "env-test-key",
"CROWDSEC_TEST_TICKER_INTERVAL": "25s",
},
wantParseErr: true,
wantConfigureErr: false,
input: `crowdsec {
api_url {$CROWDSEC_TEST_API_URL}
api_key {$CROWDSEC_TEST_API_KEY}
ticker_interval {$CROWDSEC_TEST_TICKER_INTERVAL}
}`,
wantParseErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &CrowdSec{}
if _, err := parseCaddyfileGlobalOption(tt.args.d, nil); (err != nil) != tt.wantParseErr {
t.Errorf("CrowdSec.parseCaddyfileGlobalOption() error = %v, wantParseErr %v", err, tt.wantParseErr)
return
for k, v := range tt.env {
t.Setenv(k, v)
}
if err := c.configure(); (err != nil) != tt.wantConfigureErr {
t.Errorf("CrowdSec.configure) error = %v, wantConfigureErr %v", err, tt.wantConfigureErr)
dispenser := caddyfile.NewTestDispenser(tt.input)
jsonApp, err := parseCrowdSec(dispenser, nil)
if tt.wantParseErr {
assert.Error(t, err)
return
}
// TODO: properly use go-cmp and get unexported fields to work
if tt.expected.APIUrl != "" {
if tt.expected.APIUrl != c.APIUrl {
t.Errorf("got: %s, want: %s", c.APIUrl, tt.expected.APIUrl)
}
}
if tt.expected.APIKey != "" {
if tt.expected.APIKey != c.APIKey {
t.Errorf("got: %s, want: %s", c.APIKey, tt.expected.APIKey)
}
}
if tt.expected.TickerInterval != "" {
if tt.expected.TickerInterval != c.TickerInterval {
t.Errorf("got: %s, want: %s", c.TickerInterval, tt.expected.TickerInterval)
}
}
if tt.expected.EnableStreaming != nil {
if *tt.expected.EnableStreaming != *c.EnableStreaming {
t.Errorf("got: %t, want: %t", *c.EnableStreaming, *tt.expected.EnableStreaming)
}
}
if tt.expected.EnableHardFails != nil {
if *tt.expected.EnableHardFails != *c.EnableHardFails {
t.Errorf("got: %t, want: %t", *c.EnableHardFails, *tt.expected.EnableHardFails)
}
}
assert.NoError(t, err)

app, ok := jsonApp.(httpcaddyfile.App)
require.True(t, ok)
assert.Equal(t, "crowdsec", app.Name)

var c CrowdSec
err = json.Unmarshal(app.Value, &c)
require.NoError(t, err)

assert.Equal(t, tt.expected.APIUrl, c.APIUrl)
assert.Equal(t, tt.expected.APIKey, c.APIKey)
assert.Equal(t, tt.expected.TickerInterval, c.TickerInterval)
assert.Equal(t, tt.expected.isStreamingEnabled(), c.isStreamingEnabled())
assert.Equal(t, tt.expected.shouldFailHard(), c.shouldFailHard())
})
}
}
Loading
Loading