From 39db06d9c4bdf6bb793916225b8915bf777642b4 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Tue, 4 Jun 2019 13:42:54 -0600 Subject: Implement IP/CIDR matcher and Not (negated) matcher --- modules/caddyhttp/matchers.go | 146 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 131 insertions(+), 15 deletions(-) diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go index 0dda205..d729acf 100644 --- a/modules/caddyhttp/matchers.go +++ b/modules/caddyhttp/matchers.go @@ -1,8 +1,10 @@ package caddyhttp import ( + "encoding/json" "fmt" "log" + "net" "net/http" "net/textproto" "net/url" @@ -41,6 +43,20 @@ type ( // MatchProtocol matches requests by protocol. MatchProtocol string + // MatchRemoteIP matches requests by client IP (or CIDR range). + MatchRemoteIP struct { + Ranges []string `json:"ranges,omitempty"` + + cidrs []*net.IPNet + } + + // MatchNegate matches requests by negating its matchers' results. + MatchNegate struct { + matchersRaw map[string]json.RawMessage + + matchers MatcherSet + } + // MatchStarlarkExpr matches requests by evaluating a Starlark expression. MatchStarlarkExpr string @@ -51,11 +67,11 @@ type ( func init() { caddy2.RegisterModule(caddy2.Module{ Name: "http.matchers.host", - New: func() interface{} { return MatchHost{} }, + New: func() interface{} { return new(MatchHost) }, }) caddy2.RegisterModule(caddy2.Module{ Name: "http.matchers.path", - New: func() interface{} { return MatchPath{} }, + New: func() interface{} { return new(MatchPath) }, }) caddy2.RegisterModule(caddy2.Module{ Name: "http.matchers.path_regexp", @@ -63,24 +79,32 @@ func init() { }) caddy2.RegisterModule(caddy2.Module{ Name: "http.matchers.method", - New: func() interface{} { return MatchMethod{} }, + New: func() interface{} { return new(MatchMethod) }, }) caddy2.RegisterModule(caddy2.Module{ Name: "http.matchers.query", - New: func() interface{} { return MatchQuery{} }, + New: func() interface{} { return new(MatchQuery) }, }) caddy2.RegisterModule(caddy2.Module{ Name: "http.matchers.header", - New: func() interface{} { return MatchHeader{} }, + New: func() interface{} { return new(MatchHeader) }, }) caddy2.RegisterModule(caddy2.Module{ Name: "http.matchers.header_regexp", - New: func() interface{} { return MatchHeaderRE{} }, + New: func() interface{} { return new(MatchHeaderRE) }, }) caddy2.RegisterModule(caddy2.Module{ Name: "http.matchers.protocol", New: func() interface{} { return new(MatchProtocol) }, }) + caddy2.RegisterModule(caddy2.Module{ + Name: "http.matchers.remote_ip", + New: func() interface{} { return new(MatchRemoteIP) }, + }) + caddy2.RegisterModule(caddy2.Module{ + Name: "http.matchers.not", + New: func() interface{} { return new(MatchNegate) }, + }) caddy2.RegisterModule(caddy2.Module{ Name: "http.matchers.starlark_expr", New: func() interface{} { return new(MatchStarlarkExpr) }, @@ -229,6 +253,94 @@ func (m MatchProtocol) Match(r *http.Request) bool { return false } +// UnmarshalJSON unmarshals data into m's unexported map field. +// This is done because we cannot embed the map directly into +// the struct, but we need a struct because we need another +// field just for the provisioned modules. +func (m *MatchNegate) UnmarshalJSON(data []byte) error { + return json.Unmarshal(data, &m.matchersRaw) +} + +// Provision loads the matcher modules to be negated. +func (m *MatchNegate) Provision(ctx caddy2.Context) error { + for modName, rawMsg := range m.matchersRaw { + val, err := ctx.LoadModule("http.matchers."+modName, rawMsg) + if err != nil { + return fmt.Errorf("loading matcher module '%s': %v", modName, err) + } + m.matchers = append(m.matchers, val.(RequestMatcher)) + } + m.matchersRaw = nil // allow GC to deallocate - TODO: Does this help? + return nil +} + +// Match returns true if r matches m. Since this matcher negates the +// embedded matchers, false is returned if any of its matchers match. +func (m MatchNegate) Match(r *http.Request) bool { + return !m.matchers.Match(r) +} + +// Provision parses m's IP ranges, either from IP or CIDR expressions. +func (m *MatchRemoteIP) Provision(ctx caddy2.Context) error { + for _, str := range m.Ranges { + if strings.Contains(str, "/") { + _, ipNet, err := net.ParseCIDR(str) + if err != nil { + return fmt.Errorf("parsing CIDR expression: %v", err) + } + m.cidrs = append(m.cidrs, ipNet) + } else { + ip := net.ParseIP(str) + if ip == nil { + return fmt.Errorf("invalid IP address: %s", str) + } + mask := len(ip) * 8 + m.cidrs = append(m.cidrs, &net.IPNet{ + IP: ip, + Mask: net.CIDRMask(mask, mask), + }) + } + } + return nil +} + +func (m MatchRemoteIP) getClientIP(r *http.Request) (net.IP, error) { + var remote string + if fwdFor := r.Header.Get("X-Forwarded-For"); fwdFor != "" { + remote = strings.TrimSpace(strings.Split(fwdFor, ",")[0]) + } + if remote == "" { + remote = r.RemoteAddr + } + + ipStr, _, err := net.SplitHostPort(remote) + if err != nil { + ipStr = remote // OK; probably didn't have a port + } + + ip := net.ParseIP(ipStr) + if ip == nil { + return nil, fmt.Errorf("invalid client IP address: %s", ipStr) + } + + return ip, nil +} + +// Match returns true if r matches m. +func (m MatchRemoteIP) Match(r *http.Request) bool { + clientIP, err := m.getClientIP(r) + if err != nil { + log.Printf("[ERROR] remote_ip matcher: %v", err) + return false + } + for _, ipRange := range m.cidrs { + if ipRange.Contains(clientIP) { + return true + } + } + return false +} + // Match returns true if r matches m. func (m MatchStarlarkExpr) Match(r *http.Request) bool { input := string(m) @@ -357,13 +469,17 @@ var wordRE = regexp.MustCompile(`\w+`) // Interface guards var ( - _ RequestMatcher = (*MatchHost)(nil) - _ RequestMatcher = (*MatchPath)(nil) - _ RequestMatcher = (*MatchPathRE)(nil) - _ RequestMatcher = (*MatchMethod)(nil) - _ RequestMatcher = (*MatchQuery)(nil) - _ RequestMatcher = (*MatchHeader)(nil) - _ RequestMatcher = (*MatchHeaderRE)(nil) - _ RequestMatcher = (*MatchProtocol)(nil) - _ RequestMatcher = (*MatchStarlarkExpr)(nil) + _ RequestMatcher = (*MatchHost)(nil) + _ RequestMatcher = (*MatchPath)(nil) + _ RequestMatcher = (*MatchPathRE)(nil) + _ RequestMatcher = (*MatchMethod)(nil) + _ RequestMatcher = (*MatchQuery)(nil) + _ RequestMatcher = (*MatchHeader)(nil) + _ RequestMatcher = (*MatchHeaderRE)(nil) + _ RequestMatcher = (*MatchProtocol)(nil) + _ RequestMatcher = (*MatchRemoteIP)(nil) + _ caddy2.Provisioner = (*MatchRemoteIP)(nil) + _ RequestMatcher = (*MatchNegate)(nil) + _ caddy2.Provisioner = (*MatchNegate)(nil) + _ RequestMatcher = (*MatchStarlarkExpr)(nil) ) -- cgit v1.2.3