mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-26 21:53:48 +03:00
caddytls: Add sni_regexp matcher (#6569)
This commit is contained in:
parent
91e62db666
commit
2d12fb7ac6
2 changed files with 199 additions and 2 deletions
|
@ -19,6 +19,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/caddyserver/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
|
@ -31,6 +33,7 @@ import (
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
caddy.RegisterModule(MatchServerName{})
|
caddy.RegisterModule(MatchServerName{})
|
||||||
|
caddy.RegisterModule(MatchServerNameRE{})
|
||||||
caddy.RegisterModule(MatchRemoteIP{})
|
caddy.RegisterModule(MatchRemoteIP{})
|
||||||
caddy.RegisterModule(MatchLocalIP{})
|
caddy.RegisterModule(MatchLocalIP{})
|
||||||
}
|
}
|
||||||
|
@ -91,6 +94,146 @@ func (m *MatchServerName) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MatchRegexp is an embeddable type for matching
|
||||||
|
// using regular expressions. It adds placeholders
|
||||||
|
// to the request's replacer. In fact, it is a copy of
|
||||||
|
// caddyhttp.MatchRegexp with a local replacer prefix
|
||||||
|
// and placeholders support in a regular expression pattern.
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provision compiles the regular expression which may include placeholders.
|
||||||
|
func (mre *MatchRegexp) Provision(caddy.Context) error {
|
||||||
|
repl := caddy.NewReplacer()
|
||||||
|
re, err := regexp.Compile(repl.ReplaceAll(mre.Pattern, ""))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("compiling matcher regexp %s: %v", mre.Pattern, err)
|
||||||
|
}
|
||||||
|
mre.compiled = re
|
||||||
|
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 m. 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 {
|
||||||
|
keySuffix := "." + strconv.Itoa(i)
|
||||||
|
if mre.Name != "" {
|
||||||
|
repl.Set(regexpPlaceholderPrefix+"."+mre.Name+keySuffix, match)
|
||||||
|
}
|
||||||
|
repl.Set(regexpPlaceholderPrefix+keySuffix, match)
|
||||||
|
}
|
||||||
|
|
||||||
|
// then by name
|
||||||
|
for i, name := range mre.compiled.SubexpNames() {
|
||||||
|
// skip the first element (the full match), and empty names
|
||||||
|
if i == 0 || name == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
keySuffix := "." + name
|
||||||
|
if mre.Name != "" {
|
||||||
|
repl.Set(regexpPlaceholderPrefix+"."+mre.Name+keySuffix, matches[i])
|
||||||
|
}
|
||||||
|
repl.Set(regexpPlaceholderPrefix+keySuffix, matches[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
|
||||||
|
func (mre *MatchRegexp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
|
// iterate to merge multiple matchers into one
|
||||||
|
for d.Next() {
|
||||||
|
// If this is the second iteration of the loop
|
||||||
|
// then there's more than one *_regexp matcher,
|
||||||
|
// and we would end up overwriting the old one
|
||||||
|
if mre.Pattern != "" {
|
||||||
|
return d.Err("regular expression can only be used once per named matcher")
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to the named matcher's name, if no regexp name is provided.
|
||||||
|
// Note: it requires d.SetContext(caddyfile.MatcherNameCtxKey, value)
|
||||||
|
// called before this unmarshalling, otherwise it wouldn't work.
|
||||||
|
if mre.Name == "" {
|
||||||
|
mre.Name = d.GetContextString(caddyfile.MatcherNameCtxKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.NextBlock(0) {
|
||||||
|
return d.Err("malformed regexp matcher: blocks are not supported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchServerNameRE matches based on SNI using a regular expression.
|
||||||
|
type MatchServerNameRE struct{ MatchRegexp }
|
||||||
|
|
||||||
|
// CaddyModule returns the Caddy module information.
|
||||||
|
func (MatchServerNameRE) CaddyModule() caddy.ModuleInfo {
|
||||||
|
return caddy.ModuleInfo{
|
||||||
|
ID: "tls.handshake_match.sni_regexp",
|
||||||
|
New: func() caddy.Module { return new(MatchServerNameRE) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match matches hello based on SNI using a regular expression.
|
||||||
|
func (m MatchServerNameRE) Match(hello *tls.ClientHelloInfo) bool {
|
||||||
|
repl := caddy.NewReplacer()
|
||||||
|
// caddytls.TestServerNameMatcher calls this function without any context
|
||||||
|
if ctx := hello.Context(); ctx != nil {
|
||||||
|
// In some situations the existing context may have no replacer
|
||||||
|
if replAny := ctx.Value(caddy.ReplacerCtxKey); replAny != nil {
|
||||||
|
repl = replAny.(*caddy.Replacer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.MatchRegexp.Match(hello.ServerName, repl)
|
||||||
|
}
|
||||||
|
|
||||||
// MatchRemoteIP matches based on the remote IP of the
|
// MatchRemoteIP matches based on the remote IP of the
|
||||||
// connection. Specific IPs or CIDR ranges can be specified.
|
// connection. Specific IPs or CIDR ranges can be specified.
|
||||||
//
|
//
|
||||||
|
@ -331,13 +474,21 @@ func (m *MatchLocalIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
|
|
||||||
// Interface guards
|
// Interface guards
|
||||||
var (
|
var (
|
||||||
_ ConnectionMatcher = (*MatchServerName)(nil)
|
_ ConnectionMatcher = (*MatchLocalIP)(nil)
|
||||||
_ ConnectionMatcher = (*MatchRemoteIP)(nil)
|
_ ConnectionMatcher = (*MatchRemoteIP)(nil)
|
||||||
|
_ ConnectionMatcher = (*MatchServerName)(nil)
|
||||||
|
_ ConnectionMatcher = (*MatchServerNameRE)(nil)
|
||||||
|
|
||||||
_ caddy.Provisioner = (*MatchLocalIP)(nil)
|
_ caddy.Provisioner = (*MatchLocalIP)(nil)
|
||||||
_ ConnectionMatcher = (*MatchLocalIP)(nil)
|
_ caddy.Provisioner = (*MatchRemoteIP)(nil)
|
||||||
|
_ caddy.Provisioner = (*MatchServerNameRE)(nil)
|
||||||
|
|
||||||
_ caddyfile.Unmarshaler = (*MatchLocalIP)(nil)
|
_ caddyfile.Unmarshaler = (*MatchLocalIP)(nil)
|
||||||
_ caddyfile.Unmarshaler = (*MatchRemoteIP)(nil)
|
_ caddyfile.Unmarshaler = (*MatchRemoteIP)(nil)
|
||||||
_ caddyfile.Unmarshaler = (*MatchServerName)(nil)
|
_ caddyfile.Unmarshaler = (*MatchServerName)(nil)
|
||||||
|
_ caddyfile.Unmarshaler = (*MatchServerNameRE)(nil)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var wordRE = regexp.MustCompile(`\w+`)
|
||||||
|
|
||||||
|
const regexpPlaceholderPrefix = "tls.regexp"
|
||||||
|
|
|
@ -89,6 +89,52 @@ func TestServerNameMatcher(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServerNameREMatcher(t *testing.T) {
|
||||||
|
for i, tc := range []struct {
|
||||||
|
pattern string
|
||||||
|
input string
|
||||||
|
expect bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
pattern: "^example\\.(com|net)$",
|
||||||
|
input: "example.com",
|
||||||
|
expect: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: "^example\\.(com|net)$",
|
||||||
|
input: "foo.com",
|
||||||
|
expect: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: "^example\\.(com|net)$",
|
||||||
|
input: "",
|
||||||
|
expect: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: "",
|
||||||
|
input: "",
|
||||||
|
expect: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: "^example\\.(com|net)$",
|
||||||
|
input: "foo.example.com",
|
||||||
|
expect: false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
chi := &tls.ClientHelloInfo{ServerName: tc.input}
|
||||||
|
mre := MatchServerNameRE{MatchRegexp{Pattern: tc.pattern}}
|
||||||
|
ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()})
|
||||||
|
if mre.Provision(ctx) != nil {
|
||||||
|
t.Errorf("Test %d: Failed to provision a regexp matcher (pattern=%v)", i, tc.pattern)
|
||||||
|
}
|
||||||
|
actual := mre.Match(chi)
|
||||||
|
if actual != tc.expect {
|
||||||
|
t.Errorf("Test %d: Expected %t but got %t (input=%s match=%v)",
|
||||||
|
i, tc.expect, actual, tc.input, tc.pattern)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRemoteIPMatcher(t *testing.T) {
|
func TestRemoteIPMatcher(t *testing.T) {
|
||||||
ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
|
ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
Loading…
Reference in a new issue