Organize backend into product executable and shared Go modules
This commit is contained in:
1 parent
981e84a4b4
commit
f0720ec66b
382 files changed
+1944
-6989
No files matched your search
@@ -0,0 +1,73 @@
|
||||
package ipguard
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ClientIP resolves the real caller IP from the request.
|
||||
// When the request arrives via the Caddy reverse proxy, the real client IP is
|
||||
// forwarded in the X-Real-IP header (set via "header_up X-Real-IP {remote_host}"
|
||||
// in the Caddyfile). RemoteAddr is used as a fallback for direct connections
|
||||
// (local development, health checks, etc.).
|
||||
func ClientIP(r *http.Request) net.IP {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
// Prefer X-Real-IP set by the trusted Caddy reverse proxy.
|
||||
if xri := strings.TrimSpace(r.Header.Get("X-Real-IP")); xri != "" {
|
||||
if ip := net.ParseIP(xri); ip != nil {
|
||||
return ip
|
||||
}
|
||||
}
|
||||
raw := strings.TrimSpace(r.RemoteAddr)
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
if ip := net.ParseIP(raw); ip != nil {
|
||||
return ip
|
||||
}
|
||||
host, _, err := net.SplitHostPort(raw)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return net.ParseIP(host)
|
||||
}
|
||||
|
||||
func parseCIDRs(raw []string) ([]*net.IPNet, error) {
|
||||
blocks := make([]*net.IPNet, 0, len(raw))
|
||||
for _, item := range raw {
|
||||
clean := strings.TrimSpace(item)
|
||||
if clean == "" {
|
||||
continue
|
||||
}
|
||||
_, block, err := net.ParseCIDR(clean)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blocks = append(blocks, block)
|
||||
}
|
||||
return blocks, nil
|
||||
}
|
||||
|
||||
// Allowed reports whether clientIP is allowed by configured CIDRs.
|
||||
// Empty CIDR list means unrestricted access.
|
||||
func Allowed(clientIP net.IP, cidrs []string) (bool, error) {
|
||||
blocks, err := parseCIDRs(cidrs)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if len(blocks) == 0 {
|
||||
return true, nil
|
||||
}
|
||||
if clientIP == nil {
|
||||
return false, nil
|
||||
}
|
||||
for _, block := range blocks {
|
||||
if block.Contains(clientIP) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package ipguard
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestClientIP(t *testing.T) {
|
||||
t.Run("extracts host from remote addr", func(t *testing.T) {
|
||||
req := &http.Request{RemoteAddr: "10.1.2.3:1234"}
|
||||
ip := ClientIP(req)
|
||||
if ip == nil || ip.String() != "10.1.2.3" {
|
||||
t.Fatalf("unexpected ip: %v", ip)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("supports plain ip", func(t *testing.T) {
|
||||
req := &http.Request{RemoteAddr: "8.8.8.8"}
|
||||
ip := ClientIP(req)
|
||||
if ip == nil || ip.String() != "8.8.8.8" {
|
||||
t.Fatalf("unexpected ip: %v", ip)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid remote addr", func(t *testing.T) {
|
||||
req := &http.Request{RemoteAddr: "invalid"}
|
||||
if ip := ClientIP(req); ip != nil {
|
||||
t.Fatalf("expected nil ip, got %v", ip)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("prefers X-Real-IP over remote addr", func(t *testing.T) {
|
||||
req := &http.Request{
|
||||
RemoteAddr: "172.19.0.31:5000",
|
||||
Header: http.Header{"X-Real-Ip": []string{"178.57.67.154"}},
|
||||
}
|
||||
ip := ClientIP(req)
|
||||
if ip == nil || ip.String() != "178.57.67.154" {
|
||||
t.Fatalf("expected real client ip, got %v", ip)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("falls back to remote addr when X-Real-IP absent", func(t *testing.T) {
|
||||
req := &http.Request{RemoteAddr: "10.0.0.1:9000"}
|
||||
ip := ClientIP(req)
|
||||
if ip == nil || ip.String() != "10.0.0.1" {
|
||||
t.Fatalf("expected remote addr ip, got %v", ip)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("falls back to remote addr when X-Real-IP invalid", func(t *testing.T) {
|
||||
req := &http.Request{
|
||||
RemoteAddr: "10.0.0.1:9000",
|
||||
Header: http.Header{"X-Real-Ip": []string{"not-an-ip"}},
|
||||
}
|
||||
ip := ClientIP(req)
|
||||
if ip == nil || ip.String() != "10.0.0.1" {
|
||||
t.Fatalf("expected fallback to remote addr, got %v", ip)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAllowed(t *testing.T) {
|
||||
clientIP := net.ParseIP("10.1.2.3")
|
||||
if clientIP == nil {
|
||||
t.Fatal("failed to parse test ip")
|
||||
}
|
||||
|
||||
t.Run("allows when cidr matches", func(t *testing.T) {
|
||||
allowed, err := Allowed(clientIP, []string{"10.0.0.0/8"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !allowed {
|
||||
t.Fatal("expected allowed")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("denies when cidr does not match", func(t *testing.T) {
|
||||
allowed, err := Allowed(clientIP, []string{"192.168.0.0/16"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if allowed {
|
||||
t.Fatal("expected denied")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("allows when cidr list is empty", func(t *testing.T) {
|
||||
allowed, err := Allowed(clientIP, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !allowed {
|
||||
t.Fatal("expected allowed")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid cidr fails", func(t *testing.T) {
|
||||
_, err := Allowed(clientIP, []string{"not-a-cidr"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nil client ip denied when cidrs configured", func(t *testing.T) {
|
||||
allowed, err := Allowed(nil, []string{"10.0.0.0/8"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if allowed {
|
||||
t.Fatal("expected denied")
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user