mirror of
https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin.git
synced 2026-02-05 00:23:42 +01:00
✨ Transform banTemplate to add blocking reason and client IP (#290)
* ✨ Transform banTemplate to add blocking reason * 🍱 fix test * 🍱 fix lint * 🍱 fix test * 🍱 fix lint * 🍱 fix lint * 🍱 add doc and fix lint * 🍱 fix lint * 🍱 fix lint * 🍱 fix lint * 🍱 fix lint * 🍱 fix lint * 🍱 lint html * 🍱 fix comments + fix wicketpeeker readme * 🍱 Give ClientIP in ban page * 🍱 fix test
This commit is contained in:
59
bouncer.go
59
bouncer.go
@@ -9,6 +9,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
htmltemplate "html/template"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -106,7 +107,7 @@ type Bouncer struct {
|
|||||||
crowdsecStreamRoute string
|
crowdsecStreamRoute string
|
||||||
crowdsecHeader string
|
crowdsecHeader string
|
||||||
redisUnreachableBlock bool
|
redisUnreachableBlock bool
|
||||||
banTemplateString string
|
banTemplate *htmltemplate.Template
|
||||||
clientPoolStrategy *ip.PoolStrategy
|
clientPoolStrategy *ip.PoolStrategy
|
||||||
serverPoolStrategy *ip.PoolStrategy
|
serverPoolStrategy *ip.PoolStrategy
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
@@ -159,16 +160,9 @@ func New(_ context.Context, next http.Handler, config *configuration.Config, nam
|
|||||||
config.CrowdsecLapiKey = apiKey
|
config.CrowdsecLapiKey = apiKey
|
||||||
}
|
}
|
||||||
|
|
||||||
var banTemplateString string
|
var banTemplate *htmltemplate.Template
|
||||||
if config.BanHTMLFilePath != "" {
|
if config.BanHTMLFilePath != "" {
|
||||||
var buf bytes.Buffer
|
banTemplate, _ = configuration.GetHTMLTemplate(config.BanHTMLFilePath)
|
||||||
banTemplate, _ := configuration.GetHTMLTemplate(config.BanHTMLFilePath)
|
|
||||||
err = banTemplate.Execute(&buf, nil)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("New:banTemplate is bad formatted " + err.Error())
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
banTemplateString = buf.String()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bouncer := &Bouncer{
|
bouncer := &Bouncer{
|
||||||
@@ -198,7 +192,7 @@ func New(_ context.Context, next http.Handler, config *configuration.Config, nam
|
|||||||
defaultDecisionTimeout: config.DefaultDecisionSeconds,
|
defaultDecisionTimeout: config.DefaultDecisionSeconds,
|
||||||
remediationStatusCode: config.RemediationStatusCode,
|
remediationStatusCode: config.RemediationStatusCode,
|
||||||
redisUnreachableBlock: config.RedisCacheUnreachableBlock,
|
redisUnreachableBlock: config.RedisCacheUnreachableBlock,
|
||||||
banTemplateString: banTemplateString,
|
banTemplate: banTemplate,
|
||||||
crowdsecStreamRoute: crowdsecStreamRoute,
|
crowdsecStreamRoute: crowdsecStreamRoute,
|
||||||
crowdsecHeader: crowdsecHeader,
|
crowdsecHeader: crowdsecHeader,
|
||||||
log: log,
|
log: log,
|
||||||
@@ -296,13 +290,13 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||||||
remoteIP, err := ip.GetRemoteIP(req, bouncer.serverPoolStrategy, bouncer.forwardedCustomHeader)
|
remoteIP, err := ip.GetRemoteIP(req, bouncer.serverPoolStrategy, bouncer.forwardedCustomHeader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
bouncer.log.Error(fmt.Sprintf("ServeHTTP:getRemoteIp ip:%s %s", remoteIP, err.Error()))
|
bouncer.log.Error(fmt.Sprintf("ServeHTTP:getRemoteIp ip:%s %s", remoteIP, err.Error()))
|
||||||
handleBanServeHTTP(bouncer, rw, req.Method)
|
bouncer.handleBanServeHTTP(rw, req, remoteIP, configuration.ReasonTECH)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
isTrusted, err := bouncer.clientPoolStrategy.Checker.Contains(remoteIP)
|
isTrusted, err := bouncer.clientPoolStrategy.Checker.Contains(remoteIP)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
bouncer.log.Error(fmt.Sprintf("ServeHTTP:checkerContains ip:%s %s", remoteIP, err.Error()))
|
bouncer.log.Error(fmt.Sprintf("ServeHTTP:checkerContains ip:%s %s", remoteIP, err.Error()))
|
||||||
handleBanServeHTTP(bouncer, rw, req.Method)
|
bouncer.handleBanServeHTTP(rw, req, remoteIP, configuration.ReasonTECH)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// if our IP is in the trusted list we bypass the next checks
|
// if our IP is in the trusted list we bypass the next checks
|
||||||
@@ -313,7 +307,7 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if bouncer.crowdsecMode == configuration.AppsecMode {
|
if bouncer.crowdsecMode == configuration.AppsecMode {
|
||||||
handleNextServeHTTP(bouncer, remoteIP, rw, req)
|
bouncer.handleNextServeHTTP(rw, req, remoteIP)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,20 +319,20 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||||||
bouncer.log.Debug(fmt.Sprintf("ServeHTTP:Get ip:%s isBanned:false %s", remoteIP, cacheErrString))
|
bouncer.log.Debug(fmt.Sprintf("ServeHTTP:Get ip:%s isBanned:false %s", remoteIP, cacheErrString))
|
||||||
if !bouncer.redisUnreachableBlock && cacheErrString == cache.CacheUnreachable {
|
if !bouncer.redisUnreachableBlock && cacheErrString == cache.CacheUnreachable {
|
||||||
bouncer.log.Error(fmt.Sprintf("ServeHTTP:Get ip:%s redisUnreachable=true", remoteIP))
|
bouncer.log.Error(fmt.Sprintf("ServeHTTP:Get ip:%s redisUnreachable=true", remoteIP))
|
||||||
handleNextServeHTTP(bouncer, remoteIP, rw, req)
|
bouncer.handleNextServeHTTP(rw, req, remoteIP)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if cacheErrString != cache.CacheMiss {
|
if cacheErrString != cache.CacheMiss {
|
||||||
bouncer.log.Error(fmt.Sprintf("ServeHTTP:Get ip:%s %s", remoteIP, cacheErrString))
|
bouncer.log.Error(fmt.Sprintf("ServeHTTP:Get ip:%s %s", remoteIP, cacheErrString))
|
||||||
handleBanServeHTTP(bouncer, rw, req.Method)
|
bouncer.handleBanServeHTTP(rw, req, remoteIP, configuration.ReasonTECH)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
bouncer.log.Debug(fmt.Sprintf("ServeHTTP ip:%s cache:hit isBanned:%v", remoteIP, value))
|
bouncer.log.Debug(fmt.Sprintf("ServeHTTP ip:%s cache:hit isBanned:%v", remoteIP, value))
|
||||||
if value == cache.NoBannedValue {
|
if value == cache.NoBannedValue {
|
||||||
handleNextServeHTTP(bouncer, remoteIP, rw, req)
|
bouncer.handleNextServeHTTP(rw, req, remoteIP)
|
||||||
} else {
|
} else {
|
||||||
handleRemediationServeHTTP(bouncer, remoteIP, value, rw, req)
|
bouncer.handleRemediationServeHTTP(rw, req, remoteIP, value)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -347,18 +341,18 @@ 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.
|
// Right here if we cannot join the stream we forbid the request to go on.
|
||||||
if bouncer.crowdsecMode == configuration.StreamMode || bouncer.crowdsecMode == configuration.AloneMode {
|
if bouncer.crowdsecMode == configuration.StreamMode || bouncer.crowdsecMode == configuration.AloneMode {
|
||||||
if isCrowdsecStreamHealthy {
|
if isCrowdsecStreamHealthy {
|
||||||
handleNextServeHTTP(bouncer, remoteIP, rw, req)
|
bouncer.handleNextServeHTTP(rw, req, remoteIP)
|
||||||
} else {
|
} else {
|
||||||
bouncer.log.Debug(fmt.Sprintf("ServeHTTP isCrowdsecStreamHealthy:false ip:%s updateFailure:%d", remoteIP, updateFailure))
|
bouncer.log.Debug(fmt.Sprintf("ServeHTTP isCrowdsecStreamHealthy:false ip:%s updateFailure:%d", remoteIP, updateFailure))
|
||||||
handleBanServeHTTP(bouncer, rw, req.Method)
|
bouncer.handleBanServeHTTP(rw, req, remoteIP, configuration.ReasonTECH)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
value, err := handleNoStreamCache(bouncer, remoteIP)
|
value, err := handleNoStreamCache(bouncer, remoteIP)
|
||||||
if value == cache.NoBannedValue {
|
if value == cache.NoBannedValue {
|
||||||
handleNextServeHTTP(bouncer, remoteIP, rw, req)
|
bouncer.handleNextServeHTTP(rw, req, remoteIP)
|
||||||
} else {
|
} else {
|
||||||
bouncer.log.Debug(fmt.Sprintf("ServeHTTP:handleNoStreamCache ip:%s isBanned:%v %s", remoteIP, value, err.Error()))
|
bouncer.log.Debug(fmt.Sprintf("ServeHTTP:handleNoStreamCache ip:%s isBanned:%v %s", remoteIP, value, err.Error()))
|
||||||
handleRemediationServeHTTP(bouncer, remoteIP, value, rw, req)
|
bouncer.handleRemediationServeHTTP(rw, req, remoteIP, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -392,48 +386,47 @@ type Login struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// To append Headers we need to call rw.WriteHeader after set any header.
|
// To append Headers we need to call rw.WriteHeader after set any header.
|
||||||
func handleBanServeHTTP(bouncer *Bouncer, rw http.ResponseWriter, method string) {
|
func (bouncer *Bouncer) handleBanServeHTTP(rw http.ResponseWriter, req *http.Request, remoteIP, reason string) {
|
||||||
atomic.AddInt64(&blockedRequests, 1)
|
atomic.AddInt64(&blockedRequests, 1)
|
||||||
|
|
||||||
if bouncer.remediationCustomHeader != "" {
|
if bouncer.remediationCustomHeader != "" {
|
||||||
rw.Header().Set(bouncer.remediationCustomHeader, "ban")
|
rw.Header().Set(bouncer.remediationCustomHeader, "ban")
|
||||||
}
|
}
|
||||||
if bouncer.banTemplateString == "" {
|
if bouncer.banTemplate == nil {
|
||||||
rw.WriteHeader(bouncer.remediationStatusCode)
|
rw.WriteHeader(bouncer.remediationStatusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
|
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
rw.WriteHeader(bouncer.remediationStatusCode)
|
rw.WriteHeader(bouncer.remediationStatusCode)
|
||||||
|
|
||||||
if method == http.MethodHead {
|
if req.Method == http.MethodHead {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_, err := fmt.Fprint(rw, bouncer.banTemplateString)
|
err := bouncer.banTemplate.Execute(rw, map[string]string{"RemediationReason": reason, "ClientIP": remoteIP})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// use warn when https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pull/276 is completed
|
bouncer.log.Error("handleBanServeHTTP banTemplateServe " + err.Error())
|
||||||
bouncer.log.Error("handleBanServeHTTP could not write template to ResponseWriter: " + err.Error())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleRemediationServeHTTP(bouncer *Bouncer, remoteIP, remediation string, rw http.ResponseWriter, req *http.Request) {
|
func (bouncer *Bouncer) handleRemediationServeHTTP(rw http.ResponseWriter, req *http.Request, remoteIP, remediation string) {
|
||||||
bouncer.log.Debug(fmt.Sprintf("handleRemediationServeHTTP ip:%s remediation:%s", remoteIP, remediation))
|
bouncer.log.Debug(fmt.Sprintf("handleRemediationServeHTTP ip:%s remediation:%s", remoteIP, remediation))
|
||||||
if bouncer.captchaClient.Valid && remediation == cache.CaptchaValue && req.Method != http.MethodHead {
|
if bouncer.captchaClient.Valid && remediation == cache.CaptchaValue && req.Method != http.MethodHead {
|
||||||
if bouncer.captchaClient.Check(remoteIP) {
|
if bouncer.captchaClient.Check(remoteIP) {
|
||||||
handleNextServeHTTP(bouncer, remoteIP, rw, req)
|
bouncer.handleNextServeHTTP(rw, req, remoteIP)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
atomic.AddInt64(&blockedRequests, 1) // If we serve a captcha that should count as a dropped request.
|
atomic.AddInt64(&blockedRequests, 1) // If we serve a captcha that should count as a dropped request.
|
||||||
bouncer.captchaClient.ServeHTTP(rw, req, remoteIP)
|
bouncer.captchaClient.ServeHTTP(rw, req, remoteIP)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
handleBanServeHTTP(bouncer, rw, req.Method)
|
bouncer.handleBanServeHTTP(rw, req, remoteIP, configuration.ReasonLAPI)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleNextServeHTTP(bouncer *Bouncer, remoteIP string, rw http.ResponseWriter, req *http.Request) {
|
func (bouncer *Bouncer) handleNextServeHTTP(rw http.ResponseWriter, req *http.Request, remoteIP string) {
|
||||||
if bouncer.appsecEnabled {
|
if bouncer.appsecEnabled {
|
||||||
if err := appsecQuery(bouncer, remoteIP, req); err != nil {
|
if err := appsecQuery(bouncer, remoteIP, req); err != nil {
|
||||||
bouncer.log.Debug(fmt.Sprintf("handleNextServeHTTP ip:%s isWaf:true %s", remoteIP, err.Error()))
|
bouncer.log.Debug(fmt.Sprintf("handleNextServeHTTP ip:%s isWaf:true %s", remoteIP, err.Error()))
|
||||||
handleBanServeHTTP(bouncer, rw, req.Method)
|
bouncer.handleBanServeHTTP(rw, req, remoteIP, configuration.ReasonAPPSEC)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package crowdsec_bouncer_traefik_plugin //nolint:revive,stylecheck
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
htmltemplate "html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"reflect"
|
"reflect"
|
||||||
@@ -188,40 +189,42 @@ func Test_crowdsecQuery(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHandleBanServeHTTPWithDifferentMethods(t *testing.T) {
|
func TestHandleBanServeHTTPWithDifferentMethods(t *testing.T) {
|
||||||
|
html := "<html>You are banned</html>"
|
||||||
|
banTemplate, _ := htmltemplate.New("html").Parse(html)
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
method string
|
method string
|
||||||
banTemplateString string
|
banTemplate *htmltemplate.Template
|
||||||
expectBodyContent bool
|
expectBodyContent bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "GET request should have body with template",
|
name: "GET request should have body with template",
|
||||||
method: http.MethodGet,
|
method: http.MethodGet,
|
||||||
banTemplateString: "<html>You are banned</html>",
|
banTemplate: banTemplate,
|
||||||
expectBodyContent: true,
|
expectBodyContent: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "HEAD request should NOT have body even with template",
|
name: "HEAD request should NOT have body even with template",
|
||||||
method: http.MethodHead,
|
method: http.MethodHead,
|
||||||
banTemplateString: "<html>You are banned</html>",
|
banTemplate: banTemplate,
|
||||||
expectBodyContent: false,
|
expectBodyContent: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "POST request should have body with template",
|
name: "POST request should have body with template",
|
||||||
method: http.MethodPost,
|
method: http.MethodPost,
|
||||||
banTemplateString: "<html>You are banned</html>",
|
banTemplate: banTemplate,
|
||||||
expectBodyContent: true,
|
expectBodyContent: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "PUT request should have body with template",
|
name: "PUT request should have body with template",
|
||||||
method: http.MethodPut,
|
method: http.MethodPut,
|
||||||
banTemplateString: "<html>You are banned</html>",
|
banTemplate: banTemplate,
|
||||||
expectBodyContent: true,
|
expectBodyContent: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "DELETE request should have body with template",
|
name: "DELETE request should have body with template",
|
||||||
method: http.MethodDelete,
|
method: http.MethodDelete,
|
||||||
banTemplateString: "<html>You are banned</html>",
|
banTemplate: banTemplate,
|
||||||
expectBodyContent: true,
|
expectBodyContent: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -229,16 +232,17 @@ func TestHandleBanServeHTTPWithDifferentMethods(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
bouncer := &Bouncer{
|
bouncer := &Bouncer{
|
||||||
remediationStatusCode: 403,
|
remediationStatusCode: http.StatusForbidden,
|
||||||
remediationCustomHeader: "X-Test-Remediation",
|
remediationCustomHeader: "X-Test-Remediation",
|
||||||
banTemplateString: tt.banTemplateString,
|
banTemplate: tt.banTemplate,
|
||||||
}
|
}
|
||||||
|
|
||||||
rw := httptest.NewRecorder()
|
rw := httptest.NewRecorder()
|
||||||
handleBanServeHTTP(bouncer, rw, tt.method)
|
req := &http.Request{Method: tt.method}
|
||||||
|
bouncer.handleBanServeHTTP(rw, req, "0.0.0.0", "TEST")
|
||||||
|
|
||||||
// Check status code
|
// Check status code
|
||||||
if rw.Code != 403 {
|
if rw.Code != http.StatusForbidden {
|
||||||
t.Errorf("Expected status code 403, got %d", rw.Code)
|
t.Errorf("Expected status code 403, got %d", rw.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,12 +262,13 @@ func TestHandleBanServeHTTPWithDifferentMethods(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If we expect body content, verify it matches template
|
// If we expect body content, verify it matches template
|
||||||
if tt.expectBodyContent && body != tt.banTemplateString {
|
if tt.expectBodyContent && body != html {
|
||||||
t.Errorf("Expected body %q, got %q", tt.banTemplateString, body)
|
t.Errorf("Expected body %q, got %q", html, body)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCaptchaMethodBasedLogic(t *testing.T) {
|
func TestCaptchaMethodBasedLogic(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
@@ -45,3 +45,13 @@ To play the demo environment run:
|
|||||||
```bash
|
```bash
|
||||||
make run_custom_ban_page
|
make run_custom_ban_page
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Another thing to note
|
||||||
|
In the html of the ban page, you can use:
|
||||||
|
- {{ .ClientIP }} to display the IP used to ban the request.
|
||||||
|
- {{ .RemediationReason }} that convert on runtime into why the ban page is served. It's an enum with "APPSEC", "LAPI", "TECHNICAL_ISSUE" and it is useful to help user understand why the request is blocked.
|
||||||
|
```
|
||||||
|
<script>var remediation = "{{ .RemediationReason }}"</script>
|
||||||
|
<script>var clientIp = "{{ .ClientIP }}"</script>
|
||||||
|
```
|
||||||
|
With the above tweak and some other js, you can customize your ban page on runtime.
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ wicketkeeper:
|
|||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
environment:
|
environment:
|
||||||
- ROOT_URL=http://localhost:8080
|
|
||||||
- LISTEN_PORT=8080
|
- LISTEN_PORT=8080
|
||||||
- REDIS_ADDR=redis:6379
|
- REDIS_ADDR=redis:6379
|
||||||
- DIFFICULTY=4
|
- DIFFICULTY=4
|
||||||
@@ -55,6 +54,10 @@ redis:
|
|||||||
image: redis/redis-stack-server:latest
|
image: redis/redis-stack-server:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div id="captcha" class="{{ .FrontendKey }}" data-sitekey="{{ .SiteKey }}" data-callback="captchaCallback" data-challenge-url="http://captcha.localhost:8000/v0/challenge">
|
||||||
|
```
|
||||||
|
|
||||||
## Exemple navigation
|
## Exemple navigation
|
||||||
|
|
||||||
We can try to query normally the whoami server:
|
We can try to query normally the whoami server:
|
||||||
|
|||||||
@@ -294,7 +294,7 @@
|
|||||||
<h1 class="text-2xl lg:text-3xl xl:text-4xl">CrowdSec Captcha</h1>
|
<h1 class="text-2xl lg:text-3xl xl:text-4xl">CrowdSec Captcha</h1>
|
||||||
</div>
|
</div>
|
||||||
<form action="" method="POST" class="flex flex-col items-center space-y-1" id="captcha-form">
|
<form action="" method="POST" class="flex flex-col items-center space-y-1" id="captcha-form">
|
||||||
<div id="captcha" class="{{ .FrontendKey }}" data-sitekey="{{ .SiteKey }}" data-callback="captchaCallback">
|
<div id="captcha" class="{{ .FrontendKey }}" data-sitekey="{{ .SiteKey }}" data-callback="captchaCallback" data-challenge-url="http://captcha.localhost:8000/v0/challenge">
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<div class="flex justify-center flex-wrap">
|
<div class="flex justify-center flex-wrap">
|
||||||
|
|||||||
@@ -82,7 +82,6 @@ services:
|
|||||||
image: ghcr.io/a-ve/wicketkeeper:latest
|
image: ghcr.io/a-ve/wicketkeeper:latest
|
||||||
container_name: "wicketkeeper"
|
container_name: "wicketkeeper"
|
||||||
environment:
|
environment:
|
||||||
- ROOT_URL=http://captcha.localhost:8000
|
|
||||||
- LISTEN_PORT=8080
|
- LISTEN_PORT=8080
|
||||||
- REDIS_ADDR=redis:6379
|
- REDIS_ADDR=redis:6379
|
||||||
- DIFFICULTY=4
|
- DIFFICULTY=4
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ const (
|
|||||||
LogDEBUG = "DEBUG"
|
LogDEBUG = "DEBUG"
|
||||||
LogINFO = "INFO"
|
LogINFO = "INFO"
|
||||||
LogERROR = "ERROR"
|
LogERROR = "ERROR"
|
||||||
|
ReasonTECH = "TECHNICAL_ISSUE"
|
||||||
|
ReasonLAPI = "LAPI"
|
||||||
|
ReasonAPPSEC = "APPSEC"
|
||||||
HcaptchaProvider = "hcaptcha"
|
HcaptchaProvider = "hcaptcha"
|
||||||
RecaptchaProvider = "recaptcha"
|
RecaptchaProvider = "recaptcha"
|
||||||
TurnstileProvider = "turnstile"
|
TurnstileProvider = "turnstile"
|
||||||
|
|||||||
Reference in New Issue
Block a user