mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-23 22:27:38 -05:00
6a4d638c1e
* caddyhttp: Implement CEL matcher (see #3051) CEL (Common Expression Language) is a very fast, flexible way to express complex logic, useful for matching requests when the conditions are not easy to express with JSON. This matcher may be considered experimental even after the 2.0 release. * Improve CEL module docs
870 lines
24 KiB
Go
870 lines
24 KiB
Go
// Copyright 2015 Matthew Holt and The Caddy Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package caddyhttp
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"net/textproto"
|
|
"net/url"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/caddyserver/caddy/v2"
|
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
|
)
|
|
|
|
type (
|
|
// MatchHost matches requests by the Host value (case-insensitive).
|
|
//
|
|
// When used in a top-level HTTP route,
|
|
// [qualifying domain names](/docs/automatic-https#hostname-requirements)
|
|
// may trigger [automatic HTTPS](/docs/automatic-https), which automatically
|
|
// provisions and renews certificates for you. Before doing this, you
|
|
// should ensure that DNS records for these domains are properly configured,
|
|
// especially A/AAAA pointed at your server.
|
|
//
|
|
// Automatic HTTPS can be
|
|
// [customized or disabled](/docs/json/apps/http/servers/automatic_https/).
|
|
MatchHost []string
|
|
|
|
// MatchPath matches requests by the URI's path (case-insensitive). Path
|
|
// matches are exact, but wildcards may be used:
|
|
//
|
|
// - At the end, for a prefix match (`/prefix/*`)
|
|
// - At the beginning, for a suffix match (`*.suffix`)
|
|
// - On both sides, for a substring match (`*/contains/*`)
|
|
// - In the middle, for a globular match (`/accounts/*/info`)
|
|
//
|
|
// This matcher is fast, so it does not support regular expressions or
|
|
// capture groups. For slower but more powerful matching, use the
|
|
// path_regexp matcher.
|
|
MatchPath []string
|
|
|
|
// MatchPathRE matches requests by a regular expression on the URI's path.
|
|
//
|
|
// Upon a match, it adds placeholders to the request: `{http.regexp.name.capture_group}`
|
|
// where `name` is the regular expression's name, and `capture_group` is either
|
|
// the named or positional capture group from the expression itself. If no name
|
|
// is given, then the placeholder omits the name: `{http.regexp.capture_group}`
|
|
// (potentially leading to collisions).
|
|
MatchPathRE struct{ MatchRegexp }
|
|
|
|
// MatchMethod matches requests by the method.
|
|
MatchMethod []string
|
|
|
|
// MatchQuery matches requests by URI's query string.
|
|
MatchQuery url.Values
|
|
|
|
// MatchHeader matches requests by header fields. It performs fast,
|
|
// exact string comparisons of the field values. Fast prefix, suffix,
|
|
// and substring matches can also be done by suffixing, prefixing, or
|
|
// surrounding the value with the wildcard `*` character, respectively.
|
|
// If a list is null, the header must not exist. If the list is empty,
|
|
// the field must simply exist, regardless of its value.
|
|
MatchHeader http.Header
|
|
|
|
// MatchHeaderRE matches requests by a regular expression on header fields.
|
|
//
|
|
// Upon a match, it adds placeholders to the request: `{http.regexp.name.capture_group}`
|
|
// where `name` is the regular expression's name, and `capture_group` is either
|
|
// the named or positional capture group from the expression itself. If no name
|
|
// is given, then the placeholder omits the name: `{http.regexp.capture_group}`
|
|
// (potentially leading to collisions).
|
|
MatchHeaderRE map[string]*MatchRegexp
|
|
|
|
// 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.
|
|
// To use, simply specify a set of matchers like you normally would;
|
|
// the only difference is that their result will be negated.
|
|
MatchNegate struct {
|
|
MatchersRaw caddy.ModuleMap `json:"-" caddy:"namespace=http.matchers"`
|
|
|
|
Matchers MatcherSet `json:"-"`
|
|
}
|
|
)
|
|
|
|
func init() {
|
|
caddy.RegisterModule(MatchHost{})
|
|
caddy.RegisterModule(MatchPath{})
|
|
caddy.RegisterModule(MatchPathRE{})
|
|
caddy.RegisterModule(MatchMethod{})
|
|
caddy.RegisterModule(MatchQuery{})
|
|
caddy.RegisterModule(MatchHeader{})
|
|
caddy.RegisterModule(MatchHeaderRE{})
|
|
caddy.RegisterModule(new(MatchProtocol))
|
|
caddy.RegisterModule(MatchRemoteIP{})
|
|
caddy.RegisterModule(MatchNegate{})
|
|
}
|
|
|
|
// CaddyModule returns the Caddy module information.
|
|
func (MatchHost) CaddyModule() caddy.ModuleInfo {
|
|
return caddy.ModuleInfo{
|
|
ID: "http.matchers.host",
|
|
New: func() caddy.Module { return new(MatchHost) },
|
|
}
|
|
}
|
|
|
|
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
|
|
func (m *MatchHost) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
|
for d.Next() {
|
|
*m = append(*m, d.RemainingArgs()...)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Match returns true if r matches m.
|
|
func (m MatchHost) Match(r *http.Request) bool {
|
|
reqHost, _, err := net.SplitHostPort(r.Host)
|
|
if err != nil {
|
|
// OK; probably didn't have a port
|
|
reqHost = r.Host
|
|
|
|
// make sure we strip the brackets from IPv6 addresses
|
|
reqHost = strings.TrimPrefix(reqHost, "[")
|
|
reqHost = strings.TrimSuffix(reqHost, "]")
|
|
}
|
|
|
|
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
|
|
|
|
outer:
|
|
for _, host := range m {
|
|
host = repl.ReplaceAll(host, "")
|
|
if strings.Contains(host, "*") {
|
|
patternParts := strings.Split(host, ".")
|
|
incomingParts := strings.Split(reqHost, ".")
|
|
if len(patternParts) != len(incomingParts) {
|
|
continue
|
|
}
|
|
for i := range patternParts {
|
|
if patternParts[i] == "*" {
|
|
continue
|
|
}
|
|
if !strings.EqualFold(patternParts[i], incomingParts[i]) {
|
|
continue outer
|
|
}
|
|
}
|
|
return true
|
|
} else if strings.EqualFold(reqHost, host) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// CaddyModule returns the Caddy module information.
|
|
func (MatchPath) CaddyModule() caddy.ModuleInfo {
|
|
return caddy.ModuleInfo{
|
|
ID: "http.matchers.path",
|
|
New: func() caddy.Module { return new(MatchPath) },
|
|
}
|
|
}
|
|
|
|
// Provision lower-cases the paths in m to ensure case-insensitive matching.
|
|
func (m MatchPath) Provision(_ caddy.Context) error {
|
|
for i := range m {
|
|
m[i] = strings.ToLower(m[i])
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Match returns true if r matches m.
|
|
func (m MatchPath) Match(r *http.Request) bool {
|
|
lowerPath := strings.ToLower(r.URL.Path)
|
|
|
|
// see #2917; Windows ignores trailing dots and spaces
|
|
// when accessing files (sigh), potentially causing a
|
|
// security risk (cry) if PHP files end up being served
|
|
// as static files, exposing the source code, instead of
|
|
// being matched by *.php to be treated as PHP scripts
|
|
lowerPath = strings.TrimRight(lowerPath, ". ")
|
|
|
|
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
|
|
|
|
for _, matchPath := range m {
|
|
matchPath = repl.ReplaceAll(matchPath, "")
|
|
|
|
// special case: whole path is wildcard; this is unnecessary
|
|
// as it matches all requests, which is the same as no matcher
|
|
if matchPath == "*" {
|
|
return true
|
|
}
|
|
|
|
// special case: first and last characters are wildcard,
|
|
// treat it as a fast substring match
|
|
if len(matchPath) > 1 &&
|
|
strings.HasPrefix(matchPath, "*") &&
|
|
strings.HasSuffix(matchPath, "*") {
|
|
if strings.Contains(lowerPath, matchPath[1:len(matchPath)-1]) {
|
|
return true
|
|
}
|
|
continue
|
|
}
|
|
|
|
// special case: first character is a wildcard,
|
|
// treat it as a fast suffix match
|
|
if strings.HasPrefix(matchPath, "*") {
|
|
if strings.HasSuffix(lowerPath, matchPath[1:]) {
|
|
return true
|
|
}
|
|
continue
|
|
}
|
|
|
|
// special case: last character is a wildcard,
|
|
// treat it as a fast prefix match
|
|
if strings.HasSuffix(matchPath, "*") {
|
|
if strings.HasPrefix(lowerPath, matchPath[:len(matchPath)-1]) {
|
|
return true
|
|
}
|
|
continue
|
|
}
|
|
|
|
// for everything else, try globular matching, which also
|
|
// is exact matching if there are no glob/wildcard chars;
|
|
// can ignore error here because we can't handle it anyway
|
|
matches, _ := filepath.Match(matchPath, lowerPath)
|
|
if matches {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
|
|
func (m *MatchPath) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
|
for d.Next() {
|
|
*m = append(*m, d.RemainingArgs()...)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CaddyModule returns the Caddy module information.
|
|
func (MatchPathRE) CaddyModule() caddy.ModuleInfo {
|
|
return caddy.ModuleInfo{
|
|
ID: "http.matchers.path_regexp",
|
|
New: func() caddy.Module { return new(MatchPathRE) },
|
|
}
|
|
}
|
|
|
|
// Match returns true if r matches m.
|
|
func (m MatchPathRE) Match(r *http.Request) bool {
|
|
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
|
|
return m.MatchRegexp.Match(r.URL.Path, repl)
|
|
}
|
|
|
|
// CaddyModule returns the Caddy module information.
|
|
func (MatchMethod) CaddyModule() caddy.ModuleInfo {
|
|
return caddy.ModuleInfo{
|
|
ID: "http.matchers.method",
|
|
New: func() caddy.Module { return new(MatchMethod) },
|
|
}
|
|
}
|
|
|
|
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
|
|
func (m *MatchMethod) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
|
for d.Next() {
|
|
*m = append(*m, d.RemainingArgs()...)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Match returns true if r matches m.
|
|
func (m MatchMethod) Match(r *http.Request) bool {
|
|
for _, method := range m {
|
|
if r.Method == method {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// CaddyModule returns the Caddy module information.
|
|
func (MatchQuery) CaddyModule() caddy.ModuleInfo {
|
|
return caddy.ModuleInfo{
|
|
ID: "http.matchers.query",
|
|
New: func() caddy.Module { return new(MatchQuery) },
|
|
}
|
|
}
|
|
|
|
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
|
|
func (m *MatchQuery) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
|
if *m == nil {
|
|
*m = make(map[string][]string)
|
|
}
|
|
|
|
for d.Next() {
|
|
var query string
|
|
if !d.Args(&query) {
|
|
return d.ArgErr()
|
|
}
|
|
parts := strings.SplitN(query, "=", 2)
|
|
if len(parts) != 2 {
|
|
return d.Errf("malformed query matcher token: %s; must be in param=val format", d.Val())
|
|
}
|
|
url.Values(*m).Set(parts[0], parts[1])
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Match returns true if r matches m.
|
|
func (m MatchQuery) Match(r *http.Request) bool {
|
|
for param, vals := range m {
|
|
paramVal, found := r.URL.Query()[param]
|
|
if found {
|
|
for _, v := range vals {
|
|
if paramVal[0] == v || v == "*" {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// CaddyModule returns the Caddy module information.
|
|
func (MatchHeader) CaddyModule() caddy.ModuleInfo {
|
|
return caddy.ModuleInfo{
|
|
ID: "http.matchers.header",
|
|
New: func() caddy.Module { return new(MatchHeader) },
|
|
}
|
|
}
|
|
|
|
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
|
|
func (m *MatchHeader) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
|
if *m == nil {
|
|
*m = make(map[string][]string)
|
|
}
|
|
for d.Next() {
|
|
var field, val string
|
|
if !d.Args(&field, &val) {
|
|
return d.Errf("expected both field and value")
|
|
}
|
|
http.Header(*m).Set(field, val)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Like req.Header.Get(), but that works with Host header.
|
|
// go's http module swallows "Host" header.
|
|
func getHeader(r *http.Request, field string) []string {
|
|
field = textproto.CanonicalMIMEHeaderKey(field)
|
|
|
|
if field == "Host" {
|
|
return []string{r.Host}
|
|
}
|
|
|
|
return r.Header[field]
|
|
}
|
|
|
|
// Match returns true if r matches m.
|
|
func (m MatchHeader) Match(r *http.Request) bool {
|
|
for field, allowedFieldVals := range m {
|
|
actualFieldVals := getHeader(r, field)
|
|
|
|
if allowedFieldVals != nil && len(allowedFieldVals) == 0 && actualFieldVals != nil {
|
|
// a non-nil but empty list of allowed values means
|
|
// match if the header field exists at all
|
|
continue
|
|
}
|
|
var match bool
|
|
fieldVals:
|
|
for _, actualFieldVal := range actualFieldVals {
|
|
for _, allowedFieldVal := range allowedFieldVals {
|
|
switch {
|
|
case allowedFieldVal == "*":
|
|
match = true
|
|
case strings.HasPrefix(allowedFieldVal, "*") && strings.HasSuffix(allowedFieldVal, "*"):
|
|
match = strings.Contains(actualFieldVal, allowedFieldVal[1:len(allowedFieldVal)-1])
|
|
case strings.HasPrefix(allowedFieldVal, "*"):
|
|
match = strings.HasSuffix(actualFieldVal, allowedFieldVal[1:])
|
|
case strings.HasSuffix(allowedFieldVal, "*"):
|
|
match = strings.HasPrefix(actualFieldVal, allowedFieldVal[:len(allowedFieldVal)-1])
|
|
default:
|
|
match = actualFieldVal == allowedFieldVal
|
|
}
|
|
if match {
|
|
break fieldVals
|
|
}
|
|
}
|
|
}
|
|
if !match {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// CaddyModule returns the Caddy module information.
|
|
func (MatchHeaderRE) CaddyModule() caddy.ModuleInfo {
|
|
return caddy.ModuleInfo{
|
|
ID: "http.matchers.header_regexp",
|
|
New: func() caddy.Module { return new(MatchHeaderRE) },
|
|
}
|
|
}
|
|
|
|
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
|
|
func (m *MatchHeaderRE) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
|
if *m == nil {
|
|
*m = make(map[string]*MatchRegexp)
|
|
}
|
|
for d.Next() {
|
|
var first, second, third string
|
|
if !d.Args(&first, &second) {
|
|
return d.ArgErr()
|
|
}
|
|
|
|
var name, field, val string
|
|
if d.Args(&third) {
|
|
name = first
|
|
field = second
|
|
val = third
|
|
} else {
|
|
field = first
|
|
val = second
|
|
}
|
|
|
|
(*m)[field] = &MatchRegexp{Pattern: val, Name: name}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Match returns true if r matches m.
|
|
func (m MatchHeaderRE) Match(r *http.Request) bool {
|
|
for field, rm := range m {
|
|
actualFieldVals := getHeader(r, field)
|
|
|
|
match := false
|
|
fieldVal:
|
|
for _, actualFieldVal := range actualFieldVals {
|
|
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
|
|
if rm.Match(actualFieldVal, repl) {
|
|
match = true
|
|
break fieldVal
|
|
}
|
|
}
|
|
if !match {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Provision compiles m's regular expressions.
|
|
func (m MatchHeaderRE) Provision(ctx caddy.Context) error {
|
|
for _, rm := range m {
|
|
err := rm.Provision(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Validate validates m's regular expressions.
|
|
func (m MatchHeaderRE) Validate() error {
|
|
for _, rm := range m {
|
|
err := rm.Validate()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CaddyModule returns the Caddy module information.
|
|
func (MatchProtocol) CaddyModule() caddy.ModuleInfo {
|
|
return caddy.ModuleInfo{
|
|
ID: "http.matchers.protocol",
|
|
New: func() caddy.Module { return new(MatchProtocol) },
|
|
}
|
|
}
|
|
|
|
// Match returns true if r matches m.
|
|
func (m MatchProtocol) Match(r *http.Request) bool {
|
|
switch string(m) {
|
|
case "grpc":
|
|
return r.Header.Get("content-type") == "application/grpc"
|
|
case "https":
|
|
return r.TLS != nil
|
|
case "http":
|
|
return r.TLS == nil
|
|
}
|
|
return false
|
|
}
|
|
|
|
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
|
|
func (m *MatchProtocol) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
|
for d.Next() {
|
|
var proto string
|
|
if !d.Args(&proto) {
|
|
return d.Err("expected exactly one protocol")
|
|
}
|
|
*m = MatchProtocol(proto)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CaddyModule returns the Caddy module information.
|
|
func (MatchNegate) CaddyModule() caddy.ModuleInfo {
|
|
return caddy.ModuleInfo{
|
|
ID: "http.matchers.not",
|
|
New: func() caddy.Module { return new(MatchNegate) },
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// MarshalJSON marshals m's matchers.
|
|
func (m MatchNegate) MarshalJSON() ([]byte, error) {
|
|
return json.Marshal(m.MatchersRaw)
|
|
}
|
|
|
|
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
|
|
func (m *MatchNegate) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
|
// first, unmarshal each matcher in the set from its tokens
|
|
|
|
matcherMap := make(map[string]RequestMatcher)
|
|
for d.Next() {
|
|
for d.NextBlock(0) {
|
|
matcherName := d.Val()
|
|
mod, err := caddy.GetModule("http.matchers." + matcherName)
|
|
if err != nil {
|
|
return d.Errf("getting matcher module '%s': %v", matcherName, err)
|
|
}
|
|
unm, ok := mod.New().(caddyfile.Unmarshaler)
|
|
if !ok {
|
|
return d.Errf("matcher module '%s' is not a Caddyfile unmarshaler", matcherName)
|
|
}
|
|
err = unm.UnmarshalCaddyfile(d.NewFromNextSegment())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rm := unm.(RequestMatcher)
|
|
m.Matchers = append(m.Matchers, rm)
|
|
matcherMap[matcherName] = rm
|
|
}
|
|
}
|
|
|
|
// we should now be functional, but we also need
|
|
// to be able to marshal as JSON, otherwise config
|
|
// adaptation won't work properly
|
|
m.MatchersRaw = make(caddy.ModuleMap)
|
|
for name, matchers := range matcherMap {
|
|
jsonBytes, err := json.Marshal(matchers)
|
|
if err != nil {
|
|
return fmt.Errorf("marshaling matcher %s: %v", name, err)
|
|
}
|
|
m.MatchersRaw[name] = jsonBytes
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Provision loads the matcher modules to be negated.
|
|
func (m *MatchNegate) Provision(ctx caddy.Context) error {
|
|
mods, err := ctx.LoadModule(m, "MatchersRaw")
|
|
if err != nil {
|
|
return fmt.Errorf("loading matchers: %v", err)
|
|
}
|
|
for _, modIface := range mods.(map[string]interface{}) {
|
|
m.Matchers = append(m.Matchers, modIface.(RequestMatcher))
|
|
}
|
|
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)
|
|
}
|
|
|
|
// CaddyModule returns the Caddy module information.
|
|
func (MatchRemoteIP) CaddyModule() caddy.ModuleInfo {
|
|
return caddy.ModuleInfo{
|
|
ID: "http.matchers.remote_ip",
|
|
New: func() caddy.Module { return new(MatchRemoteIP) },
|
|
}
|
|
}
|
|
|
|
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
|
|
func (m *MatchRemoteIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
|
for d.Next() {
|
|
m.Ranges = append(m.Ranges, d.RemainingArgs()...)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Provision parses m's IP ranges, either from IP or CIDR expressions.
|
|
func (m *MatchRemoteIP) Provision(ctx caddy.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
|
|
}
|
|
|
|
// MatchRegexp is an embedable type for matching
|
|
// using regular expressions. It adds placeholders
|
|
// to the request's replacer.
|
|
type MatchRegexp struct {
|
|
// A unique name for this regular expression. Optional,
|
|
// but useful to prevent overwriting captures from other
|
|
// regexp matchers.
|
|
Name string `json:"name,omitempty"`
|
|
|
|
// The regular expression to evaluate, in RE2 syntax,
|
|
// which is the same general syntax used by Go, Perl,
|
|
// and Python. For details, see
|
|
// [Go's regexp package](https://golang.org/pkg/regexp/).
|
|
// Captures are accessible via placeholders. Unnamed
|
|
// capture groups are exposed as their numeric, 1-based
|
|
// index, while named capture groups are available by
|
|
// the capture group name.
|
|
Pattern string `json:"pattern"`
|
|
|
|
compiled *regexp.Regexp
|
|
phPrefix string
|
|
}
|
|
|
|
// Provision compiles the regular expression.
|
|
func (mre *MatchRegexp) Provision(caddy.Context) error {
|
|
re, err := regexp.Compile(mre.Pattern)
|
|
if err != nil {
|
|
return fmt.Errorf("compiling matcher regexp %s: %v", mre.Pattern, err)
|
|
}
|
|
mre.compiled = re
|
|
mre.phPrefix = regexpPlaceholderPrefix
|
|
if mre.Name != "" {
|
|
mre.phPrefix += "." + mre.Name
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Validate ensures mre is set up correctly.
|
|
func (mre *MatchRegexp) Validate() error {
|
|
if mre.Name != "" && !wordRE.MatchString(mre.Name) {
|
|
return fmt.Errorf("invalid regexp name (must contain only word characters): %s", mre.Name)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Match returns true if input matches the compiled regular
|
|
// expression in mre. It sets values on the replacer repl
|
|
// associated with capture groups, using the given scope
|
|
// (namespace).
|
|
func (mre *MatchRegexp) Match(input string, repl *caddy.Replacer) bool {
|
|
matches := mre.compiled.FindStringSubmatch(input)
|
|
if matches == nil {
|
|
return false
|
|
}
|
|
|
|
// save all capture groups, first by index
|
|
for i, match := range matches {
|
|
key := fmt.Sprintf("%s.%d", mre.phPrefix, i)
|
|
repl.Set(key, match)
|
|
}
|
|
|
|
// then by name
|
|
for i, name := range mre.compiled.SubexpNames() {
|
|
if i != 0 && name != "" {
|
|
key := fmt.Sprintf("%s.%s", mre.phPrefix, name)
|
|
repl.Set(key, matches[i])
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
|
|
func (mre *MatchRegexp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
|
for d.Next() {
|
|
args := d.RemainingArgs()
|
|
switch len(args) {
|
|
case 1:
|
|
mre.Pattern = args[0]
|
|
case 2:
|
|
mre.Name = args[0]
|
|
mre.Pattern = args[1]
|
|
default:
|
|
return d.ArgErr()
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ResponseMatcher is a type which can determine if an
|
|
// HTTP response matches some criteria.
|
|
type ResponseMatcher struct {
|
|
// If set, one of these status codes would be required.
|
|
// A one-digit status can be used to represent all codes
|
|
// in that class (e.g. 3 for all 3xx codes).
|
|
StatusCode []int `json:"status_code,omitempty"`
|
|
|
|
// If set, each header specified must be one of the specified values.
|
|
Headers http.Header `json:"headers,omitempty"`
|
|
}
|
|
|
|
// Match returns true if the given statusCode and hdr match rm.
|
|
func (rm ResponseMatcher) Match(statusCode int, hdr http.Header) bool {
|
|
if !rm.matchStatusCode(statusCode) {
|
|
return false
|
|
}
|
|
return rm.matchHeaders(hdr)
|
|
}
|
|
|
|
func (rm ResponseMatcher) matchStatusCode(statusCode int) bool {
|
|
if rm.StatusCode == nil {
|
|
return true
|
|
}
|
|
for _, code := range rm.StatusCode {
|
|
if StatusCodeMatches(statusCode, code) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (rm ResponseMatcher) matchHeaders(hdr http.Header) bool {
|
|
for field, allowedFieldVals := range rm.Headers {
|
|
actualFieldVals, fieldExists := hdr[textproto.CanonicalMIMEHeaderKey(field)]
|
|
if allowedFieldVals != nil && len(allowedFieldVals) == 0 && fieldExists {
|
|
// a non-nil but empty list of allowed values means
|
|
// match if the header field exists at all
|
|
continue
|
|
}
|
|
var match bool
|
|
fieldVals:
|
|
for _, actualFieldVal := range actualFieldVals {
|
|
for _, allowedFieldVal := range allowedFieldVals {
|
|
if actualFieldVal == allowedFieldVal {
|
|
match = true
|
|
break fieldVals
|
|
}
|
|
}
|
|
}
|
|
if !match {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
var wordRE = regexp.MustCompile(`\w+`)
|
|
|
|
const regexpPlaceholderPrefix = "http.regexp"
|
|
|
|
// Interface guards
|
|
var (
|
|
_ RequestMatcher = (*MatchHost)(nil)
|
|
_ RequestMatcher = (*MatchPath)(nil)
|
|
_ RequestMatcher = (*MatchPathRE)(nil)
|
|
_ caddy.Provisioner = (*MatchPathRE)(nil)
|
|
_ RequestMatcher = (*MatchMethod)(nil)
|
|
_ RequestMatcher = (*MatchQuery)(nil)
|
|
_ RequestMatcher = (*MatchHeader)(nil)
|
|
_ RequestMatcher = (*MatchHeaderRE)(nil)
|
|
_ caddy.Provisioner = (*MatchHeaderRE)(nil)
|
|
_ RequestMatcher = (*MatchProtocol)(nil)
|
|
_ RequestMatcher = (*MatchRemoteIP)(nil)
|
|
_ caddy.Provisioner = (*MatchRemoteIP)(nil)
|
|
_ RequestMatcher = (*MatchNegate)(nil)
|
|
_ caddy.Provisioner = (*MatchNegate)(nil)
|
|
_ caddy.Provisioner = (*MatchRegexp)(nil)
|
|
|
|
_ caddyfile.Unmarshaler = (*MatchHost)(nil)
|
|
_ caddyfile.Unmarshaler = (*MatchPath)(nil)
|
|
_ caddyfile.Unmarshaler = (*MatchPathRE)(nil)
|
|
_ caddyfile.Unmarshaler = (*MatchMethod)(nil)
|
|
_ caddyfile.Unmarshaler = (*MatchQuery)(nil)
|
|
_ caddyfile.Unmarshaler = (*MatchHeader)(nil)
|
|
_ caddyfile.Unmarshaler = (*MatchHeaderRE)(nil)
|
|
_ caddyfile.Unmarshaler = (*MatchProtocol)(nil)
|
|
_ caddyfile.Unmarshaler = (*MatchRemoteIP)(nil)
|
|
|
|
_ json.Marshaler = (*MatchNegate)(nil)
|
|
_ json.Unmarshaler = (*MatchNegate)(nil)
|
|
)
|