Skip to content

Commit

Permalink
[minor] Add protectHttpMethods config
Browse files Browse the repository at this point in the history
  • Loading branch information
joecorall committed Feb 26, 2025
1 parent 098e8ef commit 08140bc
Show file tree
Hide file tree
Showing 2 changed files with 45 additions and 9 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ services:
--providers.docker=true
--providers.docker.network=default
--experimental.plugins.captcha-protect.modulename=github.com/libops/captcha-protect
--experimental.plugins.captcha-protect.version=v1.3.2
--experimental.plugins.captcha-protect.version=v1.4.0
volumes:
- /var/run/docker.sock:/var/run/docker.sock:z
- /CHANGEME/TO/A/HOST/PATH/FOR/STATE/FILE:/tmp/state.json:rw
Expand All @@ -95,7 +95,7 @@ services:
| `captchaProvider` | `string` (required) | `""` | The captcha type to use. Supported values: `turnstile`, `hcaptcha`, and `recaptcha`. |
| `siteKey` | `string` (required) | `""` | The captcha site key. |
| `secretKey` | `string` (required) | `""` | The captcha secret key. |
| `rateLimit` | `uint` | `20` | Maximum requests allowed from a subnet before a challenge is triggered. |
| `rateLimit` | `uint` | `20` | Maximum requests allowed from a subnet before a challenge is triggered. |
| `window` | `int` | `86400` | Duration (in seconds) for monitoring requests per subnet. |
| `ipv4subnetMask` | `int` | `16` | CIDR subnet mask to group IPv4 addresses for rate limiting. |
| `ipv6subnetMask` | `int` | `64` | CIDR subnet mask to group IPv6 addresses for rate limiting. |
Expand All @@ -104,6 +104,7 @@ services:
| `goodBots` | `[]string` (encouraged) | *see below* | List of second-level domains for bots that are never challenged or rate-limited. |
| `protectParameters` | `string` | `"false"` | Forces rate limiting even for good bots if URL parameters are present. Useful for protecting faceted search pages. |
| `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. |
| `protectHttpMethods` | `[]string` | `"GET,HEAD"` | Comma-separated list of HTTP methods to protect against |
| `exemptIps` | `[]string` | `privateIPs` | CIDR-formatted IPs that should never be challenged. Private IP ranges are always exempt. |
| `challengeURL` | `string` | `"/challenge"` | URL where challenges are served. This will override existing routes if there is a conflict. |
| `challengeTmpl` | `string` | `"./challenge.tmpl.html"`| Path to the Go HTML template for the captcha challenge page. |
Expand Down
49 changes: 42 additions & 7 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type Config struct {
ProtectParameters string `json:"protectParameters"`
ProtectRoutes []string `json:"protectRoutes"`
ProtectFileExtensions []string `json:"protectFileExtensions"`
ProtectHttpMethods []string `json:"protectHttpMethods"`
GoodBots []string `json:"goodBots"`
ExemptIPs []string `json:"exemptIps"`
ChallengeURL string `json:"challengeURL"`
Expand Down Expand Up @@ -70,13 +71,14 @@ type captchaResponse struct {

func CreateConfig() *Config {
return &Config{
RateLimit: 20,
Window: 86400,
IPv4SubnetMask: 16,
IPv6SubnetMask: 64,
IPForwardedHeader: "",
ProtectParameters: "false",
ProtectRoutes: []string{},
RateLimit: 20,
Window: 86400,
IPv4SubnetMask: 16,
IPv6SubnetMask: 64,
IPForwardedHeader: "",
ProtectParameters: "false",
ProtectRoutes: []string{},
ProtectHttpMethods: []string{},
ProtectFileExtensions: []string{
"html",
},
Expand Down Expand Up @@ -117,6 +119,14 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
return nil, fmt.Errorf("you must protect at least one route with the protectRoutes config value. / will cover your entire site")
}

if len(config.ProtectHttpMethods) == 0 {
config.ProtectHttpMethods = []string{
"GET",
"HEAD",
}
}
config.ParseHttpMethods()

var tmpl *template.Template
if _, err := os.Stat(config.ChallengeTmpl); os.IsNotExist(err) {
log.Warn("Unable to find template file. Using default template.", "challengeTmpl", config.ChallengeTmpl)
Expand Down Expand Up @@ -317,6 +327,10 @@ func (bc *CaptchaProtect) serveStatsPage(rw http.ResponseWriter, ip string) {
}

func (bc *CaptchaProtect) shouldApply(req *http.Request, clientIP string) bool {
if !strInSlice(req.Method, bc.config.ProtectHttpMethods) {
return false
}

_, verified := bc.verifiedCache.Get(clientIP)
if verified {
return false
Expand Down Expand Up @@ -545,6 +559,18 @@ func ParseLogLevel(level string) (slog.Level, error) {
}
}

// log a warning if protected methods contains an invalid method
func (c *Config) ParseHttpMethods() {
for _, method := range c.ProtectHttpMethods {
switch method {
case "GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "CONNECT", "OPTIONS", "TRACE":
continue
default:
log.Warn("unknown http method", "method", method)
}
}
}

func (bc *CaptchaProtect) saveState(ctx context.Context) {
ticker := time.NewTicker(1 * time.Minute)
defer ticker.Stop()
Expand Down Expand Up @@ -641,3 +667,12 @@ func getDefaultTmpl() string {
</body>
</html>`
}

func strInSlice(s string, sl []string) bool {
for _, a := range sl {
if a == s {
return true
}
}
return false
}

0 comments on commit 08140bc

Please sign in to comment.