Initial import.
This commit is contained in:
		
						commit
						17e3eab464
					
				| 
						 | 
					@ -0,0 +1,176 @@
 | 
				
			||||||
 | 
					# CaddyHole
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					A Caddy module that blocks clients based on country and sends crawlers/bots a random amount of data from `/dev/random`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Features
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- **Country-based blocking**: Block requests from specific countries using GeoIP2 database
 | 
				
			||||||
 | 
					- **Bot/Crawler detection**: Automatically detect bots and crawlers based on User-Agent
 | 
				
			||||||
 | 
					- **Bot tarpit**: Send bots/crawlers random data from `/dev/random` to waste their resources
 | 
				
			||||||
 | 
					- **Configurable**: Customize blocked countries and amount of data sent to bots
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Installation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					To use this module, you need to build Caddy with this plugin included. You can use [xcaddy](https://github.com/caddyserver/xcaddy):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					xcaddy build --with git.wntrmute.dev/kyle/caddyhole
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Configuration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Caddyfile
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```caddyfile
 | 
				
			||||||
 | 
					example.com {
 | 
				
			||||||
 | 
					    caddyhole {
 | 
				
			||||||
 | 
					        # Path to GeoIP2 database (optional, required for country blocking)
 | 
				
			||||||
 | 
					        database /path/to/GeoLite2-Country.mmdb
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # List of country ISO codes to block (optional)
 | 
				
			||||||
 | 
					        block_countries CN RU KP IN IL
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Minimum bytes to send to bots (optional, default: 1MB)
 | 
				
			||||||
 | 
					        min_bot_bytes 1048576
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Maximum bytes to send to bots (optional, default: 100MB)
 | 
				
			||||||
 | 
					        max_bot_bytes 104857600
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Your other handlers here
 | 
				
			||||||
 | 
					    respond "Hello, World!"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### JSON Config
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```json
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "apps": {
 | 
				
			||||||
 | 
					    "http": {
 | 
				
			||||||
 | 
					      "servers": {
 | 
				
			||||||
 | 
					        "srv0": {
 | 
				
			||||||
 | 
					          "listen": [":443"],
 | 
				
			||||||
 | 
					          "routes": [
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					              "handle": [
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                  "handler": "caddyhole",
 | 
				
			||||||
 | 
					                  "database_path": "/path/to/GeoLite2-Country.mmdb",
 | 
				
			||||||
 | 
					                  "blocked_countries": ["CN", "RU", "KP"],
 | 
				
			||||||
 | 
					                  "min_bot_bytes": 1048576,
 | 
				
			||||||
 | 
					                  "max_bot_bytes": 104857600
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                  "handler": "static_response",
 | 
				
			||||||
 | 
					                  "body": "Hello, World!"
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              ]
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          ]
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## How It Works
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Bot Detection
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The module detects bots/crawlers by examining the `User-Agent` header. It looks for common bot signatures including:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- bot, crawler, spider, scraper
 | 
				
			||||||
 | 
					- curl, wget
 | 
				
			||||||
 | 
					- python-requests, python-urllib
 | 
				
			||||||
 | 
					- go-http-client
 | 
				
			||||||
 | 
					- java, perl, ruby, php
 | 
				
			||||||
 | 
					- http_request
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					When a bot is detected, the module:
 | 
				
			||||||
 | 
					1. Generates a random amount of data between `min_bot_bytes` and `max_bot_bytes`
 | 
				
			||||||
 | 
					2. Streams that amount of random data from `/dev/random` (or `/dev/urandom` as fallback)
 | 
				
			||||||
 | 
					3. Returns HTTP 200 OK to make the bot think it succeeded
 | 
				
			||||||
 | 
					4. Never passes the request to downstream handlers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Country Blocking
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The module uses MaxMind's GeoIP2 database to determine the country of the client based on their IP address. If the country code matches any in the `blocked_countries` list:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. Returns HTTP 403 Forbidden
 | 
				
			||||||
 | 
					2. Sends "Access denied" message
 | 
				
			||||||
 | 
					3. Never passes the request to downstream handlers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The module checks the following headers for the client IP (in order):
 | 
				
			||||||
 | 
					1. `X-Forwarded-For` (first IP)
 | 
				
			||||||
 | 
					2. `X-Real-IP`
 | 
				
			||||||
 | 
					3. `RemoteAddr`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Execution Order
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The module processes requests in this order:
 | 
				
			||||||
 | 
					1. Check if request is from a bot → If yes, send random data
 | 
				
			||||||
 | 
					2. Check if request is from a blocked country → If yes, return 403
 | 
				
			||||||
 | 
					3. Otherwise, pass to the next handler
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## GeoIP2 Database
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					To use country blocking, you need a GeoIP2 database. You can download the free GeoLite2 Country database from MaxMind:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. Sign up for a free account at [MaxMind](https://www.maxmind.com/en/geolite2/signup)
 | 
				
			||||||
 | 
					2. Download the GeoLite2 Country database in MMDB format
 | 
				
			||||||
 | 
					3. Extract the `.mmdb` file
 | 
				
			||||||
 | 
					4. Configure the `database` path in your Caddyfile
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Block only specific countries (no bot handling)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```caddyfile
 | 
				
			||||||
 | 
					example.com {
 | 
				
			||||||
 | 
					    caddyhole {
 | 
				
			||||||
 | 
					        database /path/to/GeoLite2-Country.mmdb
 | 
				
			||||||
 | 
					        block_countries CN RU
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    respond "Hello, World!"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Bot tarpit only (no country blocking)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```caddyfile
 | 
				
			||||||
 | 
					example.com {
 | 
				
			||||||
 | 
					    caddyhole {
 | 
				
			||||||
 | 
					        min_bot_bytes 10485760    # 10MB
 | 
				
			||||||
 | 
					        max_bot_bytes 1073741824  # 1GB
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    respond "Hello, World!"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Full protection
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```caddyfile
 | 
				
			||||||
 | 
					example.com {
 | 
				
			||||||
 | 
					    caddyhole {
 | 
				
			||||||
 | 
					        database /path/to/GeoLite2-Country.mmdb
 | 
				
			||||||
 | 
					        block_countries CN RU KP IR
 | 
				
			||||||
 | 
					        min_bot_bytes 52428800    # 50MB
 | 
				
			||||||
 | 
					        max_bot_bytes 524288000   # 500MB
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    respond "Hello, World!"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Notes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Bot detection happens before country blocking, so bots will get random data regardless of their country
 | 
				
			||||||
 | 
					- The random data is streamed directly from `/dev/random` (or `/dev/urandom`), which may impact system entropy on some systems
 | 
				
			||||||
 | 
					- The `Content-Type` is set to `application/octet-stream` and `Content-Length` is set to make the response appear legitimate
 | 
				
			||||||
 | 
					- Country blocking requires a GeoIP2 database; without it, no country blocking occurs
 | 
				
			||||||
 | 
					- All configuration parameters are optional, but you need at least `database` and `block_countries` for country blocking to work
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## License
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This module is provided as-is for use with Caddy.
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,265 @@
 | 
				
			||||||
 | 
					package caddyhole
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"math/rand"
 | 
				
			||||||
 | 
						"net"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/caddyserver/caddy/v2"
 | 
				
			||||||
 | 
						"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
 | 
				
			||||||
 | 
						"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
 | 
				
			||||||
 | 
						"github.com/caddyserver/caddy/v2/modules/caddyhttp"
 | 
				
			||||||
 | 
						"github.com/oschwald/geoip2-golang"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func init() {
 | 
				
			||||||
 | 
						caddy.RegisterModule(CaddyHole{})
 | 
				
			||||||
 | 
						httpcaddyfile.RegisterHandlerDirective("caddyhole", parseCaddyfile)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CaddyHole implements a Caddy module that blocks clients based on country
 | 
				
			||||||
 | 
					// and sends crawlers/bots random data from /dev/random.
 | 
				
			||||||
 | 
					type CaddyHole struct {
 | 
				
			||||||
 | 
						// Path to the GeoIP2 database file
 | 
				
			||||||
 | 
						DatabasePath string `json:"database_path,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// List of country ISO codes to block
 | 
				
			||||||
 | 
						BlockedCountries []string `json:"blocked_countries,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Minimum bytes to send to bots (default: 1MB)
 | 
				
			||||||
 | 
						MinBotBytes int64 `json:"min_bot_bytes,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Maximum bytes to send to bots (default: 100MB)
 | 
				
			||||||
 | 
						MaxBotBytes int64 `json:"max_bot_bytes,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						db *geoip2.Reader
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CaddyModule returns the Caddy module information.
 | 
				
			||||||
 | 
					func (CaddyHole) CaddyModule() caddy.ModuleInfo {
 | 
				
			||||||
 | 
						return caddy.ModuleInfo{
 | 
				
			||||||
 | 
							ID:  "http.handlers.caddyhole",
 | 
				
			||||||
 | 
							New: func() caddy.Module { return new(CaddyHole) },
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Provision sets up the CaddyHole module.
 | 
				
			||||||
 | 
					func (c *CaddyHole) Provision(ctx caddy.Context) error {
 | 
				
			||||||
 | 
						// Set defaults
 | 
				
			||||||
 | 
						if c.MinBotBytes == 0 {
 | 
				
			||||||
 | 
							c.MinBotBytes = 1024 * 1024 // 1MB
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if c.MaxBotBytes == 0 {
 | 
				
			||||||
 | 
							c.MaxBotBytes = 100 * 1024 * 1024 // 100MB
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Open GeoIP2 database if path is provided
 | 
				
			||||||
 | 
						if c.DatabasePath != "" {
 | 
				
			||||||
 | 
							db, err := geoip2.Open(c.DatabasePath)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("failed to open GeoIP2 database: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							c.db = db
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Cleanup closes the GeoIP2 database.
 | 
				
			||||||
 | 
					func (c *CaddyHole) Cleanup() error {
 | 
				
			||||||
 | 
						if c.db != nil {
 | 
				
			||||||
 | 
							return c.db.Close()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ServeHTTP implements caddyhttp.MiddlewareHandler.
 | 
				
			||||||
 | 
					func (c *CaddyHole) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
 | 
				
			||||||
 | 
						// Check if the request is from a bot/crawler
 | 
				
			||||||
 | 
						if c.isBot(r) {
 | 
				
			||||||
 | 
							c.feedBot(w, r)
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check if the request should be blocked based on country
 | 
				
			||||||
 | 
						if c.shouldBlock(r) {
 | 
				
			||||||
 | 
							w.WriteHeader(http.StatusForbidden)
 | 
				
			||||||
 | 
							w.Write([]byte("Access denied"))
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Continue to the next handler
 | 
				
			||||||
 | 
						return next.ServeHTTP(w, r)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// isBot checks if the request is from a bot/crawler based on User-Agent.
 | 
				
			||||||
 | 
					func (c *CaddyHole) isBot(r *http.Request) bool {
 | 
				
			||||||
 | 
						userAgent := strings.ToLower(r.Header.Get("User-Agent"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Common bot/crawler identifiers
 | 
				
			||||||
 | 
						botSignatures := []string{
 | 
				
			||||||
 | 
							"bot", "crawler", "spider", "scraper", "curl", "wget",
 | 
				
			||||||
 | 
							"python-requests", "python-urllib", "go-http-client",
 | 
				
			||||||
 | 
							"java", "perl", "ruby", "php", "http_request",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, sig := range botSignatures {
 | 
				
			||||||
 | 
							if strings.Contains(userAgent, sig) {
 | 
				
			||||||
 | 
								return true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// feedBot sends random data from /dev/random to the bot.
 | 
				
			||||||
 | 
					func (c *CaddyHole) feedBot(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
						// Calculate random amount of bytes to send
 | 
				
			||||||
 | 
						bytesToSend := c.MinBotBytes
 | 
				
			||||||
 | 
						if c.MaxBotBytes > c.MinBotBytes {
 | 
				
			||||||
 | 
							bytesToSend += rand.Int63n(c.MaxBotBytes - c.MinBotBytes)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Open /dev/random
 | 
				
			||||||
 | 
						devRandom, err := os.Open("/dev/random")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							// Fallback to /dev/urandom if /dev/random is not available
 | 
				
			||||||
 | 
							devRandom, err = os.Open("/dev/urandom")
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								w.WriteHeader(http.StatusInternalServerError)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer devRandom.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Set headers to make it look like a legitimate response
 | 
				
			||||||
 | 
						w.Header().Set("Content-Type", "application/octet-stream")
 | 
				
			||||||
 | 
						w.Header().Set("Content-Length", fmt.Sprintf("%d", bytesToSend))
 | 
				
			||||||
 | 
						w.WriteHeader(http.StatusOK)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Copy random data to the response
 | 
				
			||||||
 | 
						io.CopyN(w, devRandom, bytesToSend)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// shouldBlock checks if the request should be blocked based on country.
 | 
				
			||||||
 | 
					func (c *CaddyHole) shouldBlock(r *http.Request) bool {
 | 
				
			||||||
 | 
						if c.db == nil || len(c.BlockedCountries) == 0 {
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Get the client IP address
 | 
				
			||||||
 | 
						ip := c.getClientIP(r)
 | 
				
			||||||
 | 
						if ip == nil {
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Look up the country for the IP
 | 
				
			||||||
 | 
						record, err := c.db.Country(ip)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check if the country is in the blocked list
 | 
				
			||||||
 | 
						for _, blocked := range c.BlockedCountries {
 | 
				
			||||||
 | 
							if strings.EqualFold(record.Country.IsoCode, blocked) {
 | 
				
			||||||
 | 
								return true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// getClientIP extracts the client IP address from the request.
 | 
				
			||||||
 | 
					func (c *CaddyHole) getClientIP(r *http.Request) net.IP {
 | 
				
			||||||
 | 
						// Check X-Forwarded-For header first
 | 
				
			||||||
 | 
						xff := r.Header.Get("X-Forwarded-For")
 | 
				
			||||||
 | 
						if xff != "" {
 | 
				
			||||||
 | 
							ips := strings.Split(xff, ",")
 | 
				
			||||||
 | 
							if len(ips) > 0 {
 | 
				
			||||||
 | 
								ipStr := strings.TrimSpace(ips[0])
 | 
				
			||||||
 | 
								if ip := net.ParseIP(ipStr); ip != nil {
 | 
				
			||||||
 | 
									return ip
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check X-Real-IP header
 | 
				
			||||||
 | 
						xri := r.Header.Get("X-Real-IP")
 | 
				
			||||||
 | 
						if xri != "" {
 | 
				
			||||||
 | 
							if ip := net.ParseIP(xri); ip != nil {
 | 
				
			||||||
 | 
								return ip
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Fall back to RemoteAddr
 | 
				
			||||||
 | 
						host, _, err := net.SplitHostPort(r.RemoteAddr)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return net.ParseIP(host)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
 | 
				
			||||||
 | 
					func (c *CaddyHole) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
 | 
				
			||||||
 | 
						for d.Next() {
 | 
				
			||||||
 | 
							for d.NextBlock(0) {
 | 
				
			||||||
 | 
								switch d.Val() {
 | 
				
			||||||
 | 
								case "database":
 | 
				
			||||||
 | 
									if !d.NextArg() {
 | 
				
			||||||
 | 
										return d.ArgErr()
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									c.DatabasePath = d.Val()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								case "block_countries":
 | 
				
			||||||
 | 
									c.BlockedCountries = d.RemainingArgs()
 | 
				
			||||||
 | 
									if len(c.BlockedCountries) == 0 {
 | 
				
			||||||
 | 
										return d.ArgErr()
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								case "min_bot_bytes":
 | 
				
			||||||
 | 
									if !d.NextArg() {
 | 
				
			||||||
 | 
										return d.ArgErr()
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									var err error
 | 
				
			||||||
 | 
									_, err = fmt.Sscanf(d.Val(), "%d", &c.MinBotBytes)
 | 
				
			||||||
 | 
									if err != nil {
 | 
				
			||||||
 | 
										return d.Errf("invalid min_bot_bytes value: %v", err)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								case "max_bot_bytes":
 | 
				
			||||||
 | 
									if !d.NextArg() {
 | 
				
			||||||
 | 
										return d.ArgErr()
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									var err error
 | 
				
			||||||
 | 
									_, err = fmt.Sscanf(d.Val(), "%d", &c.MaxBotBytes)
 | 
				
			||||||
 | 
									if err != nil {
 | 
				
			||||||
 | 
										return d.Errf("invalid max_bot_bytes value: %v", err)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								default:
 | 
				
			||||||
 | 
									return d.Errf("unrecognized subdirective: %s", d.Val())
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// parseCaddyfile unmarshals tokens from h into a new Middleware.
 | 
				
			||||||
 | 
					func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
 | 
				
			||||||
 | 
						var c CaddyHole
 | 
				
			||||||
 | 
						err := c.UnmarshalCaddyfile(h.Dispenser)
 | 
				
			||||||
 | 
						return &c, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Interface guards
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						_ caddy.Provisioner           = (*CaddyHole)(nil)
 | 
				
			||||||
 | 
						_ caddy.CleanerUpper          = (*CaddyHole)(nil)
 | 
				
			||||||
 | 
						_ caddyhttp.MiddlewareHandler = (*CaddyHole)(nil)
 | 
				
			||||||
 | 
						_ caddyfile.Unmarshaler       = (*CaddyHole)(nil)
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,133 @@
 | 
				
			||||||
 | 
					module git.wntrmute.dev/kyle/caddyhole
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					go 1.25.1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					require (
 | 
				
			||||||
 | 
						github.com/caddyserver/caddy/v2 v2.10.2
 | 
				
			||||||
 | 
						github.com/oschwald/geoip2-golang v1.13.0
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					require (
 | 
				
			||||||
 | 
						cel.dev/expr v0.24.0 // indirect
 | 
				
			||||||
 | 
						cloud.google.com/go/auth v0.16.2 // indirect
 | 
				
			||||||
 | 
						cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
 | 
				
			||||||
 | 
						cloud.google.com/go/compute/metadata v0.7.0 // indirect
 | 
				
			||||||
 | 
						dario.cat/mergo v1.0.1 // indirect
 | 
				
			||||||
 | 
						filippo.io/edwards25519 v1.1.0 // indirect
 | 
				
			||||||
 | 
						github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
 | 
				
			||||||
 | 
						github.com/KimMachineGun/automemlimit v0.7.4 // indirect
 | 
				
			||||||
 | 
						github.com/Masterminds/goutils v1.1.1 // indirect
 | 
				
			||||||
 | 
						github.com/Masterminds/semver/v3 v3.3.0 // indirect
 | 
				
			||||||
 | 
						github.com/Masterminds/sprig/v3 v3.3.0 // indirect
 | 
				
			||||||
 | 
						github.com/Microsoft/go-winio v0.6.0 // indirect
 | 
				
			||||||
 | 
						github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
 | 
				
			||||||
 | 
						github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect
 | 
				
			||||||
 | 
						github.com/beorn7/perks v1.0.1 // indirect
 | 
				
			||||||
 | 
						github.com/caddyserver/certmagic v0.24.0 // indirect
 | 
				
			||||||
 | 
						github.com/caddyserver/zerossl v0.1.3 // indirect
 | 
				
			||||||
 | 
						github.com/ccoveille/go-safecast v1.6.1 // indirect
 | 
				
			||||||
 | 
						github.com/cespare/xxhash v1.1.0 // indirect
 | 
				
			||||||
 | 
						github.com/cespare/xxhash/v2 v2.3.0 // indirect
 | 
				
			||||||
 | 
						github.com/chzyer/readline v1.5.1 // indirect
 | 
				
			||||||
 | 
						github.com/cloudflare/circl v1.6.1 // indirect
 | 
				
			||||||
 | 
						github.com/coreos/go-oidc/v3 v3.14.1 // indirect
 | 
				
			||||||
 | 
						github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
 | 
				
			||||||
 | 
						github.com/dgraph-io/badger v1.6.2 // indirect
 | 
				
			||||||
 | 
						github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
 | 
				
			||||||
 | 
						github.com/dgraph-io/ristretto v0.2.0 // indirect
 | 
				
			||||||
 | 
						github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
 | 
				
			||||||
 | 
						github.com/dustin/go-humanize v1.0.1 // indirect
 | 
				
			||||||
 | 
						github.com/felixge/httpsnoop v1.0.4 // indirect
 | 
				
			||||||
 | 
						github.com/francoispqt/gojay v1.2.13 // indirect
 | 
				
			||||||
 | 
						github.com/go-jose/go-jose/v3 v3.0.4 // indirect
 | 
				
			||||||
 | 
						github.com/go-jose/go-jose/v4 v4.0.5 // indirect
 | 
				
			||||||
 | 
						github.com/go-logr/logr v1.4.3 // indirect
 | 
				
			||||||
 | 
						github.com/go-logr/stdr v1.2.2 // indirect
 | 
				
			||||||
 | 
						github.com/go-sql-driver/mysql v1.8.1 // indirect
 | 
				
			||||||
 | 
						github.com/golang/protobuf v1.5.4 // indirect
 | 
				
			||||||
 | 
						github.com/golang/snappy v0.0.4 // indirect
 | 
				
			||||||
 | 
						github.com/google/cel-go v0.26.0 // indirect
 | 
				
			||||||
 | 
						github.com/google/s2a-go v0.1.9 // indirect
 | 
				
			||||||
 | 
						github.com/google/uuid v1.6.0 // indirect
 | 
				
			||||||
 | 
						github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
 | 
				
			||||||
 | 
						github.com/googleapis/gax-go/v2 v2.14.2 // indirect
 | 
				
			||||||
 | 
						github.com/huandu/xstrings v1.5.0 // indirect
 | 
				
			||||||
 | 
						github.com/inconshreveable/mousetrap v1.1.0 // indirect
 | 
				
			||||||
 | 
						github.com/jackc/pgpassfile v1.0.0 // indirect
 | 
				
			||||||
 | 
						github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
 | 
				
			||||||
 | 
						github.com/jackc/pgx/v5 v5.6.0 // indirect
 | 
				
			||||||
 | 
						github.com/jackc/puddle/v2 v2.2.1 // indirect
 | 
				
			||||||
 | 
						github.com/klauspost/compress v1.18.0 // indirect
 | 
				
			||||||
 | 
						github.com/klauspost/cpuid/v2 v2.3.0 // indirect
 | 
				
			||||||
 | 
						github.com/libdns/libdns v1.1.0 // indirect
 | 
				
			||||||
 | 
						github.com/manifoldco/promptui v0.9.0 // indirect
 | 
				
			||||||
 | 
						github.com/mattn/go-colorable v0.1.13 // indirect
 | 
				
			||||||
 | 
						github.com/mattn/go-isatty v0.0.20 // indirect
 | 
				
			||||||
 | 
						github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
 | 
				
			||||||
 | 
						github.com/mholt/acmez/v3 v3.1.2 // indirect
 | 
				
			||||||
 | 
						github.com/miekg/dns v1.1.63 // indirect
 | 
				
			||||||
 | 
						github.com/mitchellh/copystructure v1.2.0 // indirect
 | 
				
			||||||
 | 
						github.com/mitchellh/go-ps v1.0.0 // indirect
 | 
				
			||||||
 | 
						github.com/mitchellh/reflectwalk v1.0.2 // indirect
 | 
				
			||||||
 | 
						github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
 | 
				
			||||||
 | 
						github.com/oschwald/maxminddb-golang v1.13.0 // indirect
 | 
				
			||||||
 | 
						github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
 | 
				
			||||||
 | 
						github.com/pkg/errors v0.9.1 // indirect
 | 
				
			||||||
 | 
						github.com/prometheus/client_golang v1.23.0 // indirect
 | 
				
			||||||
 | 
						github.com/prometheus/client_model v0.6.2 // indirect
 | 
				
			||||||
 | 
						github.com/prometheus/common v0.65.0 // indirect
 | 
				
			||||||
 | 
						github.com/prometheus/procfs v0.16.1 // indirect
 | 
				
			||||||
 | 
						github.com/quic-go/qpack v0.5.1 // indirect
 | 
				
			||||||
 | 
						github.com/quic-go/quic-go v0.54.0 // indirect
 | 
				
			||||||
 | 
						github.com/rs/xid v1.6.0 // indirect
 | 
				
			||||||
 | 
						github.com/russross/blackfriday/v2 v2.1.0 // indirect
 | 
				
			||||||
 | 
						github.com/shopspring/decimal v1.4.0 // indirect
 | 
				
			||||||
 | 
						github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
 | 
				
			||||||
 | 
						github.com/slackhq/nebula v1.9.5 // indirect
 | 
				
			||||||
 | 
						github.com/smallstep/certificates v0.28.4 // indirect
 | 
				
			||||||
 | 
						github.com/smallstep/cli-utils v0.12.1 // indirect
 | 
				
			||||||
 | 
						github.com/smallstep/linkedca v0.23.0 // indirect
 | 
				
			||||||
 | 
						github.com/smallstep/nosql v0.7.0 // indirect
 | 
				
			||||||
 | 
						github.com/smallstep/pkcs7 v0.2.1 // indirect
 | 
				
			||||||
 | 
						github.com/smallstep/scep v0.0.0-20240926084937-8cf1ca453101 // indirect
 | 
				
			||||||
 | 
						github.com/smallstep/truststore v0.13.0 // indirect
 | 
				
			||||||
 | 
						github.com/spf13/cast v1.7.0 // indirect
 | 
				
			||||||
 | 
						github.com/spf13/cobra v1.9.1 // indirect
 | 
				
			||||||
 | 
						github.com/spf13/pflag v1.0.7 // indirect
 | 
				
			||||||
 | 
						github.com/stoewer/go-strcase v1.2.0 // indirect
 | 
				
			||||||
 | 
						github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 // indirect
 | 
				
			||||||
 | 
						github.com/urfave/cli v1.22.17 // indirect
 | 
				
			||||||
 | 
						github.com/zeebo/blake3 v0.2.4 // indirect
 | 
				
			||||||
 | 
						go.etcd.io/bbolt v1.3.10 // indirect
 | 
				
			||||||
 | 
						go.opentelemetry.io/auto/sdk v1.1.0 // indirect
 | 
				
			||||||
 | 
						go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
 | 
				
			||||||
 | 
						go.opentelemetry.io/otel v1.37.0 // indirect
 | 
				
			||||||
 | 
						go.opentelemetry.io/otel/metric v1.37.0 // indirect
 | 
				
			||||||
 | 
						go.opentelemetry.io/otel/trace v1.37.0 // indirect
 | 
				
			||||||
 | 
						go.step.sm/crypto v0.67.0 // indirect
 | 
				
			||||||
 | 
						go.uber.org/automaxprocs v1.6.0 // indirect
 | 
				
			||||||
 | 
						go.uber.org/mock v0.5.2 // indirect
 | 
				
			||||||
 | 
						go.uber.org/multierr v1.11.0 // indirect
 | 
				
			||||||
 | 
						go.uber.org/zap v1.27.0 // indirect
 | 
				
			||||||
 | 
						go.uber.org/zap/exp v0.3.0 // indirect
 | 
				
			||||||
 | 
						golang.org/x/crypto v0.40.0 // indirect
 | 
				
			||||||
 | 
						golang.org/x/crypto/x509roots/fallback v0.0.0-20250305170421-49bf5b80c810 // indirect
 | 
				
			||||||
 | 
						golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
 | 
				
			||||||
 | 
						golang.org/x/mod v0.25.0 // indirect
 | 
				
			||||||
 | 
						golang.org/x/net v0.42.0 // indirect
 | 
				
			||||||
 | 
						golang.org/x/oauth2 v0.30.0 // indirect
 | 
				
			||||||
 | 
						golang.org/x/sync v0.16.0 // indirect
 | 
				
			||||||
 | 
						golang.org/x/sys v0.34.0 // indirect
 | 
				
			||||||
 | 
						golang.org/x/term v0.33.0 // indirect
 | 
				
			||||||
 | 
						golang.org/x/text v0.27.0 // indirect
 | 
				
			||||||
 | 
						golang.org/x/time v0.12.0 // indirect
 | 
				
			||||||
 | 
						golang.org/x/tools v0.34.0 // indirect
 | 
				
			||||||
 | 
						google.golang.org/api v0.240.0 // indirect
 | 
				
			||||||
 | 
						google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
 | 
				
			||||||
 | 
						google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
 | 
				
			||||||
 | 
						google.golang.org/grpc v1.73.0 // indirect
 | 
				
			||||||
 | 
						google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect
 | 
				
			||||||
 | 
						google.golang.org/protobuf v1.36.6 // indirect
 | 
				
			||||||
 | 
						gopkg.in/yaml.v3 v3.0.1 // indirect
 | 
				
			||||||
 | 
						howett.net/plist v1.0.0 // indirect
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
		Loading…
	
		Reference in New Issue