From 4fc0a8c10d5d2892e798ade70f4feb8638bfebcd Mon Sep 17 00:00:00 2001 From: Jason Cameron Date: Thu, 16 Jan 2025 19:28:29 -0500 Subject: [PATCH] Add the ability to load ranges from a text file Add the ability to load IP ranges from a text file and update documentation. * **config.go** - Add a new subdirective `ranges_file` to load ranges from a text file. - Update the `UnmarshalCaddyfile` function to handle the new subdirective. - Update the `Validate` function to validate the ranges loaded from the text file. - Add a check to throw an error if neither `ranges` nor `ranges_file` is specified. * **middleware.go** - Add logic to check if the client IP is in any of the ranges loaded from the text file. * **plugin.go** - Update the `DefenderMiddleware` struct to include a new field `RangesFile`. - Update the `Provision` function to load ranges from the specified text file. * **examples/range_files/Caddyfile** - Add a new example Caddyfile demonstrating the usage of `ranges_file` subdirective. * **examples/range_files/ranges.txt** - Add a new text file containing example IP ranges. * **README.md** - Add documentation for the new `ranges_file` subdirective. --- README.md | 13 +++++++++++++ config.go | 33 +++++++++++++++++++++++++++++++++ examples/range_files/Caddyfile | 15 +++++++++++++++ examples/range_files/ranges.txt | 3 +++ middleware.go | 30 ++++++++++++++++++++++++++++++ plugin.go | 29 +++++++++++++++++++++++++++++ 6 files changed, 123 insertions(+) create mode 100644 examples/range_files/Caddyfile create mode 100644 examples/range_files/ranges.txt diff --git a/README.md b/README.md index 20c6229..199bb38 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,7 @@ The `defender` directive is used to configure the Caddy Defender plugin. It has ```caddyfile defender [responder_args...] { range + ranges_file } ``` @@ -83,6 +84,7 @@ defender [responder_args...] { - `custom`: Returns a custom message (requires `responder_args`). - `[responder_args...]`: Additional arguments for the responder backend. For the `custom` responder, this is the custom message to return. - ``: A list of CIDR ranges or predefined range keys (e.g., `openai`, `localhost`) to match against the client's IP. +- ``: Path to a file containing IP ranges, one per line. #### **Ordering the Middleware** To ensure the `defender` middleware runs before other middleware (e.g., `basicauth`), add the following to your global configuration: @@ -130,6 +132,17 @@ localhost:8082 { } ``` +#### **Load IP Ranges from a File** +Load IP ranges from a file: +```caddyfile +localhost:8083 { + defender block { + ranges_file ./path/to/ranges.txt + } + respond "Hello, world!" # what humans see +} +``` + --- ## **Embedded IP Ranges** diff --git a/config.go b/config.go index 00e0d8b..ab5b731 100644 --- a/config.go +++ b/config.go @@ -1,11 +1,13 @@ package caddydefender import ( + "bufio" "encoding/json" "fmt" "github.com/jasonlovesdoggo/caddy-defender/ranges/data" "github.com/jasonlovesdoggo/caddy-defender/responders" "net" + "os" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" ) @@ -55,6 +57,11 @@ func (m *DefenderMiddleware) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.NextArg() { ranges = append(ranges, d.Val()) } + case "ranges_file": + if !d.NextArg() { + return d.ArgErr() + } + m.RangesFile = d.Val() default: return d.Errf("unknown subdirective '%s'", d.Val()) } @@ -123,6 +130,10 @@ func (m *DefenderMiddleware) Validate() error { return fmt.Errorf("responder not configured") } + if len(m.AdditionalRanges) == 0 && m.RangesFile == "" { + return fmt.Errorf("either 'ranges' or 'ranges_file' must be specified") + } + for _, ipRange := range m.AdditionalRanges { // Check if the range is a predefined key (e.g., "openai") if _, ok := data.IPRanges[ipRange]; ok { @@ -137,5 +148,27 @@ func (m *DefenderMiddleware) Validate() error { } } + // Validate ranges loaded from the text file + if m.RangesFile != "" { + file, err := os.Open(m.RangesFile) + if err != nil { + return fmt.Errorf("failed to open ranges file: %v", err) + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + _, _, err := net.ParseCIDR(line) + if err != nil { + return fmt.Errorf("invalid IP range in file %q: %v", line, err) + } + } + + if err := scanner.Err(); err != nil { + return fmt.Errorf("error reading ranges file: %v", err) + } + } + return nil } diff --git a/examples/range_files/Caddyfile b/examples/range_files/Caddyfile new file mode 100644 index 0000000..159e467 --- /dev/null +++ b/examples/range_files/Caddyfile @@ -0,0 +1,15 @@ +{ + auto_https off + order defender before basicauth +} + +:80 { + defender block { + ranges_file ./examples/range_files/ranges.txt + } + respond "This is what a human sees" +} + +:83 { + respond "Clear text HTTP" +} diff --git a/examples/range_files/ranges.txt b/examples/range_files/ranges.txt new file mode 100644 index 0000000..7b86a9c --- /dev/null +++ b/examples/range_files/ranges.txt @@ -0,0 +1,3 @@ +192.168.1.0/24 +10.0.0.0/8 +172.16.0.0/12 diff --git a/middleware.go b/middleware.go index 489717c..9efb9a6 100644 --- a/middleware.go +++ b/middleware.go @@ -1,10 +1,12 @@ package caddydefender import ( + "bufio" "fmt" "go.uber.org/zap" "net" "net/http" + "os" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/jasonlovesdoggo/caddy-defender/utils" @@ -31,6 +33,34 @@ func (m DefenderMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, ne return m.responder.Respond(w, r) } + // Check if the client IP is in any of the ranges loaded from the text file + if m.RangesFile != "" { + file, err := os.Open(m.RangesFile) + if err != nil { + m.log.Error("Failed to open ranges file", zap.String("file", m.RangesFile)) + return caddyhttp.Error(http.StatusInternalServerError, fmt.Errorf("failed to open ranges file")) + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + _, ipNet, err := net.ParseCIDR(line) + if err != nil { + m.log.Error("Invalid IP range in file", zap.String("range", line)) + continue + } + if ipNet.Contains(clientIP) { + return m.responder.Respond(w, r) + } + } + + if err := scanner.Err(); err != nil { + m.log.Error("Error reading ranges file", zap.String("file", m.RangesFile)) + return caddyhttp.Error(http.StatusInternalServerError, fmt.Errorf("error reading ranges file")) + } + } + // IP is not in any of the ranges, proceed to the next handler return next.ServeHTTP(w, r) } diff --git a/plugin.go b/plugin.go index bd23457..648f530 100644 --- a/plugin.go +++ b/plugin.go @@ -1,11 +1,14 @@ package caddydefender import ( + "bufio" "encoding/json" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "go.uber.org/zap" + "net" + "os" ) func init() { @@ -25,6 +28,9 @@ type DefenderMiddleware struct { responder Responder `json:"-"` ResponderConfig json.RawMessage `json:"responder_config,omitempty"` + // RangesFile specifies the path to a file containing IP ranges + RangesFile string `json:"ranges_file,omitempty"` + // Logger log *zap.Logger } @@ -32,6 +38,29 @@ type DefenderMiddleware struct { // Provision sets up the middleware and logger. func (m *DefenderMiddleware) Provision(ctx caddy.Context) error { m.log = ctx.Logger(m) + + // Load ranges from the specified text file + if m.RangesFile != "" { + file, err := os.Open(m.RangesFile) + if err != nil { + return err + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + _, _, err := net.ParseCIDR(line) + if err != nil { + return err + } + } + + if err := scanner.Err(); err != nil { + return err + } + } + return nil }