@marcel91200 / caddy Mirror · read-only
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 11, 01:00 UTC

master
go 673 lines 18.4 KB
Raw
1 // Copyright 2015 Matthew Holt and The Caddy Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 package caddytls
16
17 import (
18 "context"
19 "crypto/tls"
20 "encoding/json"
21 "fmt"
22 "net"
23 "net/netip"
24 "regexp"
25 "slices"
26 "strconv"
27 "strings"
28
29 "github.com/caddyserver/certmagic"
30 "go.uber.org/zap"
31 "go.uber.org/zap/zapcore"
32 "golang.org/x/net/idna"
33
34 "github.com/caddyserver/caddy/v2"
35 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
36 "github.com/caddyserver/caddy/v2/internal"
37 )
38
39 func init() {
40 caddy.RegisterModule(MatchServerName{})
41 caddy.RegisterModule(MatchServerNameRE{})
42 caddy.RegisterModule(MatchRemoteIP{})
43 caddy.RegisterModule(MatchLocalIP{})
44 caddy.RegisterModule(MatchNot{})
45 }
46
47 // MatchServerName matches based on SNI. Names in
48 // this list may use left-most-label wildcards,
49 // similar to wildcard certificates.
50 type MatchServerName []string
51
52 // CaddyModule returns the Caddy module information.
53 func (MatchServerName) CaddyModule() caddy.ModuleInfo {
54 return caddy.ModuleInfo{
55 ID: "tls.handshake_match.sni",
56 New: func() caddy.Module { return new(MatchServerName) },
57 }
58 }
59
60 // Match matches hello based on SNI.
61 func (m MatchServerName) Match(hello *tls.ClientHelloInfo) bool {
62 var repl *caddy.Replacer
63 // caddytls.TestServerNameMatcher calls this function without any context
64 if ctx := hello.Context(); ctx != nil {
65 // In some situations the existing context may have no replacer
66 if replAny := ctx.Value(caddy.ReplacerCtxKey); replAny != nil {
67 repl = replAny.(*caddy.Replacer)
68 }
69 }
70
71 if repl == nil {
72 repl = caddy.NewReplacer()
73 }
74
75 serverName := asciiServerNameForMatch(hello.ServerName)
76 for _, name := range m {
77 rs := asciiServerNameForMatch(repl.ReplaceAll(name, ""))
78 if certmagic.MatchWildcard(serverName, rs) {
79 return true
80 }
81 }
82 return false
83 }
84
85 func asciiServerNameForMatch(name string) string {
86 if name == "" {
87 return name
88 }
89
90 // Fast path: if the name is pure ASCII, skip idna.ToASCII.
91 // SNI values on the wire are always ASCII (RFC 6066), and most
92 // config patterns are also ASCII. For pure ASCII input, idna.ToASCII
93 // only validates and lowercases, which is equivalent to our fallback.
94 if isPureASCII(name) {
95 return strings.ToLower(name)
96 }
97
98 // Config can use Unicode IDNs.
99 ascii, err := idna.ToASCII(name)
100 if err == nil {
101 return strings.ToLower(ascii)
102 }
103
104 if !strings.Contains(name, "*") {
105 return strings.ToLower(name)
106 }
107
108 labels := strings.Split(name, ".")
109 for i, label := range labels {
110 if label == "" || label == "*" {
111 continue
112 }
113 ascii, err := idna.ToASCII(label)
114 if err != nil {
115 return strings.ToLower(name)
116 }
117 labels[i] = strings.ToLower(ascii)
118 }
119 return strings.Join(labels, ".")
120 }
121
122 func isPureASCII(s string) bool {
123 for i := 0; i < len(s); i++ {
124 if s[i] >= 0x80 {
125 return false
126 }
127 }
128 return true
129 }
130
131 // UnmarshalCaddyfile sets up the MatchServerName from Caddyfile tokens. Syntax:
132 //
133 // sni <domains...>
134 func (m *MatchServerName) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
135 for d.Next() {
136 wrapper := d.Val()
137
138 // At least one same-line option must be provided
139 if d.CountRemainingArgs() == 0 {
140 return d.ArgErr()
141 }
142
143 *m = append(*m, d.RemainingArgs()...)
144
145 // No blocks are supported
146 if d.NextBlock(d.Nesting()) {
147 return d.Errf("malformed TLS handshake matcher '%s': blocks are not supported", wrapper)
148 }
149 }
150
151 return nil
152 }
153
154 // MatchRegexp is an embeddable type for matching
155 // using regular expressions. It adds placeholders
156 // to the request's replacer. In fact, it is a copy of
157 // caddyhttp.MatchRegexp with a local replacer prefix
158 // and placeholders support in a regular expression pattern.
159 type MatchRegexp struct {
160 // A unique name for this regular expression. Optional,
161 // but useful to prevent overwriting captures from other
162 // regexp matchers.
163 Name string `json:"name,omitempty"`
164
165 // The regular expression to evaluate, in RE2 syntax,
166 // which is the same general syntax used by Go, Perl,
167 // and Python. For details, see
168 // [Go's regexp package](https://golang.org/pkg/regexp/).
169 // Captures are accessible via placeholders. Unnamed
170 // capture groups are exposed as their numeric, 1-based
171 // index, while named capture groups are available by
172 // the capture group name.
173 Pattern string `json:"pattern"`
174
175 compiled *regexp.Regexp
176 }
177
178 // Provision compiles the regular expression which may include placeholders.
179 func (mre *MatchRegexp) Provision(caddy.Context) error {
180 repl := caddy.NewReplacer()
181 re, err := regexp.Compile(repl.ReplaceAll(mre.Pattern, ""))
182 if err != nil {
183 return fmt.Errorf("compiling matcher regexp %s: %v", mre.Pattern, err)
184 }
185 mre.compiled = re
186 return nil
187 }
188
189 // Validate ensures mre is set up correctly.
190 func (mre *MatchRegexp) Validate() error {
191 if mre.Name != "" && !wordRE.MatchString(mre.Name) {
192 return fmt.Errorf("invalid regexp name (must contain only word characters): %s", mre.Name)
193 }
194 return nil
195 }
196
197 // Match returns true if input matches the compiled regular
198 // expression in m. It sets values on the replacer repl
199 // associated with capture groups, using the given scope
200 // (namespace).
201 func (mre *MatchRegexp) Match(input string, repl *caddy.Replacer) bool {
202 matches := mre.compiled.FindStringSubmatch(input)
203 if matches == nil {
204 return false
205 }
206
207 // save all capture groups, first by index
208 for i, match := range matches {
209 keySuffix := "." + strconv.Itoa(i)
210 if mre.Name != "" {
211 repl.Set(regexpPlaceholderPrefix+"."+mre.Name+keySuffix, match)
212 }
213 repl.Set(regexpPlaceholderPrefix+keySuffix, match)
214 }
215
216 // then by name
217 for i, name := range mre.compiled.SubexpNames() {
218 // skip the first element (the full match), and empty names
219 if i == 0 || name == "" {
220 continue
221 }
222
223 keySuffix := "." + name
224 if mre.Name != "" {
225 repl.Set(regexpPlaceholderPrefix+"."+mre.Name+keySuffix, matches[i])
226 }
227 repl.Set(regexpPlaceholderPrefix+keySuffix, matches[i])
228 }
229
230 return true
231 }
232
233 // UnmarshalCaddyfile implements caddyfile.Unmarshaler.
234 func (mre *MatchRegexp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
235 // iterate to merge multiple matchers into one
236 for d.Next() {
237 // If this is the second iteration of the loop
238 // then there's more than one *_regexp matcher,
239 // and we would end up overwriting the old one
240 if mre.Pattern != "" {
241 return d.Err("regular expression can only be used once per named matcher")
242 }
243
244 args := d.RemainingArgs()
245 switch len(args) {
246 case 1:
247 mre.Pattern = args[0]
248 case 2:
249 mre.Name = args[0]
250 mre.Pattern = args[1]
251 default:
252 return d.ArgErr()
253 }
254
255 // Default to the named matcher's name, if no regexp name is provided.
256 // Note: it requires d.SetContext(caddyfile.MatcherNameCtxKey, value)
257 // called before this unmarshalling, otherwise it wouldn't work.
258 if mre.Name == "" {
259 mre.Name = d.GetContextString(caddyfile.MatcherNameCtxKey)
260 }
261
262 if d.NextBlock(0) {
263 return d.Err("malformed regexp matcher: blocks are not supported")
264 }
265 }
266 return nil
267 }
268
269 // MatchServerNameRE matches based on SNI using a regular expression.
270 type MatchServerNameRE struct{ MatchRegexp }
271
272 // CaddyModule returns the Caddy module information.
273 func (MatchServerNameRE) CaddyModule() caddy.ModuleInfo {
274 return caddy.ModuleInfo{
275 ID: "tls.handshake_match.sni_regexp",
276 New: func() caddy.Module { return new(MatchServerNameRE) },
277 }
278 }
279
280 // Match matches hello based on SNI using a regular expression.
281 func (m MatchServerNameRE) Match(hello *tls.ClientHelloInfo) bool {
282 // Note: caddytls.TestServerNameMatcher calls this function without any context
283 ctx := hello.Context()
284 if ctx == nil {
285 // layer4.Connection implements GetContext() to pass its context here,
286 // since hello.Context() returns nil
287 if mayHaveContext, ok := hello.Conn.(interface{ GetContext() context.Context }); ok {
288 ctx = mayHaveContext.GetContext()
289 }
290 }
291
292 var repl *caddy.Replacer
293 if ctx != nil {
294 // In some situations the existing context may have no replacer
295 if replAny := ctx.Value(caddy.ReplacerCtxKey); replAny != nil {
296 repl = replAny.(*caddy.Replacer)
297 }
298 }
299
300 if repl == nil {
301 repl = caddy.NewReplacer()
302 }
303
304 return m.MatchRegexp.Match(hello.ServerName, repl)
305 }
306
307 // MatchRemoteIP matches based on the remote IP of the
308 // connection. Specific IPs or CIDR ranges can be specified.
309 //
310 // Note that IPs can sometimes be spoofed, so do not rely
311 // on this as a replacement for actual authentication.
312 type MatchRemoteIP struct {
313 // The IPs or CIDR ranges to match.
314 Ranges []string `json:"ranges,omitempty"`
315
316 // The IPs or CIDR ranges to *NOT* match.
317 NotRanges []string `json:"not_ranges,omitempty"`
318
319 cidrs []netip.Prefix
320 notCidrs []netip.Prefix
321 logger *zap.Logger
322 }
323
324 // CaddyModule returns the Caddy module information.
325 func (MatchRemoteIP) CaddyModule() caddy.ModuleInfo {
326 return caddy.ModuleInfo{
327 ID: "tls.handshake_match.remote_ip",
328 New: func() caddy.Module { return new(MatchRemoteIP) },
329 }
330 }
331
332 // Provision parses m's IP ranges, either from IP or CIDR expressions.
333 func (m *MatchRemoteIP) Provision(ctx caddy.Context) error {
334 repl := caddy.NewReplacer()
335 m.logger = ctx.Logger()
336 for _, str := range m.Ranges {
337 rs := repl.ReplaceAll(str, "")
338 cidrs, err := m.parseIPRange(rs)
339 if err != nil {
340 return err
341 }
342 m.cidrs = append(m.cidrs, cidrs...)
343 }
344 for _, str := range m.NotRanges {
345 rs := repl.ReplaceAll(str, "")
346 cidrs, err := m.parseIPRange(rs)
347 if err != nil {
348 return err
349 }
350 m.notCidrs = append(m.notCidrs, cidrs...)
351 }
352 return nil
353 }
354
355 // Match matches hello based on the connection's remote IP.
356 func (m MatchRemoteIP) Match(hello *tls.ClientHelloInfo) bool {
357 remoteAddr := hello.Conn.RemoteAddr().String()
358 ipStr, _, err := net.SplitHostPort(remoteAddr)
359 if err != nil {
360 ipStr = remoteAddr // weird; maybe no port?
361 }
362 ipAddr, err := netip.ParseAddr(ipStr)
363 if err != nil {
364 if c := m.logger.Check(zapcore.ErrorLevel, "invalid client IP address"); c != nil {
365 c.Write(zap.String("ip", ipStr))
366 }
367 return false
368 }
369 return (len(m.cidrs) == 0 || m.matches(ipAddr, m.cidrs)) &&
370 (len(m.notCidrs) == 0 || !m.matches(ipAddr, m.notCidrs))
371 }
372
373 func (MatchRemoteIP) parseIPRange(str string) ([]netip.Prefix, error) {
374 var cidrs []netip.Prefix
375 if strings.Contains(str, "/") {
376 ipNet, err := netip.ParsePrefix(str)
377 if err != nil {
378 return nil, fmt.Errorf("parsing CIDR expression: %v", err)
379 }
380 cidrs = append(cidrs, ipNet)
381 } else {
382 ipAddr, err := netip.ParseAddr(str)
383 if err != nil {
384 return nil, fmt.Errorf("invalid IP address: '%s': %v", str, err)
385 }
386 ip := netip.PrefixFrom(ipAddr, ipAddr.BitLen())
387 cidrs = append(cidrs, ip)
388 }
389 return cidrs, nil
390 }
391
392 func (MatchRemoteIP) matches(ip netip.Addr, ranges []netip.Prefix) bool {
393 return slices.ContainsFunc(ranges, func(prefix netip.Prefix) bool {
394 return prefix.Contains(ip)
395 })
396 }
397
398 // UnmarshalCaddyfile sets up the MatchRemoteIP from Caddyfile tokens. Syntax:
399 //
400 // remote_ip <ranges...>
401 //
402 // Note: IPs and CIDRs prefixed with ! symbol are treated as not_ranges
403 func (m *MatchRemoteIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
404 for d.Next() {
405 wrapper := d.Val()
406
407 // At least one same-line option must be provided
408 if d.CountRemainingArgs() == 0 {
409 return d.ArgErr()
410 }
411
412 for d.NextArg() {
413 val := d.Val()
414 var exclamation bool
415 if len(val) > 1 && val[0] == '!' {
416 exclamation, val = true, val[1:]
417 }
418 ranges := []string{val}
419 if val == "private_ranges" {
420 ranges = internal.PrivateRangesCIDR()
421 }
422 if exclamation {
423 m.NotRanges = append(m.NotRanges, ranges...)
424 } else {
425 m.Ranges = append(m.Ranges, ranges...)
426 }
427 }
428
429 // No blocks are supported
430 if d.NextBlock(d.Nesting()) {
431 return d.Errf("malformed TLS handshake matcher '%s': blocks are not supported", wrapper)
432 }
433 }
434
435 return nil
436 }
437
438 // MatchLocalIP matches based on the IP address of the interface
439 // receiving the connection. Specific IPs or CIDR ranges can be specified.
440 type MatchLocalIP struct {
441 // The IPs or CIDR ranges to match.
442 Ranges []string `json:"ranges,omitempty"`
443
444 cidrs []netip.Prefix
445 logger *zap.Logger
446 }
447
448 // CaddyModule returns the Caddy module information.
449 func (MatchLocalIP) CaddyModule() caddy.ModuleInfo {
450 return caddy.ModuleInfo{
451 ID: "tls.handshake_match.local_ip",
452 New: func() caddy.Module { return new(MatchLocalIP) },
453 }
454 }
455
456 // Provision parses m's IP ranges, either from IP or CIDR expressions.
457 func (m *MatchLocalIP) Provision(ctx caddy.Context) error {
458 repl := caddy.NewReplacer()
459 m.logger = ctx.Logger()
460 for _, str := range m.Ranges {
461 rs := repl.ReplaceAll(str, "")
462 cidrs, err := m.parseIPRange(rs)
463 if err != nil {
464 return err
465 }
466 m.cidrs = append(m.cidrs, cidrs...)
467 }
468 return nil
469 }
470
471 // Match matches hello based on the connection's remote IP.
472 func (m MatchLocalIP) Match(hello *tls.ClientHelloInfo) bool {
473 localAddr := hello.Conn.LocalAddr().String()
474 ipStr, _, err := net.SplitHostPort(localAddr)
475 if err != nil {
476 ipStr = localAddr // weird; maybe no port?
477 }
478 ipAddr, err := netip.ParseAddr(ipStr)
479 if err != nil {
480 if c := m.logger.Check(zapcore.ErrorLevel, "invalid local IP address"); c != nil {
481 c.Write(zap.String("ip", ipStr))
482 }
483 return false
484 }
485 return (len(m.cidrs) == 0 || m.matches(ipAddr, m.cidrs))
486 }
487
488 func (MatchLocalIP) parseIPRange(str string) ([]netip.Prefix, error) {
489 var cidrs []netip.Prefix
490 if strings.Contains(str, "/") {
491 ipNet, err := netip.ParsePrefix(str)
492 if err != nil {
493 return nil, fmt.Errorf("parsing CIDR expression: %v", err)
494 }
495 cidrs = append(cidrs, ipNet)
496 } else {
497 ipAddr, err := netip.ParseAddr(str)
498 if err != nil {
499 return nil, fmt.Errorf("invalid IP address: '%s': %v", str, err)
500 }
501 ip := netip.PrefixFrom(ipAddr, ipAddr.BitLen())
502 cidrs = append(cidrs, ip)
503 }
504 return cidrs, nil
505 }
506
507 func (MatchLocalIP) matches(ip netip.Addr, ranges []netip.Prefix) bool {
508 return slices.ContainsFunc(ranges, func(prefix netip.Prefix) bool {
509 return prefix.Contains(ip)
510 })
511 }
512
513 // UnmarshalCaddyfile sets up the MatchLocalIP from Caddyfile tokens. Syntax:
514 //
515 // local_ip <ranges...>
516 func (m *MatchLocalIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
517 for d.Next() {
518 wrapper := d.Val()
519
520 // At least one same-line option must be provided
521 if d.CountRemainingArgs() == 0 {
522 return d.ArgErr()
523 }
524
525 for d.NextArg() {
526 val := d.Val()
527 if val == "private_ranges" {
528 m.Ranges = append(m.Ranges, internal.PrivateRangesCIDR()...)
529 continue
530 }
531 m.Ranges = append(m.Ranges, val)
532 }
533
534 // No blocks are supported
535 if d.NextBlock(d.Nesting()) {
536 return d.Errf("malformed TLS handshake matcher '%s': blocks are not supported", wrapper)
537 }
538 }
539
540 return nil
541 }
542
543 // MatchNot matches a TLS handshake by negating the results of its
544 // matcher sets. A single "not" matcher takes one or more matcher sets.
545 // Each matcher set is OR'ed; in other words, if any matcher set returns
546 // true, the final result of the "not" matcher is false. Individual
547 // matchers within a set work the same (i.e. different matchers in
548 // the same set are AND'ed).
549 //
550 // NOTE: The generated docs which describe the structure of this
551 // module are wrong because of how this type unmarshals JSON in a
552 // custom way. The correct structure is:
553 //
554 // ```json
555 // [{}, {}]
556 // ```
557 //
558 // where each of the array elements is a matcher set, i.e. an
559 // object keyed by matcher name.
560 type MatchNot struct {
561 MatcherSetsRaw []caddy.ModuleMap `json:"-" caddy:"namespace=tls.handshake_match"`
562
563 matcherSets [][]ConnectionMatcher
564 }
565
566 // CaddyModule returns the Caddy module information.
567 func (MatchNot) CaddyModule() caddy.ModuleInfo {
568 return caddy.ModuleInfo{
569 ID: "tls.handshake_match.not",
570 New: func() caddy.Module { return new(MatchNot) },
571 }
572 }
573
574 // UnmarshalJSON satisfies json.Unmarshaler. It puts the JSON
575 // bytes directly into m's MatcherSetsRaw field.
576 func (m *MatchNot) UnmarshalJSON(data []byte) error {
577 return json.Unmarshal(data, &m.MatcherSetsRaw)
578 }
579
580 // MarshalJSON satisfies json.Marshaler by marshaling
581 // m's raw matcher sets.
582 func (m MatchNot) MarshalJSON() ([]byte, error) {
583 return json.Marshal(m.MatcherSetsRaw)
584 }
585
586 // Provision loads the matcher modules to be negated.
587 func (m *MatchNot) Provision(ctx caddy.Context) error {
588 matcherSets, err := ctx.LoadModule(m, "MatcherSetsRaw")
589 if err != nil {
590 return fmt.Errorf("loading matcher sets: %v", err)
591 }
592 for _, modMap := range matcherSets.([]map[string]any) {
593 var ms []ConnectionMatcher
594 for _, modIface := range modMap {
595 cm, ok := modIface.(ConnectionMatcher)
596 if !ok {
597 return fmt.Errorf("module is not a TLS handshake matcher: %T", modIface)
598 }
599 ms = append(ms, cm)
600 }
601 m.matcherSets = append(m.matcherSets, ms)
602 }
603 return nil
604 }
605
606 // Match returns true if hello does not match any of m's
607 // matcher sets.
608 func (m MatchNot) Match(hello *tls.ClientHelloInfo) bool {
609 setLoop:
610 for _, ms := range m.matcherSets {
611 for _, matcher := range ms {
612 if !matcher.Match(hello) {
613 continue setLoop
614 }
615 }
616 return false
617 }
618 return true
619 }
620
621 // UnmarshalCaddyfile sets up the MatchNot from Caddyfile tokens. Syntax:
622 //
623 // not <matcher> [<args...>]
624 // not {
625 // <matcher> [<args...>]
626 // }
627 func (m *MatchNot) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
628 // iterate to merge multiple matchers into one
629 for d.Next() {
630 wrapper := d.Val()
631
632 matcherSet, err := ParseCaddyfileNestedMatcherSet(d)
633 if err != nil {
634 return err
635 }
636
637 // At least one matcher must be provided
638 if len(matcherSet) == 0 {
639 return d.Errf("malformed TLS handshake matcher '%s': no matchers to negate", wrapper)
640 }
641
642 m.MatcherSetsRaw = append(m.MatcherSetsRaw, matcherSet)
643 }
644
645 return nil
646 }
647
648 // Interface guards
649 var (
650 _ ConnectionMatcher = (*MatchLocalIP)(nil)
651 _ ConnectionMatcher = (*MatchNot)(nil)
652 _ ConnectionMatcher = (*MatchRemoteIP)(nil)
653 _ ConnectionMatcher = (*MatchServerName)(nil)
654 _ ConnectionMatcher = (*MatchServerNameRE)(nil)
655
656 _ caddy.Provisioner = (*MatchLocalIP)(nil)
657 _ caddy.Provisioner = (*MatchNot)(nil)
658 _ caddy.Provisioner = (*MatchRemoteIP)(nil)
659 _ caddy.Provisioner = (*MatchServerNameRE)(nil)
660
661 _ caddyfile.Unmarshaler = (*MatchLocalIP)(nil)
662 _ caddyfile.Unmarshaler = (*MatchNot)(nil)
663 _ caddyfile.Unmarshaler = (*MatchRemoteIP)(nil)
664 _ caddyfile.Unmarshaler = (*MatchServerName)(nil)
665 _ caddyfile.Unmarshaler = (*MatchServerNameRE)(nil)
666
667 _ json.Marshaler = (*MatchNot)(nil)
668 _ json.Unmarshaler = (*MatchNot)(nil)
669 )
670
671 var wordRE = regexp.MustCompile(`\w+`)
672
673 const regexpPlaceholderPrefix = "tls.regexp"