Implement IP/CIDR matcher and Not (negated) matcher

This commit is contained in:
Matthew Holt 2019-06-04 13:42:54 -06:00
parent f064889a4f
commit 39db06d9c4
1 changed files with 131 additions and 15 deletions

View File

@ -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)
)