Skip to content

Commit

Permalink
Add the ability to load ranges from a text file
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
JasonLovesDoggo committed Jan 17, 2025
1 parent e4a3e26 commit 4fc0a8c
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 0 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ The `defender` directive is used to configure the Caddy Defender plugin. It has
```caddyfile
defender <responder> [responder_args...] {
range <ip_ranges...>
ranges_file <file_path>
}
```

Expand All @@ -83,6 +84,7 @@ defender <responder> [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.
- `<ip_ranges...>`: A list of CIDR ranges or predefined range keys (e.g., `openai`, `localhost`) to match against the client's IP.
- `<file_path>`: 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:
Expand Down Expand Up @@ -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**
Expand Down
33 changes: 33 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
@@ -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"
)
Expand Down Expand Up @@ -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())
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
15 changes: 15 additions & 0 deletions examples/range_files/Caddyfile
Original file line number Diff line number Diff line change
@@ -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"
}
3 changes: 3 additions & 0 deletions examples/range_files/ranges.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
192.168.1.0/24
10.0.0.0/8
172.16.0.0/12
30 changes: 30 additions & 0 deletions middleware.go
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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)
}
29 changes: 29 additions & 0 deletions plugin.go
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -25,13 +28,39 @@ 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
}

// 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
}

Expand Down

0 comments on commit 4fc0a8c

Please sign in to comment.