@marcel91200 / caddy Mirror · read-only / commits / 2d12fb7a
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 12, 08:15 UTC

caddytls: Add sni_regexp matcher (#6569)

vnxme committed Sep 12, 2024 at 05:51 UTC 2d12fb7ac6c7dfcbb8abeafbfb64af5ad1175bb3
2 files changed +199 -2
modules/caddytls/matchers.go
+153 -2
@@ -19,6 +19,8 @@ import (
19 "fmt"
20 "net"
21 "net/netip"
22 + "regexp"
23 + "strconv"
24 "strings"
25
26 "github.com/caddyserver/certmagic"
@@ -31,6 +33,7 @@ import (
33
34 func init() {
35 caddy.RegisterModule(MatchServerName{})
36 + caddy.RegisterModule(MatchServerNameRE{})
37 caddy.RegisterModule(MatchRemoteIP{})
38 caddy.RegisterModule(MatchLocalIP{})
39 }
@@ -91,6 +94,146 @@ func (m *MatchServerName) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
94 return nil
95 }
96
97 +// MatchRegexp is an embeddable type for matching
98 +// using regular expressions. It adds placeholders
99 +// to the request's replacer. In fact, it is a copy of
100 +// caddyhttp.MatchRegexp with a local replacer prefix
101 +// and placeholders support in a regular expression pattern.
102 +type MatchRegexp struct {
103 + // A unique name for this regular expression. Optional,
104 + // but useful to prevent overwriting captures from other
105 + // regexp matchers.
106 + Name string `json:"name,omitempty"`
107 +
108 + // The regular expression to evaluate, in RE2 syntax,
109 + // which is the same general syntax used by Go, Perl,
110 + // and Python. For details, see
111 + // [Go's regexp package](https://golang.org/pkg/regexp/).
112 + // Captures are accessible via placeholders. Unnamed
113 + // capture groups are exposed as their numeric, 1-based
114 + // index, while named capture groups are available by
115 + // the capture group name.
116 + Pattern string `json:"pattern"`
117 +
118 + compiled *regexp.Regexp
119 +}
120 +
121 +// Provision compiles the regular expression which may include placeholders.
122 +func (mre *MatchRegexp) Provision(caddy.Context) error {
123 + repl := caddy.NewReplacer()
124 + re, err := regexp.Compile(repl.ReplaceAll(mre.Pattern, ""))
125 + if err != nil {
126 + return fmt.Errorf("compiling matcher regexp %s: %v", mre.Pattern, err)
127 + }
128 + mre.compiled = re
129 + return nil
130 +}
131 +
132 +// Validate ensures mre is set up correctly.
133 +func (mre *MatchRegexp) Validate() error {
134 + if mre.Name != "" && !wordRE.MatchString(mre.Name) {
135 + return fmt.Errorf("invalid regexp name (must contain only word characters): %s", mre.Name)
136 + }
137 + return nil
138 +}
139 +
140 +// Match returns true if input matches the compiled regular
141 +// expression in m. It sets values on the replacer repl
142 +// associated with capture groups, using the given scope
143 +// (namespace).
144 +func (mre *MatchRegexp) Match(input string, repl *caddy.Replacer) bool {
145 + matches := mre.compiled.FindStringSubmatch(input)
146 + if matches == nil {
147 + return false
148 + }
149 +
150 + // save all capture groups, first by index
151 + for i, match := range matches {
152 + keySuffix := "." + strconv.Itoa(i)
153 + if mre.Name != "" {
154 + repl.Set(regexpPlaceholderPrefix+"."+mre.Name+keySuffix, match)
155 + }
156 + repl.Set(regexpPlaceholderPrefix+keySuffix, match)
157 + }
158 +
159 + // then by name
160 + for i, name := range mre.compiled.SubexpNames() {
161 + // skip the first element (the full match), and empty names
162 + if i == 0 || name == "" {
163 + continue
164 + }
165 +
166 + keySuffix := "." + name
167 + if mre.Name != "" {
168 + repl.Set(regexpPlaceholderPrefix+"."+mre.Name+keySuffix, matches[i])
169 + }
170 + repl.Set(regexpPlaceholderPrefix+keySuffix, matches[i])
171 + }
172 +
173 + return true
174 +}
175 +
176 +// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
177 +func (mre *MatchRegexp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
178 + // iterate to merge multiple matchers into one
179 + for d.Next() {
180 + // If this is the second iteration of the loop
181 + // then there's more than one *_regexp matcher,
182 + // and we would end up overwriting the old one
183 + if mre.Pattern != "" {
184 + return d.Err("regular expression can only be used once per named matcher")
185 + }
186 +
187 + args := d.RemainingArgs()
188 + switch len(args) {
189 + case 1:
190 + mre.Pattern = args[0]
191 + case 2:
192 + mre.Name = args[0]
193 + mre.Pattern = args[1]
194 + default:
195 + return d.ArgErr()
196 + }
197 +
198 + // Default to the named matcher's name, if no regexp name is provided.
199 + // Note: it requires d.SetContext(caddyfile.MatcherNameCtxKey, value)
200 + // called before this unmarshalling, otherwise it wouldn't work.
201 + if mre.Name == "" {
202 + mre.Name = d.GetContextString(caddyfile.MatcherNameCtxKey)
203 + }
204 +
205 + if d.NextBlock(0) {
206 + return d.Err("malformed regexp matcher: blocks are not supported")
207 + }
208 + }
209 + return nil
210 +}
211 +
212 +// MatchServerNameRE matches based on SNI using a regular expression.
213 +type MatchServerNameRE struct{ MatchRegexp }
214 +
215 +// CaddyModule returns the Caddy module information.
216 +func (MatchServerNameRE) CaddyModule() caddy.ModuleInfo {
217 + return caddy.ModuleInfo{
218 + ID: "tls.handshake_match.sni_regexp",
219 + New: func() caddy.Module { return new(MatchServerNameRE) },
220 + }
221 +}
222 +
223 +// Match matches hello based on SNI using a regular expression.
224 +func (m MatchServerNameRE) Match(hello *tls.ClientHelloInfo) bool {
225 + repl := caddy.NewReplacer()
226 + // caddytls.TestServerNameMatcher calls this function without any context
227 + if ctx := hello.Context(); ctx != nil {
228 + // In some situations the existing context may have no replacer
229 + if replAny := ctx.Value(caddy.ReplacerCtxKey); replAny != nil {
230 + repl = replAny.(*caddy.Replacer)
231 + }
232 + }
233 +
234 + return m.MatchRegexp.Match(hello.ServerName, repl)
235 +}
236 +
237 // MatchRemoteIP matches based on the remote IP of the
238 // connection. Specific IPs or CIDR ranges can be specified.
239 //
@@ -331,13 +474,21 @@ func (m *MatchLocalIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
474
475 // Interface guards
476 var (
334 - _ ConnectionMatcher = (*MatchServerName)(nil)
477 + _ ConnectionMatcher = (*MatchLocalIP)(nil)
478 _ ConnectionMatcher = (*MatchRemoteIP)(nil)
479 + _ ConnectionMatcher = (*MatchServerName)(nil)
480 + _ ConnectionMatcher = (*MatchServerNameRE)(nil)
481
482 _ caddy.Provisioner = (*MatchLocalIP)(nil)
338 - _ ConnectionMatcher = (*MatchLocalIP)(nil)
483 + _ caddy.Provisioner = (*MatchRemoteIP)(nil)
484 + _ caddy.Provisioner = (*MatchServerNameRE)(nil)
485
486 _ caddyfile.Unmarshaler = (*MatchLocalIP)(nil)
487 _ caddyfile.Unmarshaler = (*MatchRemoteIP)(nil)
488 _ caddyfile.Unmarshaler = (*MatchServerName)(nil)
489 + _ caddyfile.Unmarshaler = (*MatchServerNameRE)(nil)
490 )
491 +
492 +var wordRE = regexp.MustCompile(`\w+`)
493 +
494 +const regexpPlaceholderPrefix = "tls.regexp"
modules/caddytls/matchers_test.go
+46
@@ -89,6 +89,52 @@ func TestServerNameMatcher(t *testing.T) {
89 }
90 }
91
92 +func TestServerNameREMatcher(t *testing.T) {
93 + for i, tc := range []struct {
94 + pattern string
95 + input string
96 + expect bool
97 + }{
98 + {
99 + pattern: "^example\\.(com|net)$",
100 + input: "example.com",
101 + expect: true,
102 + },
103 + {
104 + pattern: "^example\\.(com|net)$",
105 + input: "foo.com",
106 + expect: false,
107 + },
108 + {
109 + pattern: "^example\\.(com|net)$",
110 + input: "",
111 + expect: false,
112 + },
113 + {
114 + pattern: "",
115 + input: "",
116 + expect: true,
117 + },
118 + {
119 + pattern: "^example\\.(com|net)$",
120 + input: "foo.example.com",
121 + expect: false,
122 + },
123 + } {
124 + chi := &tls.ClientHelloInfo{ServerName: tc.input}
125 + mre := MatchServerNameRE{MatchRegexp{Pattern: tc.pattern}}
126 + ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()})
127 + if mre.Provision(ctx) != nil {
128 + t.Errorf("Test %d: Failed to provision a regexp matcher (pattern=%v)", i, tc.pattern)
129 + }
130 + actual := mre.Match(chi)
131 + if actual != tc.expect {
132 + t.Errorf("Test %d: Expected %t but got %t (input=%s match=%v)",
133 + i, tc.expect, actual, tc.input, tc.pattern)
134 + }
135 + }
136 +}
137 +
138 func TestRemoteIPMatcher(t *testing.T) {
139 ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
140 defer cancel()