Skip to content

Commit 726540b

Browse files
committed
[minor] Add protectHttpMethods config
1 parent 098e8ef commit 726540b

File tree

2 files changed

+45
-9
lines changed

2 files changed

+45
-9
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ services:
7171
--providers.docker=true
7272
--providers.docker.network=default
7373
--experimental.plugins.captcha-protect.modulename=github.com/libops/captcha-protect
74-
--experimental.plugins.captcha-protect.version=v1.3.2
74+
--experimental.plugins.captcha-protect.version=v1.4.0
7575
volumes:
7676
- /var/run/docker.sock:/var/run/docker.sock:z
7777
- /CHANGEME/TO/A/HOST/PATH/FOR/STATE/FILE:/tmp/state.json:rw
@@ -95,7 +95,7 @@ services:
9595
| `captchaProvider` | `string` (required) | `""` | The captcha type to use. Supported values: `turnstile`, `hcaptcha`, and `recaptcha`. |
9696
| `siteKey` | `string` (required) | `""` | The captcha site key. |
9797
| `secretKey` | `string` (required) | `""` | The captcha secret key. |
98-
| `rateLimit` | `uint` | `20` | Maximum requests allowed from a subnet before a challenge is triggered. |
98+
| `rateLimit` | `uint` | `20` | Maximum requests allowed from a subnet before a challenge is triggered. |
9999
| `window` | `int` | `86400` | Duration (in seconds) for monitoring requests per subnet. |
100100
| `ipv4subnetMask` | `int` | `16` | CIDR subnet mask to group IPv4 addresses for rate limiting. |
101101
| `ipv6subnetMask` | `int` | `64` | CIDR subnet mask to group IPv6 addresses for rate limiting. |
@@ -104,6 +104,7 @@ services:
104104
| `goodBots` | `[]string` (encouraged) | *see below* | List of second-level domains for bots that are never challenged or rate-limited. |
105105
| `protectParameters` | `string` | `"false"` | Forces rate limiting even for good bots if URL parameters are present. Useful for protecting faceted search pages. |
106106
| `protectFileExtensions` | `[]string` | `""` | Comma-separated file extensions to protect. By default, your protected routes only protect html files. This is to prevent files like CSS/JS/img from tripping the rate limit. |
107+
| `protectHttpMethods` | `[]string` | `"GET,HEAD"` | Comma-separated list of HTTP methods to protect against |
107108
| `exemptIps` | `[]string` | `privateIPs` | CIDR-formatted IPs that should never be challenged. Private IP ranges are always exempt. |
108109
| `challengeURL` | `string` | `"/challenge"` | URL where challenges are served. This will override existing routes if there is a conflict. |
109110
| `challengeTmpl` | `string` | `"./challenge.tmpl.html"`| Path to the Go HTML template for the captcha challenge page. |

main.go

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ type Config struct {
3434
ProtectParameters string `json:"protectParameters"`
3535
ProtectRoutes []string `json:"protectRoutes"`
3636
ProtectFileExtensions []string `json:"protectFileExtensions"`
37+
ProtectHttpMethods []string `json:"protectHttpMethods"`
3738
GoodBots []string `json:"goodBots"`
3839
ExemptIPs []string `json:"exemptIps"`
3940
ChallengeURL string `json:"challengeURL"`
@@ -70,13 +71,14 @@ type captchaResponse struct {
7071

7172
func CreateConfig() *Config {
7273
return &Config{
73-
RateLimit: 20,
74-
Window: 86400,
75-
IPv4SubnetMask: 16,
76-
IPv6SubnetMask: 64,
77-
IPForwardedHeader: "",
78-
ProtectParameters: "false",
79-
ProtectRoutes: []string{},
74+
RateLimit: 20,
75+
Window: 86400,
76+
IPv4SubnetMask: 16,
77+
IPv6SubnetMask: 64,
78+
IPForwardedHeader: "",
79+
ProtectParameters: "false",
80+
ProtectRoutes: []string{},
81+
ProtectHttpMethods: []string{},
8082
ProtectFileExtensions: []string{
8183
"html",
8284
},
@@ -117,6 +119,14 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
117119
return nil, fmt.Errorf("you must protect at least one route with the protectRoutes config value. / will cover your entire site")
118120
}
119121

122+
if len(config.ProtectHttpMethods) == 0 {
123+
config.ProtectHttpMethods = []string{
124+
"GET",
125+
"HEAD",
126+
}
127+
}
128+
config.ParseHttpMethods()
129+
120130
var tmpl *template.Template
121131
if _, err := os.Stat(config.ChallengeTmpl); os.IsNotExist(err) {
122132
log.Warn("Unable to find template file. Using default template.", "challengeTmpl", config.ChallengeTmpl)
@@ -317,6 +327,10 @@ func (bc *CaptchaProtect) serveStatsPage(rw http.ResponseWriter, ip string) {
317327
}
318328

319329
func (bc *CaptchaProtect) shouldApply(req *http.Request, clientIP string) bool {
330+
if !strInSlice(req.Method, bc.config.ProtectHttpMethods) {
331+
return false
332+
}
333+
320334
_, verified := bc.verifiedCache.Get(clientIP)
321335
if verified {
322336
return false
@@ -545,6 +559,18 @@ func ParseLogLevel(level string) (slog.Level, error) {
545559
}
546560
}
547561

562+
// log a warning if protected methods contains an invalid method
563+
func (c *Config) ParseHttpMethods() error {
564+
for _, method := range c.ProtectHttpMethods {
565+
switch method {
566+
case "GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "CONNECT", "OPTIONS", "TRACE":
567+
continue
568+
default:
569+
log.Warn("unknown http method %s", method)
570+
}
571+
}
572+
}
573+
548574
func (bc *CaptchaProtect) saveState(ctx context.Context) {
549575
ticker := time.NewTicker(1 * time.Minute)
550576
defer ticker.Stop()
@@ -641,3 +667,12 @@ func getDefaultTmpl() string {
641667
</body>
642668
</html>`
643669
}
670+
671+
func strInSlice(s string, sl []string) bool {
672+
for _, a := range sl {
673+
if a == s {
674+
return true
675+
}
676+
}
677+
return false
678+
}

0 commit comments

Comments
 (0)