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