Compare commits

...
14 Commits
Author SHA1 Message Date
maxlerebourg 4708d76854 Add variable to not block if redis is unreachable (#214)
*  Add variable to not block if redis is unreachable

* 🚨 fix lint

* 📝 Update README.md
2025-02-10 20:10:29 +01:00
maxlerebourg c34d7f4617 🍱 Fix lint (#211) 2025-01-29 08:14:15 +01:00
mathieuHaandMax Lerebourg 0e9620bfe9 👷 chore(ci) bump CI and automate dep updates (#210)
* 👷 chore(ci) bump CI and automate dep updates

* 🚨 chore(go) fix golang lint

* 🍱 fix lint

---------

Co-authored-by: Max Lerebourg <maxlerebourg@gmail.com>
2025-01-29 08:07:04 +01:00
maxlerebourg 92f05b0ba5 [BREAKING-CHANGE] Add CrowdsecAppsecBodyLimit (#208)
*  Add CrowdsecAppsecBodyLimit

* 🍱 fix lint

* 🍱 fix lint

* 🍱 fix error on main
2025-01-24 21:04:45 +01:00
980a7dd05e Add AppSec Path Variable (#202)
* Added Appsec Path config Variable

*  Add path env var for lapi and appsec

* 🍱 Update README.md

---------

Co-authored-by: Tobias Heinze <tobias.heinze@telekom.de>
Co-authored-by: Max Lerebourg <maxlerebourg@gmail.com>
2025-01-24 20:12:30 +01:00
mathieuHaandmaxlerebourg 5c8a60118f 🐛 fix(user-agent) add version in ua to reduce warning logs from crowd… (#195)
* 🐛 fix(user-agent) add version in ua to reduce warning logs from crowdsec LAPI

* 🐛 fix(user-agent) add version in ua to reduce warning logs from crowdsec LAPI

* 🐛 fix(user-agent) remove whitespaces

* 🐛 fix(user-agent) Add Cap for Bouncer

---------

Co-authored-by: maxlerebourg <maxlerebourg@gmail.com>
2024-10-24 21:39:53 +02:00
mathieuHa 8fb0a016b6 Add Traefik Crowdsec Plugin Basic User Agent (#192)
*  Add Traefik Crowdsec Plugin Basic User Agent
2024-10-05 13:15:34 +02:00
maxlerebourg 45d5f38c4d add remediation header when plugin made decision (#189)
*  add remediation header when plugin made decision

* 🍱 add documentation
2024-09-25 19:30:27 +02:00
maxlerebourgandmax.lerebourg f1de1c924e 🔨 174 fix classname for Turnstile (#178)
Co-authored-by: max.lerebourg <max.lerebourg@monisnap.com>
2024-06-19 20:13:49 +02:00
mathieuHa 36e6043c32 📝 doc(cache) Specify that local cache is in memory and not in the filesystem (#177) 2024-06-09 16:20:55 +02:00
maxlerebourg 123cf15434 Add CrowdsecAppsecUnreachableBlock (#175)
*  Add CrowdsecAppsecUnreachableBlock

* 🍱 update readme

* 🍱 fix lint

* 🍱 fix lint
2024-06-09 10:59:45 +02:00
mathieuHa f89c5e25a9 ✏️ fix(validation) Replace typo on CaptchaProvider config validation (#176) 2024-06-09 10:47:17 +02:00
maxlerebourg 58946d9fa2 📄 Update README.md for lang doc (#170) 2024-05-22 11:24:11 +02:00
mathieuHaandMax Lerebourg 6187a722ca 167 feature update to go 122 (#168)
* ⬆️ Upgrade golang version

* 🚨 Optimize Lint for strings

* 🔒️ Add allow list of packages

* 🚨 Fix final lint

* 👷 Update ci

* 🍱 upgrade dependencies

* 🍱 fix comment

---------

Co-authored-by: Max Lerebourg <maxlerebourg@gmail.com>
2024-05-18 13:20:14 +02:00
18 changed files with 323 additions and 240 deletions
+24
View File
@@ -0,0 +1,24 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
# Maintain dependencies for Go
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"
# Maintain dependencies for build tools
- package-ecosystem: "gomod"
directory: "/tools"
schedule:
interval: "weekly"
# Maintain dependencies for GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
-47
View File
@@ -1,47 +0,0 @@
name: Go Matrix
on: [push, pull_request]
jobs:
cross:
name: Go
runs-on: ${{ matrix.os }}
env:
CGO_ENABLED: 0
strategy:
matrix:
go-version: [ 1.19, 1.x ]
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
# https://github.com/marketplace/actions/setup-go-environment
- name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
# https://github.com/marketplace/actions/checkout
- name: Checkout code
uses: actions/checkout@v2
# https://github.com/marketplace/actions/cache
- name: Cache Go modules
uses: actions/cache@v2
with:
# In order:
# * Module download cache
# * Build cache (Linux)
# * Build cache (Mac)
# * Build cache (Windows)
path: |
~/go/pkg/mod
~/.cache/go-build
~/Library/Caches/go-build
%LocalAppData%\go-build
key: ${{ runner.os }}-${{ matrix.go-version }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-${{ matrix.go-version }}-go-
- name: Test
run: go test -v -cover ./...
+6 -6
View File
@@ -12,9 +12,9 @@ jobs:
name: Main Process
runs-on: ubuntu-latest
env:
GO_VERSION: 1.19
GOLANGCI_LINT_VERSION: v1.50.0
YAEGI_VERSION: v0.14.2
GO_VERSION: 1.23
GOLANGCI_LINT_VERSION: v1.63.4
YAEGI_VERSION: v0.16.1
CGO_ENABLED: 0
defaults:
run:
@@ -24,20 +24,20 @@ jobs:
# https://github.com/marketplace/actions/setup-go-environment
- name: Set up Go ${{ env.GO_VERSION }}
uses: actions/setup-go@v2
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
# https://github.com/marketplace/actions/checkout
- name: Check out code
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
path: go/src/github.com/${{ github.repository }}
fetch-depth: 0
# https://github.com/marketplace/actions/cache
- name: Cache Go modules
uses: actions/cache@v2
uses: actions/cache@v4
with:
path: ${{ github.workspace }}/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+27 -20
View File
@@ -1,15 +1,11 @@
run:
timeout: 3m
skip-files: []
skip-dirs: []
linters-settings:
govet:
enable-all: true
disable:
- fieldalignment
golint:
min-confidence: 0
gocyclo:
min-complexity: 15
goconst:
@@ -25,24 +21,37 @@ linters-settings:
- FIXME
gofumpt:
extra-rules: true
depguard:
rules:
Main:
files:
- $all
- "!$test"
allow:
- $gostd
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/ip
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/configuration
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/cache
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/captcha
- github.com/leprosus/golang-ttl-map
- github.com/maxlerebourg/simpleredis
Test:
files:
- $test
allow:
- $gostd
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/ip
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/configuration
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/cache
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/captcha
linters:
enable-all: true
disable:
- deadcode # deprecated
- exhaustivestruct # deprecated
- golint # deprecated
- ifshort # deprecated
- interfacer # deprecated
- maligned # deprecated
- nosnakecase # deprecated
- scopelint # deprecated
- scopelint # deprecated
- structcheck # deprecated
- varcheck # deprecated
- sqlclosecheck # not relevant (SQL)
- rowserrcheck # not relevant (SQL)
- execinquery # not relevant (SQL)
- cyclop # duplicate of gocyclo
- bodyclose # Too many false positives: https://github.com/timakin/bodyclose/issues/30
- dupl
@@ -53,21 +62,19 @@ linters:
- wsl
- exhaustive
- exhaustruct
- goerr113
- err113
- wrapcheck
- ifshort
- noctx
- lll
- gomnd
- forbidigo
- varnamelen
- wastedassign # is disabled because of generics
- gofumpt
- gci
- mnd
issues:
exclude-use-default: false
max-per-linter: 0
max-same-issues: 0
exclude:
- "G402: TLS InsecureSkipVerify may be true."
+41 -8
View File
@@ -36,7 +36,7 @@ On successfull completion, he will be cleaned for a specified period of time bef
The following captcha providers are supported now:
- [hcaptcha](https://www.hcaptcha.com/)
- [recaptcha](https://www.google.com/recaptcha/about/)
- [turnstile](https://www.cloudflare.com/fr-fr/products/turnstile/)
- [turnstile](https://www.cloudflare.com/products/turnstile/)
There are 5 operating modes (CrowdsecMode) for this plugin:
@@ -51,7 +51,7 @@ There are 5 operating modes (CrowdsecMode) for this plugin:
The `streaming mode` is recommended for performance, decisions are updated every 60 sec by default and that's the only communication between Traefik and Crowdsec. Every request that happens hits the cache for quick decisions.
The cache can be local to Traefik using the filesystem, or a separate Redis instance.
The cache can be local to Traefik in memory or using a separate Redis instance.
Below are Mermaid diagrams detailling how each mode work:
@@ -310,6 +310,9 @@ make run
*This means if an IP is banned, all services which are protected by an instance of the plugin will deny requests from that IP*
Only one instance of the plugin is *possible*.
**/!\ Appsec maximum body limit is defaulted to 10MB**
*By careful when you upgrade to >1.4.x*
### Variables
- Enabled
- bool
@@ -329,10 +332,22 @@ Only one instance of the plugin is *possible*.
- string
- default: "crowdsec:7422"
- Crowdsec Appsec Server available on which host and port. The scheme will be handled by the CrowdsecLapiScheme var.
- CrowdsecAppsecPath
- string
- default: "/"
- Crowdsec Appsec Server available on this path. Will be appended to CrowdsecAppsecHost. Need to finish with "/".
- CrowdsecAppsecFailureBlock
- bool
- default: true
- Block request when Crowdsec Appsec Server have a [status 500](https://docs.crowdsec.net/docs/next/appsec/protocol#response-code).
- CrowdsecAppsecUnreachableBlock
- bool
- default: true
- Block request when Crowdsec Appsec Server is unreachable.
- CrowdsecAppsecBodyLimit
- int64
- default: 10485760 (= 10MB)
- Transmit only the first number of bytes to Crowdsec Appsec Server.
- CrowdsecLapiScheme
- string
- default: `http`, expected values are: `http`, `https`
@@ -340,6 +355,10 @@ Only one instance of the plugin is *possible*.
- string
- default: "crowdsec:8080"
- Crowdsec LAPI available on which host and port.
- CrowdsecLapiPath
- string
- default: "/"
- Crowdsec LAPI Server available on this path. Will be appended to CrowdsecLapiHost. Need to finish with "/".
- CrowdsecLapiKey
- string
- default: ""
@@ -364,18 +383,22 @@ Only one instance of the plugin is *possible*.
- string
- default: []
- List of client IPs to trust, they will bypass any check from the bouncer or cache (useful for LAN or VPN IP)
- ForwardedHeadersTrustedIPs
- []string
- default: []
- List of IPs of trusted Proxies that are in front of traefik (ex: Cloudflare)
- RemediationHeadersCustomName
- string
- default: ""
- Name of the header you want in response when request are cancelled (possible value of the header `ban` or `captcha`)
- ForwardedHeadersCustomName
- string
- default: "X-Forwarded-For"
- Name of the header where the real IP of the client should be retrieved
- ForwardedHeadersTrustedIPs
- []string
- default: []
- List of IPs of trusted Proxies that are in front of traefik (ex: Cloudflare)
- RedisCacheEnabled
- bool
- default: false
- enable Redis cache instead of filesystem cache
- enable Redis cache instead of in-memory cache
- RedisCacheHost
- string
- default: "redis:6379"
@@ -388,6 +411,10 @@ Only one instance of the plugin is *possible*.
- string
- default: ""
- Database selection for the Redis service
- RedisUnreachableBlock
- bool
- default: true
- Block request when Redis is unreachable (if Redis is unreachable, 1-second delay is added to each request)
- HTTPTimeoutSeconds
- int64
- default: 10
@@ -485,11 +512,15 @@ http:
crowdsecMode: live
crowdsecAppsecEnabled: false
crowdsecAppsecHost: crowdsec:7422
crowdsecAppsecPath: "/"
crowdsecAppsecFailureBlock: true
crowdsecAppsecUnreachableBlock: true
crowdsecAppsecBodyLimit: 10485760
crowdsecLapiKey: privateKey-foo
crowdsecLapiKeyFile: /etc/traefik/cs-privateKey-foo
crowdsecLapiHost: crowdsec:8080
crowdsecLapiScheme: http
crowdsecLapiHost: crowdsec:8080
crowdsecLapiPath: "/"
crowdsecLapiTLSInsecureVerify: false
crowdsecCapiMachineId: login
crowdsecCapiPassword: password
@@ -503,10 +534,12 @@ http:
clientTrustedIPs:
- 192.168.1.0/24
forwardedHeadersCustomName: X-Custom-Header
remediationHeadersCustomName: cs-remediation
redisCacheEnabled: false
redisCacheHost: "redis:6379"
redisCachePassword: password
redisCacheDatabase: "5"
redisCacheUnreachableBlock: true
crowdsecLapiTLSCertificateAuthority: |-
-----BEGIN CERTIFICATE-----
MIIEBzCCAu+gAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwgZQxCzAJBgNVBAYTAlVT
+109 -72
View File
@@ -1,5 +1,5 @@
// Package crowdsec_bouncer_traefik_plugin implements a middleware that communicates with crowdsec.
// It can cache results to filesystem or redis, or even ask crowdsec for every requests.
// It can cache results in memory or using redis, or even ask crowdsec for every requests.
package crowdsec_bouncer_traefik_plugin //nolint:revive,stylecheck
import (
@@ -7,6 +7,7 @@ import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
@@ -58,38 +59,44 @@ type Bouncer struct {
name string
template *template.Template
enabled bool
appsecEnabled bool
appsecHost string
appsecFailureBlock bool
crowdsecScheme string
crowdsecHost string
crowdsecKey string
crowdsecMode string
crowdsecMachineID string
crowdsecPassword string
crowdsecScenarios []string
updateInterval int64
updateMaxFailure int
defaultDecisionTimeout int64
customHeader string
crowdsecStreamRoute string
crowdsecHeader string
banTemplateString string
clientPoolStrategy *ip.PoolStrategy
serverPoolStrategy *ip.PoolStrategy
httpClient *http.Client
cacheClient *cache.Client
captchaClient *captcha.Client
log *logger.Log
enabled bool
appsecEnabled bool
appsecHost string
appsecPath string
appsecFailureBlock bool
appsecUnreachableBlock bool
appsecBodyLimit int64
crowdsecScheme string
crowdsecHost string
crowdsecPath string
crowdsecKey string
crowdsecMode string
crowdsecMachineID string
crowdsecPassword string
crowdsecScenarios []string
updateInterval int64
updateMaxFailure int
defaultDecisionTimeout int64
remediationCustomHeader string
forwardedCustomHeader string
crowdsecStreamRoute string
crowdsecHeader string
redisUnreachableBlock bool
banTemplateString string
clientPoolStrategy *ip.PoolStrategy
serverPoolStrategy *ip.PoolStrategy
httpClient *http.Client
cacheClient *cache.Client
captchaClient *captcha.Client
log *logger.Log
}
// New creates the crowdsec bouncer plugin.
func New(ctx context.Context, next http.Handler, config *configuration.Config, name string) (http.Handler, error) {
func New(_ context.Context, next http.Handler, config *configuration.Config, name string) (http.Handler, error) {
log := logger.New(config.LogLevel)
err := configuration.ValidateParams(config)
if err != nil {
log.Error(fmt.Sprintf("New:validateParams %s", err.Error()))
log.Error("New:validateParams " + err.Error())
return nil, err
}
@@ -102,8 +109,10 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
if config.CrowdsecMode == configuration.AloneMode {
config.CrowdsecCapiMachineID, _ = configuration.GetVariable(config, "CrowdsecCapiMachineID")
config.CrowdsecCapiPassword, _ = configuration.GetVariable(config, "CrowdsecCapiPassword")
config.CrowdsecLapiScheme = configuration.HTTPS
config.CrowdsecLapiHost = crowdsecCapiHost
config.CrowdsecLapiScheme = "https"
config.CrowdsecLapiPath = "/"
config.CrowdsecAppsecEnabled = false
config.UpdateIntervalSeconds = 7200 // 2 hours
crowdsecStreamRoute = crowdsecCapiStreamRoute
crowdsecHeader = crowdsecCapiHeader
@@ -112,13 +121,13 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
crowdsecHeader = crowdsecLapiHeader
tlsConfig, err = configuration.GetTLSConfigCrowdsec(config, log)
if err != nil {
log.Error(fmt.Sprintf("New:getTLSConfigCrowdsec fail to get tlsConfig %s", err.Error()))
log.Error("New:getTLSConfigCrowdsec fail to get tlsConfig " + err.Error())
return nil, err
}
apiKey, errAPIKey := configuration.GetVariable(config, "CrowdsecLapiKey")
if errAPIKey != nil && len(tlsConfig.Certificates) == 0 {
log.Error(fmt.Sprintf("New:crowdsecLapiKey fail to get CrowdsecLapiKey and no client certificate setup %s", errAPIKey.Error()))
return nil, err
log.Error("New:crowdsecLapiKey fail to get CrowdsecLapiKey and no client certificate setup " + errAPIKey.Error())
return nil, errAPIKey
}
config.CrowdsecLapiKey = apiKey
}
@@ -129,7 +138,7 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
banTemplate, _ := configuration.GetHTMLTemplate(config.BanHTMLFilePath)
err = banTemplate.Execute(&buf, nil)
if err != nil {
log.Error(fmt.Sprintf("New:banTemplate is bad formatted %s", err.Error()))
log.Error("New:banTemplate is bad formatted " + err.Error())
return nil, err
}
banTemplateString = buf.String()
@@ -140,25 +149,31 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
name: name,
template: template.New("CrowdsecBouncer").Delims("[[", "]]"),
enabled: config.Enabled,
crowdsecMode: config.CrowdsecMode,
appsecEnabled: config.CrowdsecAppsecEnabled,
appsecHost: config.CrowdsecAppsecHost,
appsecFailureBlock: config.CrowdsecAppsecFailureBlock,
crowdsecScheme: config.CrowdsecLapiScheme,
crowdsecHost: config.CrowdsecLapiHost,
crowdsecKey: config.CrowdsecLapiKey,
crowdsecMachineID: config.CrowdsecCapiMachineID,
crowdsecPassword: config.CrowdsecCapiPassword,
crowdsecScenarios: config.CrowdsecCapiScenarios,
updateInterval: config.UpdateIntervalSeconds,
updateMaxFailure: config.UpdateMaxFailure,
customHeader: config.ForwardedHeadersCustomName,
defaultDecisionTimeout: config.DefaultDecisionSeconds,
banTemplateString: banTemplateString,
crowdsecStreamRoute: crowdsecStreamRoute,
crowdsecHeader: crowdsecHeader,
log: log,
enabled: config.Enabled,
crowdsecMode: config.CrowdsecMode,
appsecEnabled: config.CrowdsecAppsecEnabled,
appsecHost: config.CrowdsecAppsecHost,
appsecPath: config.CrowdsecAppsecPath,
appsecFailureBlock: config.CrowdsecAppsecFailureBlock,
appsecUnreachableBlock: config.CrowdsecAppsecUnreachableBlock,
appsecBodyLimit: config.CrowdsecAppsecBodyLimit,
crowdsecScheme: config.CrowdsecLapiScheme,
crowdsecHost: config.CrowdsecLapiHost,
crowdsecPath: config.CrowdsecLapiPath,
crowdsecKey: config.CrowdsecLapiKey,
crowdsecMachineID: config.CrowdsecCapiMachineID,
crowdsecPassword: config.CrowdsecCapiPassword,
crowdsecScenarios: config.CrowdsecCapiScenarios,
updateInterval: config.UpdateIntervalSeconds,
updateMaxFailure: config.UpdateMaxFailure,
remediationCustomHeader: config.RemediationHeadersCustomName,
forwardedCustomHeader: config.ForwardedHeadersCustomName,
defaultDecisionTimeout: config.DefaultDecisionSeconds,
redisUnreachableBlock: config.RedisCacheUnreachableBlock,
banTemplateString: banTemplateString,
crowdsecStreamRoute: crowdsecStreamRoute,
crowdsecHeader: crowdsecHeader,
log: log,
serverPoolStrategy: &ip.PoolStrategy{
Checker: serverChecker,
},
@@ -199,6 +214,7 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
config.CaptchaProvider,
config.CaptchaSiteKey,
config.CaptchaSecretKey,
config.RemediationHeadersCustomName,
config.CaptchaHTMLFilePath,
config.CaptchaGracePeriodSeconds,
)
@@ -209,7 +225,7 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
if (config.CrowdsecMode == configuration.StreamMode || config.CrowdsecMode == configuration.AloneMode) && ticker == nil {
if config.CrowdsecMode == configuration.AloneMode {
if err := getToken(bouncer); err != nil {
bouncer.log.Error(fmt.Sprintf("New:getToken %s", err.Error()))
bouncer.log.Error("New:getToken " + err.Error())
return nil, err
}
}
@@ -219,22 +235,22 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
handleStreamTicker(bouncer)
})
}
bouncer.log.Debug(fmt.Sprintf("New initialized mode:%s", config.CrowdsecMode))
bouncer.log.Debug("New initialized mode:" + config.CrowdsecMode)
return bouncer, nil
}
// ServeHTTP principal function of plugin.
//
//nolint:nestif
//nolint:nestif,gocyclo
func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if !bouncer.enabled {
bouncer.next.ServeHTTP(rw, req)
return
}
// Here we check for the trusted IPs in the customHeader
remoteIP, err := ip.GetRemoteIP(req, bouncer.serverPoolStrategy, bouncer.customHeader)
// Here we check for the trusted IPs in the forwardedCustomHeader
remoteIP, err := ip.GetRemoteIP(req, bouncer.serverPoolStrategy, bouncer.forwardedCustomHeader)
if err != nil {
bouncer.log.Error(fmt.Sprintf("ServeHTTP:getRemoteIp ip:%s %s", remoteIP, err.Error()))
handleBanServeHTTP(bouncer, rw)
@@ -264,6 +280,11 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if cacheErr != nil {
cacheErrString := cacheErr.Error()
bouncer.log.Debug(fmt.Sprintf("ServeHTTP:Get ip:%s isBanned:false %s", remoteIP, cacheErrString))
if !bouncer.redisUnreachableBlock && cacheErrString == cache.CacheUnreachable {
bouncer.log.Error(fmt.Sprintf("ServeHTTP:Get ip:%s redisUnreachable=true", remoteIP))
handleNextServeHTTP(bouncer, remoteIP, rw, req)
return
}
if cacheErrString != cache.CacheMiss {
bouncer.log.Error(fmt.Sprintf("ServeHTTP:Get ip:%s %s", remoteIP, cacheErrString))
handleBanServeHTTP(bouncer, rw)
@@ -334,8 +355,14 @@ func handleBanServeHTTP(bouncer *Bouncer, rw http.ResponseWriter) {
return
}
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
if bouncer.remediationCustomHeader != "" {
rw.Header().Set(bouncer.remediationCustomHeader, "ban")
}
rw.WriteHeader(http.StatusForbidden)
fmt.Fprint(rw, bouncer.banTemplateString)
_, err := fmt.Fprint(rw, bouncer.banTemplateString)
if err != nil {
bouncer.log.Error("handleBanServeHTTP could not write template to ResponseWriter")
}
}
func handleRemediationServeHTTP(bouncer *Bouncer, remoteIP, remediation string, rw http.ResponseWriter, req *http.Request) {
@@ -399,7 +426,7 @@ func handleNoStreamCache(bouncer *Bouncer, remoteIP string) (string, error) {
routeURL := url.URL{
Scheme: bouncer.crowdsecScheme,
Host: bouncer.crowdsecHost,
Path: crowdsecLapiRoute,
Path: bouncer.crowdsecPath + crowdsecLapiRoute,
RawQuery: fmt.Sprintf("ip=%v&banned=true", remoteIP),
}
body, err := crowdsecQuery(bouncer, routeURL.String(), false)
@@ -443,7 +470,7 @@ func handleNoStreamCache(bouncer *Bouncer, remoteIP string) (string, error) {
case "captcha":
value = cache.CaptchaValue
default:
bouncer.log.Debug(fmt.Sprintf("handleStreamCache:unknownType %s", decision.Type))
bouncer.log.Debug("handleStreamCache:unknownType " + decision.Type)
}
if isLiveMode {
durationSecond := int64(duration.Seconds())
@@ -452,7 +479,7 @@ func handleNoStreamCache(bouncer *Bouncer, remoteIP string) (string, error) {
}
bouncer.cacheClient.Set(remoteIP, value, durationSecond)
}
return value, fmt.Errorf("handleNoStreamCache:banned")
return value, errors.New("handleNoStreamCache:banned")
}
func getToken(bouncer *Bouncer) error {
@@ -495,7 +522,7 @@ func handleStreamCache(bouncer *Bouncer) error {
streamRouteURL := url.URL{
Scheme: bouncer.crowdsecScheme,
Host: bouncer.crowdsecHost,
Path: bouncer.crowdsecStreamRoute,
Path: bouncer.crowdsecPath + bouncer.crowdsecStreamRoute,
RawQuery: fmt.Sprintf("startup=%t", !isCrowdsecStreamHealthy || isStartup),
}
body, err := crowdsecQuery(bouncer, streamRouteURL.String(), false)
@@ -517,7 +544,7 @@ func handleStreamCache(bouncer *Bouncer) error {
case "captcha":
value = cache.CaptchaValue
default:
bouncer.log.Debug(fmt.Sprintf("handleStreamCache:unknownType %s", decision.Type))
bouncer.log.Debug("handleStreamCache:unknownType " + decision.Type)
}
bouncer.cacheClient.Set(decision.Value, value, int64(duration.Seconds()))
}
@@ -543,13 +570,15 @@ func crowdsecQuery(bouncer *Bouncer, stringURL string, isPost bool) ([]byte, err
req, _ = http.NewRequest(http.MethodGet, stringURL, nil)
}
req.Header.Add(bouncer.crowdsecHeader, bouncer.crowdsecKey)
req.Header.Add("User-Agent", "Crowdsec-Bouncer-Traefik-Plugin/1.X.X")
res, err := bouncer.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("crowdsecQuery url:%s %w", stringURL, err)
return nil, fmt.Errorf("crowdsecQuery:unreachable url:%s %w", stringURL, err)
}
defer func() {
if err = res.Body.Close(); err != nil {
bouncer.log.Error(fmt.Sprintf("crowdsecQuery:closeBody %s", err.Error()))
bouncer.log.Error("crowdsecQuery:closeBody " + err.Error())
}
}()
if res.StatusCode == http.StatusUnauthorized && bouncer.crowdsecMode == configuration.AloneMode {
@@ -573,15 +602,19 @@ func appsecQuery(bouncer *Bouncer, ip string, httpReq *http.Request) error {
routeURL := url.URL{
Scheme: bouncer.crowdsecScheme,
Host: bouncer.appsecHost,
Path: "/",
Path: bouncer.appsecPath,
}
var req *http.Request
if httpReq.Body != nil && httpReq.ContentLength > 0 {
bodyBytes, err := io.ReadAll(httpReq.Body)
if bouncer.appsecBodyLimit > 0 && httpReq.Body != nil && httpReq.ContentLength > 0 {
var bodyBuffer bytes.Buffer
limitedReader := io.LimitReader(httpReq.Body, bouncer.appsecBodyLimit)
teeReader := io.TeeReader(limitedReader, &bodyBuffer)
bodyBytes, err := io.ReadAll(teeReader)
if err != nil {
return fmt.Errorf("appsecQuery:GetBody %w", err)
}
httpReq.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
// Conserve body intact after reading it for other middlewares and service
httpReq.Body = io.NopCloser(io.MultiReader(&bodyBuffer, httpReq.Body))
req, _ = http.NewRequest(http.MethodPost, routeURL.String(), bytes.NewBuffer(bodyBytes))
} else {
req, _ = http.NewRequest(http.MethodGet, routeURL.String(), nil)
@@ -601,17 +634,21 @@ func appsecQuery(bouncer *Bouncer, ip string, httpReq *http.Request) error {
res, err := bouncer.httpClient.Do(req)
if err != nil {
return fmt.Errorf("appsecQuery %w", err)
bouncer.log.Error("appsecQuery:unreachable")
if bouncer.appsecUnreachableBlock {
return fmt.Errorf("appsecQuery:unreachable %w", err)
}
return nil
}
defer func() {
if err = res.Body.Close(); err != nil {
bouncer.log.Error(fmt.Sprintf("appsecQuery:closeBody %s", err.Error()))
bouncer.log.Error("appsecQuery:closeBody " + err.Error())
}
}()
if res.StatusCode == http.StatusInternalServerError {
bouncer.log.Debug("crowdsecQuery statusCode:500")
bouncer.log.Info("appsecQuery:failure")
if bouncer.appsecFailureBlock {
return fmt.Errorf("appsecQuery statusCode:%d", res.StatusCode)
return errors.New("appsecQuery statusCode:500")
}
return nil
}
+4 -4
View File
@@ -18,7 +18,7 @@ func TestServeHTTP(t *testing.T) {
cfg.CrowdsecLapiKey = "test"
ctx := context.Background()
next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {})
next := http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {})
handler, err := New(ctx, next, cfg, "demo-plugin")
if err != nil {
@@ -75,7 +75,7 @@ func TestBouncer_ServeHTTP(t *testing.T) {
crowdsecMode string
updateInterval int64
defaultDecisionTimeout int64
customHeader string
forwardedCustomHeader string
clientPoolStrategy *ip.PoolStrategy
serverPoolStrategy *ip.PoolStrategy
httpClient *http.Client
@@ -93,7 +93,7 @@ func TestBouncer_ServeHTTP(t *testing.T) {
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Run(tt.name, func(_ *testing.T) {
bouncer := &Bouncer{
next: tt.fields.next,
name: tt.fields.name,
@@ -105,7 +105,7 @@ func TestBouncer_ServeHTTP(t *testing.T) {
crowdsecMode: tt.fields.crowdsecMode,
updateInterval: tt.fields.updateInterval,
defaultDecisionTimeout: tt.fields.defaultDecisionTimeout,
customHeader: tt.fields.customHeader,
forwardedCustomHeader: tt.fields.forwardedCustomHeader,
clientPoolStrategy: tt.fields.clientPoolStrategy,
serverPoolStrategy: tt.fields.serverPoolStrategy,
httpClient: tt.fields.httpClient,
+3 -1
View File
@@ -3,7 +3,9 @@ You need to create a crowdsec API credentials for the CAPI.
You can follow the documentation here: https://docs.crowdsec.net/docs/central_api/intro
```bash
curl -X POST "https://api.crowdsec.net/v2/watchers" -H "accept: application/json" -H "Content-Type: application/json" -d "{ \"password\": \"PASSWORD\", \"machine_id\": \"LOGIN\"}"
LOGIN=...
PASSWORD=...
curl -X POST "https://api.crowdsec.net/v2/watchers" -H "accept: application/json" -H "Content-Type: application/json" -d "{ \"password\": \"$PASSWORD\", \"machine_id\": \"$LOGIN\"}"
```
These CAPI credentials must be set in your docker-compose.yml or in your config files
+1 -2
View File
@@ -35,8 +35,7 @@ services:
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
# - "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecmode=alone"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecMode=alone"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.CrowdsecCapiMachineId=FIXME"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.CrowdsecCapiPassword=FIXME"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseccapiscenarios=crowdsecurity/sshd,crowdsecurity/asterisk_bf,crowdsecurity/asterisk_user_enum,crowdsecurity/base-http-scenarios"
+2 -2
View File
@@ -1,8 +1,8 @@
module github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
go 1.19
go 1.22
require (
github.com/leprosus/golang-ttl-map v1.1.7
github.com/maxlerebourg/simpleredis v1.0.9
github.com/maxlerebourg/simpleredis v1.0.11
)
+2 -2
View File
@@ -1,4 +1,4 @@
github.com/leprosus/golang-ttl-map v1.1.7 h1:cF4AAFDDnJTFSV+/42sKLhmMluvLdRlCGS2UaifH6UM=
github.com/leprosus/golang-ttl-map v1.1.7/go.mod h1:4QWHJPeVBbrkhOhXdhCv9IEiyj/YzkO04/iexy4vSe0=
github.com/maxlerebourg/simpleredis v1.0.9 h1:aj1hKaYPeOVE4Ksu3TV/zsreUDDWOpKXBAvoFysiqII=
github.com/maxlerebourg/simpleredis v1.0.9/go.mod h1:/DH8zOK6kDskSqoX/m5CJJdNGfkIQZd/ERBJgytDDSk=
github.com/maxlerebourg/simpleredis v1.0.11 h1:B33TUeIrHtJH2/Qj2bRdU+UZ1BvZwFyP55JWMxHirWg=
github.com/maxlerebourg/simpleredis v1.0.11/go.mod h1:lT4LX02SOsE9PxUcSrz1QW5ZnO86gPbaiYBxmtcXEls=
+12 -5
View File
@@ -3,6 +3,7 @@
package cache
import (
"errors"
"fmt"
ttl_map "github.com/leprosus/golang-ttl-map"
@@ -22,6 +23,8 @@ const (
CaptchaDoneValue = "d"
// CacheMiss error string when cache is miss.
CacheMiss = "cache:miss"
// CacheUnreachable error string when cache is unreachable.
CacheUnreachable = "cache:unreachable"
)
//nolint:gochecknoglobals
@@ -38,7 +41,7 @@ func (localCache) get(key string) (string, error) {
if isCached && isValid && len(valueString) > 0 {
return valueString, nil
}
return "", fmt.Errorf(CacheMiss)
return "", errors.New(CacheMiss)
}
func (localCache) set(key, value string, duration int64) {
@@ -59,21 +62,25 @@ func (redisCache) get(key string) (string, error) {
if err == nil && len(valueString) > 0 {
return valueString, nil
}
if err.Error() == simpleredis.RedisMiss {
return "", fmt.Errorf(CacheMiss)
errRedisMessage := err.Error()
if errRedisMessage == simpleredis.RedisMiss {
return "", errors.New(CacheMiss)
}
if errRedisMessage == simpleredis.RedisUnreachable {
return "", errors.New(CacheUnreachable)
}
return "", err
}
func (rc redisCache) set(key, value string, duration int64) {
if err := redis.Set(key, []byte(value), duration); err != nil {
rc.log.Error(fmt.Sprintf("cache:setDecisionRedisCache %s", err.Error()))
rc.log.Error("cache:setDecisionRedisCache" + err.Error())
}
}
func (rc redisCache) delete(key string) {
if err := redis.Del(key); err != nil {
rc.log.Error(fmt.Sprintf("cache:deleteDecisionRedisCache %s", err.Error()))
rc.log.Error("cache:deleteDecisionRedisCache " + err.Error())
}
}
+24 -19
View File
@@ -16,15 +16,16 @@ import (
// Client Captcha client.
type Client struct {
Valid bool
provider string
siteKey string
secretKey string
gracePeriodSeconds int64
captchaTemplate *template.Template
cacheClient *cache.Client
httpClient *http.Client
log *logger.Log
Valid bool
provider string
siteKey string
secretKey string
remediationCustomHeader string
gracePeriodSeconds int64
captchaTemplate *template.Template
cacheClient *cache.Client
httpClient *http.Client
log *logger.Log
}
type infoProvider struct {
@@ -48,14 +49,14 @@ var (
},
configuration.TurnstileProvider: {
js: "https://challenges.cloudflare.com/turnstile/v0/api.js",
key: "cf-captcha",
key: "cf-turnstile",
validate: "https://challenges.cloudflare.com/turnstile/v0/siteverify",
},
}
)
// New Initialize captcha client.
func (c *Client) New(log *logger.Log, cacheClient *cache.Client, httpClient *http.Client, provider, siteKey, secretKey, captchaTemplatePath string, gracePeriodSeconds int64) error {
func (c *Client) New(log *logger.Log, cacheClient *cache.Client, httpClient *http.Client, provider, siteKey, secretKey, remediationCustomHeader, captchaTemplatePath string, gracePeriodSeconds int64) error {
c.Valid = provider != ""
if !c.Valid {
return nil
@@ -63,6 +64,7 @@ func (c *Client) New(log *logger.Log, cacheClient *cache.Client, httpClient *htt
c.siteKey = siteKey
c.secretKey = secretKey
c.provider = provider
c.remediationCustomHeader = remediationCustomHeader
html, _ := configuration.GetHTMLTemplate(captchaTemplatePath)
c.captchaTemplate = html
c.gracePeriodSeconds = gracePeriodSeconds
@@ -76,17 +78,20 @@ func (c *Client) New(log *logger.Log, cacheClient *cache.Client, httpClient *htt
func (c *Client) ServeHTTP(rw http.ResponseWriter, r *http.Request, remoteIP string) {
valid, err := c.Validate(r)
if err != nil {
c.log.Debug(fmt.Sprintf("captcha:ServeHTTP:validate %s", err.Error()))
c.log.Info("captcha:ServeHTTP:validate " + err.Error())
rw.WriteHeader(http.StatusBadRequest)
return
}
if valid {
c.log.Debug("captcha:ServeHTTP captcha:valid")
c.cacheClient.Set(fmt.Sprintf("%s_captcha", remoteIP), cache.CaptchaDoneValue, c.gracePeriodSeconds)
c.cacheClient.Set(remoteIP+"_captcha", cache.CaptchaDoneValue, c.gracePeriodSeconds)
http.Redirect(rw, r, r.URL.String(), http.StatusFound)
return
}
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
if c.remediationCustomHeader != "" {
rw.Header().Set(c.remediationCustomHeader, "captcha")
}
rw.WriteHeader(http.StatusOK)
err = c.captchaTemplate.Execute(rw, map[string]string{
"SiteKey": c.siteKey,
@@ -94,13 +99,13 @@ func (c *Client) ServeHTTP(rw http.ResponseWriter, r *http.Request, remoteIP str
"FrontendKey": captcha[c.provider].key,
})
if err != nil {
c.log.Info(fmt.Sprintf("captcha:ServeHTTP captchaTemplateServe %s", err.Error()))
c.log.Info("captcha:ServeHTTP captchaTemplateServe " + err.Error())
}
}
// Check Verify if the captcha is already done.
func (c *Client) Check(remoteIP string) bool {
value, _ := c.cacheClient.Get(fmt.Sprintf("%s_captcha", remoteIP))
value, _ := c.cacheClient.Get(remoteIP + "_captcha")
passed := value == cache.CaptchaDoneValue
c.log.Debug(fmt.Sprintf("captcha:Check ip:%s pass:%v", remoteIP, passed))
return passed
@@ -113,10 +118,10 @@ type responseProvider struct {
// Validate Verify the captcha from provider API.
func (c *Client) Validate(r *http.Request) (bool, error) {
if r.Method != http.MethodPost {
c.log.Debug(fmt.Sprintf("captcha:Validate invalid method: %s", r.Method))
c.log.Debug("captcha:Validate invalid method: " + r.Method)
return false, nil
}
var response = r.FormValue(fmt.Sprintf("%s-response", captcha[c.provider].key))
var response = r.FormValue(captcha[c.provider].key + "-response")
if response == "" {
c.log.Debug("captcha:Validate no captcha response found in request")
return false, nil
@@ -130,10 +135,10 @@ func (c *Client) Validate(r *http.Request) (bool, error) {
}
defer func() {
if err = res.Body.Close(); err != nil {
c.log.Error(fmt.Sprintf("captcha:Validate %s", err.Error()))
c.log.Error("captcha:Validate " + err.Error())
}
}()
if !strings.Contains(res.Header.Get("content-type"), "application/json") {
if !strings.Contains(res.Header.Get("Content-Type"), "application/json") {
c.log.Debug("captcha:Validate responseType:noJson")
return false, nil
}
+60 -44
View File
@@ -4,6 +4,7 @@ package configuration
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"html/template"
"net/http"
@@ -39,9 +40,13 @@ type Config struct {
CrowdsecMode string `json:"crowdsecMode,omitempty"`
CrowdsecAppsecEnabled bool `json:"crowdsecAppsecEnabled,omitempty"`
CrowdsecAppsecHost string `json:"crowdsecAppsecHost,omitempty"`
CrowdsecAppsecPath string `json:"crowdsecAppsecPath,omitempty"`
CrowdsecAppsecFailureBlock bool `json:"crowdsecAppsecFailureBlock,omitempty"`
CrowdsecAppsecUnreachableBlock bool `json:"crowdsecAppsecUnreachableBlock,omitempty"`
CrowdsecAppsecBodyLimit int64 `json:"crowdsecAppsecBodyLimit,omitempty"`
CrowdsecLapiScheme string `json:"crowdsecLapiScheme,omitempty"`
CrowdsecLapiHost string `json:"crowdsecLapiHost,omitempty"`
CrowdsecLapiPath string `json:"crowdsecLapiPath,omitempty"`
CrowdsecLapiKey string `json:"crowdsecLapiKey,omitempty"`
CrowdsecLapiKeyFile string `json:"crowdsecLapiKeyFile,omitempty"`
CrowdsecLapiTLSInsecureVerify bool `json:"crowdsecLapiTlsInsecureVerify,omitempty"`
@@ -60,6 +65,7 @@ type Config struct {
UpdateMaxFailure int `json:"updateMaxFailure,omitempty"`
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"`
HTTPTimeoutSeconds int64 `json:"httpTimeoutSeconds,omitempty"`
RemediationHeadersCustomName string `json:"remediationHeadersCustomName,omitempty"`
ForwardedHeadersCustomName string `json:"forwardedHeadersCustomName,omitempty"`
ForwardedHeadersTrustedIPs []string `json:"forwardedHeadersTrustedIps,omitempty"`
ClientTrustedIPs []string `json:"clientTrustedIps,omitempty"`
@@ -68,6 +74,7 @@ type Config struct {
RedisCachePassword string `json:"redisCachePassword,omitempty"`
RedisCachePasswordFile string `json:"redisCachePasswordFile,omitempty"`
RedisCacheDatabase string `json:"redisCacheDatabase,omitempty"`
RedisCacheUnreachableBlock bool `json:"redisCacheUnreachableBlock,omitempty"`
BanHTMLFilePath string `json:"banHtmlFilePath,omitempty"`
CaptchaHTMLFilePath string `json:"captchaHtmlFilePath,omitempty"`
CaptchaProvider string `json:"captchaProvider,omitempty"`
@@ -90,33 +97,39 @@ func contains(source []string, target string) bool {
// New creates the default plugin configuration.
func New() *Config {
return &Config{
Enabled: false,
LogLevel: "INFO",
CrowdsecMode: LiveMode,
CrowdsecAppsecEnabled: false,
CrowdsecAppsecHost: "crowdsec:7422",
CrowdsecAppsecFailureBlock: true,
CrowdsecLapiScheme: HTTP,
CrowdsecLapiHost: "crowdsec:8080",
CrowdsecLapiKey: "",
CrowdsecLapiTLSInsecureVerify: false,
UpdateIntervalSeconds: 60,
UpdateMaxFailure: 0,
DefaultDecisionSeconds: 60,
HTTPTimeoutSeconds: 10,
CaptchaProvider: "",
CaptchaSiteKey: "",
CaptchaSecretKey: "",
CaptchaGracePeriodSeconds: 1800,
CaptchaHTMLFilePath: "/captcha.html",
BanHTMLFilePath: "",
ForwardedHeadersCustomName: "X-Forwarded-For",
ForwardedHeadersTrustedIPs: []string{},
ClientTrustedIPs: []string{},
RedisCacheEnabled: false,
RedisCacheHost: "redis:6379",
RedisCachePassword: "",
RedisCacheDatabase: "",
Enabled: false,
LogLevel: "INFO",
CrowdsecMode: LiveMode,
CrowdsecAppsecEnabled: false,
CrowdsecAppsecHost: "crowdsec:7422",
CrowdsecAppsecPath: "/",
CrowdsecAppsecFailureBlock: true,
CrowdsecAppsecUnreachableBlock: true,
CrowdsecAppsecBodyLimit: 10485760,
CrowdsecLapiScheme: HTTP,
CrowdsecLapiHost: "crowdsec:8080",
CrowdsecLapiPath: "/",
CrowdsecLapiKey: "",
CrowdsecLapiTLSInsecureVerify: false,
UpdateIntervalSeconds: 60,
UpdateMaxFailure: 0,
DefaultDecisionSeconds: 60,
HTTPTimeoutSeconds: 10,
CaptchaProvider: "",
CaptchaSiteKey: "",
CaptchaSecretKey: "",
CaptchaGracePeriodSeconds: 1800,
CaptchaHTMLFilePath: "/captcha.html",
BanHTMLFilePath: "",
RemediationHeadersCustomName: "",
ForwardedHeadersCustomName: "X-Forwarded-For",
ForwardedHeadersTrustedIPs: []string{},
ClientTrustedIPs: []string{},
RedisCacheEnabled: false,
RedisCacheHost: "redis:6379",
RedisCachePassword: "",
RedisCacheDatabase: "",
RedisCacheUnreachableBlock: true,
}
}
@@ -124,7 +137,7 @@ func New() *Config {
func GetVariable(config *Config, key string) (string, error) {
value := ""
object := reflect.Indirect(reflect.ValueOf(config))
field := object.FieldByName(fmt.Sprintf("%sFile", key))
field := object.FieldByName(key + "File")
// Here linter say you should simplify this code, but lets not, performance is important not clarity and complexity
fp := field.String()
if fp != "" {
@@ -151,7 +164,7 @@ func GetVariable(config *Config, key string) (string, error) {
func GetHTMLTemplate(path string) (*template.Template, error) {
var err error
if path == "" {
return nil, fmt.Errorf("no html template provided")
return nil, errors.New("no html template provided")
}
//nolint:gosec
b, err := os.ReadFile(path)
@@ -212,11 +225,11 @@ func ValidateParams(config *Config) error {
}
}
if err := validateURL("CrowdsecLapi", config.CrowdsecLapiScheme, config.CrowdsecLapiHost); err != nil {
if err := validateURL("CrowdsecLapi", config.CrowdsecLapiScheme, config.CrowdsecLapiHost, config.CrowdsecLapiPath); err != nil {
return err
}
if err := validateURL("CrowdsecAppsec", config.CrowdsecLapiScheme, config.CrowdsecAppsecHost); err != nil {
if err := validateURL("CrowdsecAppsec", config.CrowdsecLapiScheme, config.CrowdsecAppsecHost, config.CrowdsecAppsecPath); err != nil {
return err
}
@@ -234,7 +247,7 @@ func ValidateParams(config *Config) error {
}
// We need to either have crowdsecLapiKey defined or the BouncerCert and Bouncerkey
if lapiKey == "" && (certBouncer == "" || certBouncerKey == "") {
return fmt.Errorf("CrowdsecLapiKey || (CrowdsecLapiTLSCertificateBouncer && CrowdsecLapiTLSCertificateBouncerKey): cannot be all empty")
return errors.New("CrowdsecLapiKey || (CrowdsecLapiTLSCertificateBouncer && CrowdsecLapiTLSCertificateBouncerKey): cannot be all empty")
} else if lapiKey != "" && (certBouncer == "" || certBouncerKey == "") {
lapiKey = strings.TrimSpace(lapiKey)
if err = validateParamsAPIKey(lapiKey); err != nil {
@@ -252,11 +265,11 @@ func ValidateParams(config *Config) error {
return nil
}
func validateURL(variable, scheme, host string) error {
// This only check that the format of the URL scheme://host is correct and do not make requests
testURL := url.URL{Scheme: scheme, Host: host}
func validateURL(variable, scheme, host, path string) error {
// This only check that the format of the URL scheme://host/path is correct and do not make requests
testURL := url.URL{Scheme: scheme, Host: host, Path: path}
if _, err := http.NewRequest(http.MethodGet, testURL.String(), nil); err != nil {
return fmt.Errorf("CrowdsecLapiScheme://%sHost: '%v://%v' must be an URL", variable, scheme, host)
return fmt.Errorf("CrowdsecLapiScheme://%sHost: '%v://%v%v' must be a valid URL", variable, scheme, host, path)
}
return nil
}
@@ -267,7 +280,7 @@ func validateURL(variable, scheme, host string) error {
// See https://httpwg.github.io/specs/rfc7230.html#rule.token.separators
func validateParamsAPIKey(lapiKey string) error {
reg := regexp.MustCompile("^[a-zA-Z0-9 !#$%&'*+-.^_`|~=/]*$")
if !reg.Match([]byte(lapiKey)) {
if !reg.MatchString(lapiKey) {
return fmt.Errorf("CrowdsecLapiKey doesn't valid this regexp: '/%s/'", reg.String())
}
return nil
@@ -279,12 +292,12 @@ func validateParamsTLS(config *Config) error {
return err
}
if certAuth == "" {
return fmt.Errorf("CrowdsecLapiTLSCertificateAuthority must be specified when CrowdsecLapiScheme='https' and CrowdsecLapiTLSInsecureVerify=false")
return errors.New("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 errors.New("failed parsing pem file")
}
return nil
}
@@ -321,17 +334,20 @@ func validateParamsRequired(config *Config) error {
}
}
if config.UpdateMaxFailure < -1 {
return fmt.Errorf("UpdateMaxFailure: cannot be less than -1")
return errors.New("UpdateMaxFailure: cannot be less than -1")
}
if config.CrowdsecAppsecBodyLimit < 0 {
return errors.New("CrowdsecAppsecBodyLimit: cannot be less than 0")
}
if !contains([]string{NoneMode, LiveMode, StreamMode, AloneMode, AppsecMode}, config.CrowdsecMode) {
return fmt.Errorf("CrowdsecMode: must be one of 'none', 'live', 'stream', 'alone' or 'appsec'")
return errors.New("CrowdsecMode: must be one of 'none', 'live', 'stream', 'alone' or 'appsec'")
}
if !contains([]string{HTTP, HTTPS}, config.CrowdsecLapiScheme) {
return fmt.Errorf("CrowdsecLapiScheme: must be one of 'http' or 'https'")
return errors.New("CrowdsecLapiScheme: must be one of 'http' or 'https'")
}
if !contains([]string{"", HcaptchaProvider, RecaptchaProvider, TurnstileProvider}, config.CaptchaProvider) {
return fmt.Errorf("CrowdsecLapiScheme: must be one of 'hcaptcha', 'recaptcha' or 'turnstile'")
return errors.New("CaptchaProvider: must be one of 'hcaptcha', 'recaptcha' or 'turnstile'")
}
return nil
}
@@ -360,7 +376,7 @@ func GetTLSConfigCrowdsec(config *Config, log *logger.Log) (*tls.Config, error)
if !tlsConfig.RootCAs.AppendCertsFromPEM([]byte(certAuthority)) {
// here we return because if CrowdsecLapiTLSInsecureVerify is false
// and CA not load, we can't communicate with https
return nil, fmt.Errorf("getTLSConfigCrowdsec:cannot load CA and verify cert is enabled")
return nil, errors.New("getTLSConfigCrowdsec:cannot load CA and verify cert is enabled")
}
log.Debug("getTLSConfigCrowdsec:CrowdsecLapiTLSCertificateAuthority CA added successfully")
}
+2 -1
View File
@@ -3,6 +3,7 @@
package ip
import (
"errors"
"fmt"
"net"
"net/http"
@@ -45,7 +46,7 @@ func NewChecker(log *logger.Log, trustedIPs []string) (*Checker, error) {
// Contains checks if provided address is in the trusted IPs.
func (ip *Checker) Contains(addr string) (bool, error) {
if len(addr) == 0 {
return false, fmt.Errorf("Contains:noAddress")
return false, errors.New("Contains:noAddress")
}
ipAddr, err := parseIP(addr)
+3 -3
View File
@@ -34,15 +34,15 @@ func New(logLevel string) *Log {
// Info log to Stdout.
func (l *Log) Info(str string) {
l.logInfo.Printf(str)
l.logInfo.Printf("%s", str)
}
// Debug log to Stdout.
func (l *Log) Debug(str string) {
l.logDebug.Printf(str)
l.logDebug.Printf("%s", str)
}
// Error log to Stderr.
func (l *Log) Error(str string) {
l.logError.Printf(str)
l.logError.Printf("%s", str)
}
+1 -2
View File
@@ -18,7 +18,7 @@ const (
RedisMiss = "redis:miss"
RedisTimeout = "redis:timeout"
RedisNoAuth = "redis:noauth"
RedisIssue = "redis:issue?"
RedisIssue = "redis:issue?"
)
// A redisCmd is used to communicate with redis at low level using commands.
@@ -122,7 +122,6 @@ func (sr *SimpleRedis) askRedis(cmd redisCmd, channel chan redisCmd) redisCmd {
}
read, _ = reader.ReadLineBytes()
return redisCmd{Data: read}
}
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
# github.com/leprosus/golang-ttl-map v1.1.7
## explicit; go 1.15
github.com/leprosus/golang-ttl-map
# github.com/maxlerebourg/simpleredis v1.0.9
## explicit; go 1.19
# github.com/maxlerebourg/simpleredis v1.0.11
## explicit; go 1.22
github.com/maxlerebourg/simpleredis