@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 1,944 lines 66 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 httpcaddyfile
16
17 import (
18 "cmp"
19 "encoding/json"
20 "fmt"
21 "maps"
22 "net"
23 "reflect"
24 "slices"
25 "sort"
26 "strconv"
27 "strings"
28
29 "github.com/caddyserver/certmagic"
30 "go.uber.org/zap"
31
32 "github.com/caddyserver/caddy/v2"
33 "github.com/caddyserver/caddy/v2/caddyconfig"
34 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
35 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
36 "github.com/caddyserver/caddy/v2/modules/caddypki"
37 "github.com/caddyserver/caddy/v2/modules/caddytls"
38 )
39
40 func init() {
41 caddyconfig.RegisterAdapter("caddyfile", caddyfile.Adapter{ServerType: ServerType{}})
42 }
43
44 // App represents the configuration for a non-standard
45 // Caddy app module (e.g. third-party plugin) which was
46 // parsed from a global options block.
47 type App struct {
48 // The JSON key for the app being configured
49 Name string
50
51 // The raw app config as JSON
52 Value json.RawMessage
53 }
54
55 // ServerType can set up a config from an HTTP Caddyfile.
56 type ServerType struct{}
57
58 // Setup makes a config from the tokens.
59 func (st ServerType) Setup(
60 inputServerBlocks []caddyfile.ServerBlock,
61 options map[string]any,
62 ) (*caddy.Config, []caddyconfig.Warning, error) {
63 var warnings []caddyconfig.Warning
64 gc := counter{new(int)}
65 state := make(map[string]any)
66
67 // adaptation fills the options in as it goes, so work on our own copy;
68 // the caller may reuse or share the map they gave us
69 options = maps.Clone(options)
70 if options == nil {
71 options = make(map[string]any)
72 }
73
74 // an order accumulated by a previous adaptation must not carry into
75 // this one, so drop it and let the first "order" option start over
76 // from the order the plugins registered
77 delete(options, "order")
78
79 // load all the server blocks and associate them with a "pile" of config values
80 originalServerBlocks := make([]serverBlock, 0, len(inputServerBlocks))
81 for _, sblock := range inputServerBlocks {
82 for j, k := range sblock.Keys {
83 if j == 0 && strings.HasPrefix(k.Text, "@") {
84 return nil, warnings, fmt.Errorf("%s:%d: cannot define a matcher outside of a site block: '%s'", k.File, k.Line, k.Text)
85 }
86 if _, ok := registeredDirectives[k.Text]; ok {
87 return nil, warnings, fmt.Errorf("%s:%d: parsed '%s' as a site address, but it is a known directive; directives must appear in a site block", k.File, k.Line, k.Text)
88 }
89 }
90 originalServerBlocks = append(originalServerBlocks, serverBlock{
91 block: sblock,
92 pile: make(map[string][]ConfigValue),
93 })
94 }
95
96 // apply any global options
97 var err error
98 originalServerBlocks, err = st.evaluateGlobalOptionsBlock(originalServerBlocks, options)
99 if err != nil {
100 return nil, warnings, err
101 }
102
103 // this will replace both static and user-defined placeholder shorthands
104 // with actual identifiers used by Caddy
105 replacer := NewShorthandReplacer()
106
107 originalServerBlocks, err = st.extractNamedRoutes(originalServerBlocks, options, &warnings, replacer)
108 if err != nil {
109 return nil, warnings, err
110 }
111
112 for _, sb := range originalServerBlocks {
113 for i := range sb.block.Segments {
114 replacer.ApplyToSegment(&sb.block.Segments[i])
115 }
116
117 if len(sb.block.Keys) == 0 {
118 return nil, warnings, fmt.Errorf("server block without any key is global configuration, and if used, it must be first")
119 }
120
121 // extract matcher definitions
122 matcherDefs := make(map[string]caddy.ModuleMap)
123 for _, segment := range sb.block.Segments {
124 if dir := segment.Directive(); strings.HasPrefix(dir, matcherPrefix) {
125 d := caddyfile.NewDispenser(segment)
126 err := parseMatcherDefinitions(d, matcherDefs)
127 if err != nil {
128 return nil, warnings, err
129 }
130 }
131 }
132
133 // evaluate each directive ("segment") in this block
134 for _, segment := range sb.block.Segments {
135 dir := segment.Directive()
136
137 if strings.HasPrefix(dir, matcherPrefix) {
138 // matcher definitions were pre-processed
139 continue
140 }
141
142 dirFunc, ok := registeredDirectives[dir]
143 if !ok {
144 tkn := segment[0]
145 message := "%s:%d: unrecognized directive: %s"
146 if !sb.block.HasBraces {
147 message += "\nDid you mean to define a second site? If so, you must use curly braces around each site to separate their configurations."
148 }
149 return nil, warnings, fmt.Errorf(message, tkn.File, tkn.Line, dir)
150 }
151
152 h := Helper{
153 Dispenser: caddyfile.NewDispenser(segment),
154 options: options,
155 warnings: &warnings,
156 matcherDefs: matcherDefs,
157 parentBlock: sb.block,
158 groupCounter: gc,
159 State: state,
160 BlockState: state,
161 }
162
163 results, err := dirFunc(h)
164 if err != nil {
165 return nil, warnings, fmt.Errorf("parsing caddyfile tokens for '%s': %v", dir, err)
166 }
167
168 dir = normalizeDirectiveName(dir)
169
170 for _, result := range results {
171 result.directive = dir
172 sb.pile[result.Class] = append(sb.pile[result.Class], result)
173 }
174
175 // specially handle named routes that were pulled out from
176 // the invoke directive, which could be nested anywhere within
177 // some subroutes in this directive; we add them to the pile
178 // for this server block
179 if state[namedRouteKey] != nil {
180 for name := range state[namedRouteKey].(map[string]struct{}) {
181 result := ConfigValue{Class: namedRouteKey, Value: name}
182 sb.pile[result.Class] = append(sb.pile[result.Class], result)
183 }
184 state[namedRouteKey] = nil
185 }
186 }
187 }
188
189 // map
190 sbmap, err := st.mapAddressToProtocolToServerBlocks(originalServerBlocks, options)
191 if err != nil {
192 return nil, warnings, err
193 }
194
195 // reduce
196 pairings := st.consolidateAddrMappings(sbmap)
197
198 // each pairing of listener addresses to list of server
199 // blocks is basically a server definition
200 servers, err := st.serversFromPairings(pairings, options, &warnings, gc)
201 if err != nil {
202 return nil, warnings, err
203 }
204
205 // hoist the metrics config from per-server to global
206 metrics, _ := options["metrics"].(*caddyhttp.Metrics)
207 for _, s := range servers {
208 if s.Metrics != nil {
209 metrics = cmp.Or(metrics, &caddyhttp.Metrics{})
210 metrics = &caddyhttp.Metrics{
211 PerHost: metrics.PerHost || s.Metrics.PerHost,
212 }
213 s.Metrics = nil // we don't need it anymore
214 }
215 }
216
217 // now that each server is configured, make the HTTP app
218 httpApp := caddyhttp.App{
219 HTTPPort: tryInt(options["http_port"], &warnings),
220 HTTPSPort: tryInt(options["https_port"], &warnings),
221 GracePeriod: tryDuration(options["grace_period"], &warnings),
222 ShutdownDelay: tryDuration(options["shutdown_delay"], &warnings),
223 Metrics: metrics,
224 Servers: servers,
225 }
226
227 // then make the TLS app
228 tlsApp, warnings, err := st.buildTLSApp(pairings, options, warnings)
229 if err != nil {
230 return nil, warnings, err
231 }
232
233 // then make the PKI app
234 pkiApp, warnings, err := st.buildPKIApp(pairings, options, warnings)
235 if err != nil {
236 return nil, warnings, err
237 }
238
239 // extract any custom logs, and enforce configured levels
240 var customLogs []namedCustomLog
241 var hasDefaultLog bool
242 addCustomLog := func(ncl namedCustomLog) {
243 if ncl.name == "" {
244 return
245 }
246 if ncl.name == caddy.DefaultLoggerName {
247 hasDefaultLog = true
248 }
249 if _, ok := options["debug"]; ok && ncl.log != nil && ncl.log.Level == "" {
250 ncl.log.Level = zap.DebugLevel.CapitalString()
251 }
252 customLogs = append(customLogs, ncl)
253 }
254
255 // Apply global log options, when set
256 if options["log"] != nil {
257 for _, logValue := range options["log"].([]ConfigValue) {
258 addCustomLog(logValue.Value.(namedCustomLog))
259 }
260 }
261
262 if !hasDefaultLog {
263 // if the default log was not customized, ensure we
264 // configure it with any applicable options
265 if _, ok := options["debug"]; ok {
266 customLogs = append(customLogs, namedCustomLog{
267 name: caddy.DefaultLoggerName,
268 log: &caddy.CustomLog{
269 BaseLog: caddy.BaseLog{Level: zap.DebugLevel.CapitalString()},
270 },
271 })
272 }
273 }
274
275 // Apply server-specific log options
276 for _, p := range pairings {
277 for _, sb := range p.serverBlocks {
278 for _, clVal := range sb.pile["custom_log"] {
279 addCustomLog(clVal.Value.(namedCustomLog))
280 }
281 }
282 }
283
284 // annnd the top-level config, then we're done!
285 cfg := &caddy.Config{AppsRaw: make(caddy.ModuleMap)}
286
287 // loop through the configured options, and if any of
288 // them are an httpcaddyfile App, then we insert them
289 // into the config as raw Caddy apps
290 for _, opt := range options {
291 if app, ok := opt.(App); ok {
292 cfg.AppsRaw[app.Name] = app.Value
293 }
294 }
295
296 // insert the standard Caddy apps into the config
297 if len(httpApp.Servers) > 0 {
298 cfg.AppsRaw["http"] = caddyconfig.JSON(httpApp, &warnings)
299 }
300 if !reflect.DeepEqual(tlsApp, &caddytls.TLS{CertificatesRaw: make(caddy.ModuleMap)}) {
301 cfg.AppsRaw["tls"] = caddyconfig.JSON(tlsApp, &warnings)
302 }
303 if !reflect.DeepEqual(pkiApp, &caddypki.PKI{CAs: make(map[string]*caddypki.CA)}) {
304 cfg.AppsRaw["pki"] = caddyconfig.JSON(pkiApp, &warnings)
305 }
306 if filesystems, ok := options["filesystem"].(caddy.Module); ok {
307 cfg.AppsRaw["caddy.filesystems"] = caddyconfig.JSON(
308 filesystems,
309 &warnings,
310 )
311 }
312
313 if storageCvtr, ok := options["storage"].(caddy.StorageConverter); ok {
314 cfg.StorageRaw = caddyconfig.JSONModuleObject(storageCvtr,
315 "module",
316 storageCvtr.(caddy.Module).CaddyModule().ID.Name(),
317 &warnings)
318 }
319 if adminConfig, ok := options["admin"].(*caddy.AdminConfig); ok && adminConfig != nil {
320 cfg.Admin = adminConfig
321 }
322 if pc, ok := options["persist_config"].(string); ok && pc == "off" {
323 if cfg.Admin == nil {
324 cfg.Admin = new(caddy.AdminConfig)
325 }
326 if cfg.Admin.Config == nil {
327 cfg.Admin.Config = new(caddy.ConfigSettings)
328 }
329 cfg.Admin.Config.Persist = new(bool)
330 }
331
332 if len(customLogs) > 0 {
333 if cfg.Logging == nil {
334 cfg.Logging = &caddy.Logging{
335 Logs: make(map[string]*caddy.CustomLog),
336 }
337 }
338
339 // Add the default log first if defined, so that it doesn't
340 // accidentally get re-created below due to the Exclude logic
341 for _, ncl := range customLogs {
342 if ncl.name == caddy.DefaultLoggerName && ncl.log != nil {
343 cfg.Logging.Logs[caddy.DefaultLoggerName] = ncl.log
344 break
345 }
346 }
347
348 // Add the rest of the custom logs
349 for _, ncl := range customLogs {
350 if ncl.log == nil || ncl.name == caddy.DefaultLoggerName {
351 continue
352 }
353 if ncl.name != "" {
354 cfg.Logging.Logs[ncl.name] = ncl.log
355 }
356 // most users seem to prefer not writing access logs
357 // to the default log when they are directed to a
358 // file or have any other special customization
359 if ncl.name != caddy.DefaultLoggerName && len(ncl.log.Include) > 0 {
360 defaultLog, ok := cfg.Logging.Logs[caddy.DefaultLoggerName]
361 if !ok {
362 defaultLog = new(caddy.CustomLog)
363 cfg.Logging.Logs[caddy.DefaultLoggerName] = defaultLog
364 }
365 defaultLog.Exclude = append(defaultLog.Exclude, ncl.log.Include...)
366
367 // avoid duplicates by sorting + compacting
368 sort.Strings(defaultLog.Exclude)
369 defaultLog.Exclude = slices.Compact(defaultLog.Exclude)
370 }
371 }
372 // we may have not actually added anything, so remove if empty
373 if len(cfg.Logging.Logs) == 0 {
374 cfg.Logging = nil
375 }
376 }
377
378 return cfg, warnings, nil
379 }
380
381 // evaluateGlobalOptionsBlock evaluates the global options block,
382 // which is expected to be the first server block if it has zero
383 // keys. It returns the updated list of server blocks with the
384 // global options block removed, and updates options accordingly.
385 func (ServerType) evaluateGlobalOptionsBlock(serverBlocks []serverBlock, options map[string]any) ([]serverBlock, error) {
386 if len(serverBlocks) == 0 || len(serverBlocks[0].block.Keys) > 0 {
387 return serverBlocks, nil
388 }
389
390 for _, segment := range serverBlocks[0].block.Segments {
391 opt := segment.Directive()
392 var val any
393 var err error
394 disp := caddyfile.NewDispenser(segment)
395
396 optFunc, ok := registeredGlobalOptions[opt]
397 if !ok {
398 tkn := segment[0]
399 return nil, fmt.Errorf("%s:%d: unrecognized global option: %s", tkn.File, tkn.Line, opt)
400 }
401
402 val, err = optFunc(disp, options[opt])
403 if err != nil {
404 return nil, fmt.Errorf("parsing caddyfile tokens for '%s': %v", opt, err)
405 }
406
407 // As a special case, fold multiple "servers" options together
408 // in an array instead of overwriting a possible existing value
409 if opt == "servers" {
410 existingOpts, ok := options[opt].([]serverOptions)
411 if !ok {
412 existingOpts = []serverOptions{}
413 }
414 serverOpts, ok := val.(serverOptions)
415 if !ok {
416 return nil, fmt.Errorf("unexpected type from 'servers' global options: %T", val)
417 }
418 options[opt] = append(existingOpts, serverOpts)
419 continue
420 }
421 // Additionally, fold multiple "log" options together into an
422 // array so that multiple loggers can be configured.
423 if opt == "log" {
424 existingOpts, ok := options[opt].([]ConfigValue)
425 if !ok {
426 existingOpts = []ConfigValue{}
427 }
428 logOpts, ok := val.([]ConfigValue)
429 if !ok {
430 return nil, fmt.Errorf("unexpected type from 'log' global options: %T", val)
431 }
432 options[opt] = append(existingOpts, logOpts...)
433 continue
434 }
435 // Also fold multiple "default_bind" options together into an
436 // array so that server blocks can have multiple binds by default.
437 if opt == "default_bind" {
438 existingOpts, ok := options[opt].([]ConfigValue)
439 if !ok {
440 existingOpts = []ConfigValue{}
441 }
442 defaultBindOpts, ok := val.([]ConfigValue)
443 if !ok {
444 return nil, fmt.Errorf("unexpected type from 'default_bind' global options: %T", val)
445 }
446 options[opt] = append(existingOpts, defaultBindOpts...)
447 continue
448 }
449
450 options[opt] = val
451 }
452
453 // If we got "servers" options, we'll sort them by their listener address
454 if serverOpts, ok := options["servers"].([]serverOptions); ok {
455 sort.Slice(serverOpts, func(i, j int) bool {
456 return len(serverOpts[i].ListenerAddress) > len(serverOpts[j].ListenerAddress)
457 })
458
459 // Reject the config if there are duplicate listener address
460 seen := make(map[string]bool)
461 for _, entry := range serverOpts {
462 if _, alreadySeen := seen[entry.ListenerAddress]; alreadySeen {
463 return nil, fmt.Errorf("cannot have 'servers' global options with duplicate listener addresses: %s", entry.ListenerAddress)
464 }
465 seen[entry.ListenerAddress] = true
466 }
467 }
468
469 return serverBlocks[1:], nil
470 }
471
472 // extractNamedRoutes pulls out any named route server blocks
473 // so they don't get parsed as sites, and stores them in options
474 // for later.
475 func (ServerType) extractNamedRoutes(
476 serverBlocks []serverBlock,
477 options map[string]any,
478 warnings *[]caddyconfig.Warning,
479 replacer ShorthandReplacer,
480 ) ([]serverBlock, error) {
481 namedRoutes := map[string]*caddyhttp.Route{}
482 namedRouteInvokes := map[string][]string{}
483
484 gc := counter{new(int)}
485 state := make(map[string]any)
486
487 // copy the server blocks so we can
488 // splice out the named route ones
489 filtered := append([]serverBlock{}, serverBlocks...)
490 index := -1
491
492 for _, sb := range serverBlocks {
493 index++
494 if !sb.block.IsNamedRoute {
495 continue
496 }
497
498 // splice out this block, because we know it's not a real server
499 filtered = append(filtered[:index], filtered[index+1:]...)
500 index--
501
502 if len(sb.block.Segments) == 0 {
503 continue
504 }
505
506 wholeSegment := caddyfile.Segment{}
507 for i := range sb.block.Segments {
508 // replace user-defined placeholder shorthands in extracted named routes
509 replacer.ApplyToSegment(&sb.block.Segments[i])
510
511 // zip up all the segments since ParseSegmentAsSubroute
512 // was designed to take a directive+
513 wholeSegment = append(wholeSegment, sb.block.Segments[i]...)
514 }
515
516 h := Helper{
517 Dispenser: caddyfile.NewDispenser(wholeSegment),
518 options: options,
519 warnings: warnings,
520 matcherDefs: nil,
521 parentBlock: sb.block,
522 groupCounter: gc,
523 State: state,
524 BlockState: state,
525 }
526
527 handler, err := ParseSegmentAsSubroute(h)
528 if err != nil {
529 return nil, err
530 }
531 subroute := handler.(*caddyhttp.Subroute)
532 route := caddyhttp.Route{}
533
534 if len(subroute.Routes) == 1 && len(subroute.Routes[0].MatcherSetsRaw) == 0 {
535 // if there's only one route with no matcher, then we can simplify
536 route.HandlersRaw = append(route.HandlersRaw, subroute.Routes[0].HandlersRaw[0])
537 } else {
538 // otherwise we need the whole subroute
539 route.HandlersRaw = []json.RawMessage{caddyconfig.JSONModuleObject(handler, "handler", subroute.CaddyModule().ID.Name(), h.warnings)}
540 }
541
542 key := sb.block.GetKeysText()[0]
543 if _, exists := namedRoutes[key]; exists {
544 return nil, fmt.Errorf("cannot have duplicate named_routes: %s", key)
545 }
546 namedRoutes[key] = &route
547
548 // remember which named routes this named route invokes,
549 // so they can also be attached to any server that invokes
550 // this one
551 if state[namedRouteKey] != nil {
552 for name := range state[namedRouteKey].(map[string]struct{}) {
553 namedRouteInvokes[key] = append(namedRouteInvokes[key], name)
554 }
555 state[namedRouteKey] = nil
556 }
557 }
558 options["named_routes"] = namedRoutes
559 options["named_route_invokes"] = namedRouteInvokes
560
561 return filtered, nil
562 }
563
564 // serversFromPairings creates the servers for each pairing of addresses
565 // to server blocks. Each pairing is essentially a server definition.
566 func (st *ServerType) serversFromPairings(
567 pairings []sbAddrAssociation,
568 options map[string]any,
569 warnings *[]caddyconfig.Warning,
570 groupCounter counter,
571 ) (map[string]*caddyhttp.Server, error) {
572 servers := make(map[string]*caddyhttp.Server)
573 defaultSNI := tryString(options["default_sni"], warnings)
574 fallbackSNI := tryString(options["fallback_sni"], warnings)
575
576 httpPort := strconv.Itoa(caddyhttp.DefaultHTTPPort)
577 if hp, ok := options["http_port"].(int); ok {
578 httpPort = strconv.Itoa(hp)
579 }
580 httpsPort := strconv.Itoa(caddyhttp.DefaultHTTPSPort)
581 if hsp, ok := options["https_port"].(int); ok {
582 httpsPort = strconv.Itoa(hsp)
583 }
584 autoHTTPS := []string{}
585 if ah, ok := options["auto_https"].([]string); ok {
586 autoHTTPS = ah
587 }
588
589 for i, p := range pairings {
590 // detect ambiguous site definitions: server blocks which
591 // have the same host bound to the same interface (listener
592 // address), otherwise their routes will improperly be added
593 // to the same server (see issue #4635)
594 for j, sblock1 := range p.serverBlocks {
595 for _, key := range sblock1.block.GetKeysText() {
596 for k, sblock2 := range p.serverBlocks {
597 if k == j {
598 continue
599 }
600 if slices.Contains(sblock2.block.GetKeysText(), key) {
601 return nil, fmt.Errorf("ambiguous site definition: %s", key)
602 }
603 }
604 }
605 }
606
607 var (
608 addresses []string
609 protocols [][]string
610 )
611
612 for _, addressWithProtocols := range p.addressesWithProtocols {
613 addresses = append(addresses, addressWithProtocols.address)
614 protocols = append(protocols, addressWithProtocols.protocols)
615 }
616
617 srv := &caddyhttp.Server{
618 Listen: addresses,
619 ListenProtocols: protocols,
620 }
621
622 // remove srv.ListenProtocols[j] if it only contains the default protocols
623 for j, lnProtocols := range srv.ListenProtocols {
624 srv.ListenProtocols[j] = nil
625 for _, lnProtocol := range lnProtocols {
626 if lnProtocol != "" {
627 srv.ListenProtocols[j] = lnProtocols
628 break
629 }
630 }
631 }
632
633 // remove srv.ListenProtocols if it only contains the default protocols for all listen addresses
634 listenProtocols := srv.ListenProtocols
635 srv.ListenProtocols = nil
636 for _, lnProtocols := range listenProtocols {
637 if lnProtocols != nil {
638 srv.ListenProtocols = listenProtocols
639 break
640 }
641 }
642
643 // handle the auto_https global option
644 for _, val := range autoHTTPS {
645 switch val {
646 case "off":
647 if srv.AutoHTTPS == nil {
648 srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
649 }
650 srv.AutoHTTPS.Disabled = true
651
652 case "disable_redirects":
653 if srv.AutoHTTPS == nil {
654 srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
655 }
656 srv.AutoHTTPS.DisableRedir = true
657
658 case "disable_certs":
659 if srv.AutoHTTPS == nil {
660 srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
661 }
662 srv.AutoHTTPS.DisableCerts = true
663
664 case "ignore_loaded_certs":
665 if srv.AutoHTTPS == nil {
666 srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
667 }
668 srv.AutoHTTPS.IgnoreLoadedCerts = true
669 }
670 }
671
672 // Using paths in site addresses is deprecated
673 // See ParseAddress() where parsing should later reject paths
674 // See https://github.com/caddyserver/caddy/pull/4728 for a full explanation
675 for _, sblock := range p.serverBlocks {
676 for _, addr := range sblock.parsedKeys {
677 if addr.Path != "" {
678 caddy.Log().Named("caddyfile").Warn("Using a path in a site address is deprecated; please use the 'handle' directive instead", zap.String("address", addr.String()))
679 }
680 }
681 }
682
683 // sort server blocks by their keys; this is important because
684 // only the first matching site should be evaluated, and we should
685 // attempt to match most specific site first (host and path), in
686 // case their matchers overlap; we do this somewhat naively by
687 // descending sort by length of host then path
688 sort.SliceStable(p.serverBlocks, func(i, j int) bool {
689 // TODO: we could pre-process the specificities for efficiency,
690 // but I don't expect many blocks will have THAT many keys...
691 var iLongestPath, jLongestPath string
692 var iLongestHost, jLongestHost string
693 var iWildcardHost, jWildcardHost bool
694 for _, addr := range p.serverBlocks[i].parsedKeys {
695 if strings.Contains(addr.Host, "*") || addr.Host == "" {
696 iWildcardHost = true
697 }
698 if specificity(addr.Host) > specificity(iLongestHost) {
699 iLongestHost = addr.Host
700 }
701 if specificity(addr.Path) > specificity(iLongestPath) {
702 iLongestPath = addr.Path
703 }
704 }
705 for _, addr := range p.serverBlocks[j].parsedKeys {
706 if strings.Contains(addr.Host, "*") || addr.Host == "" {
707 jWildcardHost = true
708 }
709 if specificity(addr.Host) > specificity(jLongestHost) {
710 jLongestHost = addr.Host
711 }
712 if specificity(addr.Path) > specificity(jLongestPath) {
713 jLongestPath = addr.Path
714 }
715 }
716 // catch-all blocks (blocks with no hostname) should always go
717 // last, even after blocks with wildcard hosts
718 if specificity(iLongestHost) == 0 {
719 return false
720 }
721 if specificity(jLongestHost) == 0 {
722 return true
723 }
724 if iWildcardHost != jWildcardHost {
725 // site blocks that have a key with a wildcard in the hostname
726 // must always be less specific than blocks without one; see
727 // https://github.com/caddyserver/caddy/issues/3410
728 return jWildcardHost && !iWildcardHost
729 }
730 if specificity(iLongestHost) == specificity(jLongestHost) {
731 return len(iLongestPath) > len(jLongestPath)
732 }
733 return specificity(iLongestHost) > specificity(jLongestHost)
734 })
735
736 var hasCatchAllTLSConnPolicy, addressQualifiesForTLS bool
737 var emptyConnPolicies caddytls.ConnectionPolicies
738 autoHTTPSWillAddConnPolicy := srv.AutoHTTPS == nil || !srv.AutoHTTPS.Disabled
739
740 // if needed, the ServerLogConfig is initialized beforehand so
741 // that all server blocks can populate it with data, even when not
742 // coming with a log directive
743 for _, sblock := range p.serverBlocks {
744 if len(sblock.pile["custom_log"]) != 0 {
745 srv.Logs = new(caddyhttp.ServerLogConfig)
746 break
747 }
748 }
749
750 // add named routes to the server if 'invoke' was used inside of it
751 configuredNamedRoutes := options["named_routes"].(map[string]*caddyhttp.Route)
752 namedRouteInvokes := options["named_route_invokes"].(map[string][]string)
753 for _, sblock := range p.serverBlocks {
754 if len(sblock.pile[namedRouteKey]) == 0 {
755 continue
756 }
757 for _, value := range sblock.pile[namedRouteKey] {
758 if srv.NamedRoutes == nil {
759 srv.NamedRoutes = map[string]*caddyhttp.Route{}
760 }
761 // named routes may invoke other named routes, so
762 // resolve the set of invoked names transitively
763 names := []string{value.Value.(string)}
764 for len(names) > 0 {
765 name := names[0]
766 names = names[1:]
767 if _, ok := srv.NamedRoutes[name]; ok {
768 continue
769 }
770 if configuredNamedRoutes[name] == nil {
771 return nil, fmt.Errorf("cannot invoke named route '%s', which was not defined", name)
772 }
773 srv.NamedRoutes[name] = configuredNamedRoutes[name]
774 names = append(names, namedRouteInvokes[name]...)
775 }
776 }
777 }
778
779 // create a subroute for each site in the server block
780 for _, sblock := range p.serverBlocks {
781 matcherSetsEnc, err := st.compileEncodedMatcherSets(sblock)
782 if err != nil {
783 return nil, fmt.Errorf("server block %v: compiling matcher sets: %v", sblock.block.Keys, err)
784 }
785
786 hosts := sblock.hostsFromKeys(false)
787
788 // emit warnings if user put unspecified IP addresses; they probably want the bind directive
789 for _, h := range hosts {
790 if h == "0.0.0.0" || h == "::" {
791 caddy.Log().Named("caddyfile").Warn("Site block has an unspecified IP address which only matches requests having that Host header; you probably want the 'bind' directive to configure the socket", zap.String("address", h))
792 }
793 }
794
795 // collect hosts that are forced to be automated
796 forceAutomatedNames := make(map[string]struct{})
797 if _, ok := sblock.pile["tls.force_automate"]; ok {
798 for _, host := range hosts {
799 forceAutomatedNames[host] = struct{}{}
800 }
801 }
802
803 // tls: connection policies
804 if cpVals, ok := sblock.pile["tls.connection_policy"]; ok {
805 // tls connection policies
806 for _, cpVal := range cpVals {
807 // Policies are parsed once per original server block. Each listener
808 // rewrites and consolidates them independently.
809 cp, err := cloneConnectionPolicy(cpVal.Value.(*caddytls.ConnectionPolicy))
810 if err != nil {
811 return nil, fmt.Errorf("cloning TLS connection policy: %v", err)
812 }
813
814 // make sure the policy covers all hostnames from the block
815 for _, h := range hosts {
816 if h == defaultSNI {
817 hosts = append(hosts, "")
818 cp.DefaultSNI = defaultSNI
819 break
820 }
821 if h == fallbackSNI {
822 hosts = append(hosts, "")
823 cp.FallbackSNI = fallbackSNI
824 break
825 }
826 }
827
828 if len(hosts) > 0 {
829 slices.Sort(hosts) // for deterministic JSON output
830 cp.MatchersRaw = caddy.ModuleMap{
831 "sni": caddyconfig.JSON(hosts, warnings), // make sure to match all hosts, not just auto-HTTPS-qualified ones
832 }
833 } else {
834 cp.DefaultSNI = defaultSNI
835 cp.FallbackSNI = fallbackSNI
836 }
837
838 // only append this policy if it actually changes something,
839 // or if the configuration explicitly automates certs for
840 // these names (this is necessary to hoist a connection policy
841 // above one that may manually load a wildcard cert that would
842 // otherwise clobber the automated one; the code that appends
843 // policies that manually load certs comes later, so they're
844 // lower in the list)
845 if !cp.SettingsEmpty() || mapContains(forceAutomatedNames, hosts) {
846 srv.TLSConnPolicies = append(srv.TLSConnPolicies, cp)
847 hasCatchAllTLSConnPolicy = len(hosts) == 0
848 } else if len(hosts) > 0 {
849 emptyConnPolicies = append(emptyConnPolicies, cp)
850 }
851 }
852 } else if specificHosts := slices.DeleteFunc(
853 slices.Clone(hosts),
854 func(h string) bool { return h == "" || strings.Contains(h, "*") },
855 ); len(specificHosts) > 0 {
856 // site blocks with no TLS connection policy of their own may
857 // still need an empty policy hoisted above a wildcard policy
858 // that requires client authentication, or that requirement
859 // would apply to their more specific hostnames too - see the
860 // hoisting loop below and issue #7860
861 slices.Sort(specificHosts)
862 emptyConnPolicies = append(emptyConnPolicies, &caddytls.ConnectionPolicy{
863 MatchersRaw: caddy.ModuleMap{
864 "sni": caddyconfig.JSON(specificHosts, warnings),
865 },
866 })
867 }
868
869 for _, addr := range sblock.parsedKeys {
870 // if server only uses HTTP port, auto-HTTPS will not apply
871 if listenersUseAnyPortOtherThan(srv.Listen, httpPort) {
872 // exclude any hosts that were defined explicitly with "http://"
873 // in the key from automated cert management (issue #2998)
874 if addr.Scheme == "http" && addr.Host != "" {
875 if srv.AutoHTTPS == nil {
876 srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
877 }
878 if !slices.Contains(srv.AutoHTTPS.Skip, addr.Host) {
879 srv.AutoHTTPS.Skip = append(srv.AutoHTTPS.Skip, addr.Host)
880 }
881 }
882 }
883
884 // If TLS is specified as directive, it will also result in 1 or more connection policy being created
885 // Thus, catch-all address with non-standard port, e.g. :8443, can have TLS enabled without
886 // specifying prefix "https://"
887 // Second part of the condition is to allow creating TLS conn policy even though `auto_https` has been disabled
888 // ensuring compatibility with behavior described in below link
889 // https://caddy.community/t/making-sense-of-auto-https-and-why-disabling-it-still-serves-https-instead-of-http/9761
890 createdTLSConnPolicies, ok := sblock.pile["tls.connection_policy"]
891 hasTLSEnabled := (ok && len(createdTLSConnPolicies) > 0) ||
892 (addr.Host != "" && (srv.AutoHTTPS == nil || !slices.Contains(srv.AutoHTTPS.Skip, addr.Host)))
893
894 // we'll need to remember if the address qualifies for auto-HTTPS, so we
895 // can add a TLS conn policy if necessary
896 if addr.Scheme == "https" ||
897 (addr.Scheme != "http" && addr.Port != httpPort && hasTLSEnabled) {
898 addressQualifiesForTLS = true
899 }
900
901 // predict whether auto-HTTPS will add the conn policy for us; if so, we
902 // may not need to add one for this server
903 autoHTTPSWillAddConnPolicy = autoHTTPSWillAddConnPolicy &&
904 (addr.Port == httpsPort || (addr.Port != httpPort && addr.Host != ""))
905 }
906
907 // Look for any config values that provide listener wrappers on the server block
908 for _, listenerConfig := range sblock.pile["listener_wrapper"] {
909 listenerWrapper, ok := listenerConfig.Value.(caddy.ListenerWrapper)
910 if !ok {
911 return nil, fmt.Errorf("config for a listener wrapper did not provide a value that implements caddy.ListenerWrapper")
912 }
913 jsonListenerWrapper := caddyconfig.JSONModuleObject(
914 listenerWrapper,
915 "wrapper",
916 listenerWrapper.(caddy.Module).CaddyModule().ID.Name(),
917 warnings,
918 )
919 srv.ListenerWrappersRaw = append(srv.ListenerWrappersRaw, jsonListenerWrapper)
920 }
921
922 // Look for any config values that provide packet conn wrappers on the server block
923 for _, listenerConfig := range sblock.pile["packet_conn_wrapper"] {
924 packetConnWrapper, ok := listenerConfig.Value.(caddy.PacketConnWrapper)
925 if !ok {
926 return nil, fmt.Errorf("config for a packet conn wrapper did not provide a value that implements caddy.PacketConnWrapper")
927 }
928 jsonPacketConnWrapper := caddyconfig.JSONModuleObject(
929 packetConnWrapper,
930 "wrapper",
931 packetConnWrapper.(caddy.Module).CaddyModule().ID.Name(),
932 warnings,
933 )
934 srv.PacketConnWrappersRaw = append(srv.PacketConnWrappersRaw, jsonPacketConnWrapper)
935 }
936
937 // set up each handler directive, making sure to honor directive order
938 dirRoutes := sblock.pile["route"]
939 siteSubroute, err := buildSubroute(dirRoutes, groupCounter, directiveOrderFor(options))
940 if err != nil {
941 return nil, err
942 }
943
944 // add the site block's route(s) to the server
945 srv.Routes = appendSubrouteToRouteList(srv.Routes, siteSubroute, matcherSetsEnc, p, warnings)
946
947 // if error routes are defined, add those too
948 if errorSubrouteVals, ok := sblock.pile["error_route"]; ok {
949 if srv.Errors == nil {
950 srv.Errors = new(caddyhttp.HTTPErrorConfig)
951 }
952 sort.SliceStable(errorSubrouteVals, func(i, j int) bool {
953 sri, srj := errorSubrouteVals[i].Value.(*caddyhttp.Subroute), errorSubrouteVals[j].Value.(*caddyhttp.Subroute)
954 return len(sri.Routes[0].MatcherSetsRaw) != 0 && len(srj.Routes[0].MatcherSetsRaw) == 0
955 })
956 errorsSubroute := &caddyhttp.Subroute{}
957 for _, val := range errorSubrouteVals {
958 sr := val.Value.(*caddyhttp.Subroute)
959 errorsSubroute.Routes = append(errorsSubroute.Routes, sr.Routes...)
960 }
961 srv.Errors.Routes = appendSubrouteToRouteList(srv.Errors.Routes, errorsSubroute, matcherSetsEnc, p, warnings)
962 }
963
964 // add log associations
965 // see https://github.com/caddyserver/caddy/issues/3310
966 sblockLogHosts := sblock.hostsFromKeys(true)
967 for _, cval := range sblock.pile["custom_log"] {
968 ncl := cval.Value.(namedCustomLog)
969
970 // if `no_hostname` is set, then this logger will not
971 // be associated with any of the site block's hostnames,
972 // and only be usable via the `log_name` directive
973 // or the `access_logger_names` variable
974 if ncl.noHostname {
975 continue
976 }
977
978 if sblock.hasHostCatchAllKey() && len(ncl.hostnames) == 0 {
979 // all requests for hosts not able to be listed should use
980 // this log because it's a catch-all-hosts server block
981 srv.Logs.DefaultLoggerName = ncl.name
982 } else if len(ncl.hostnames) > 0 {
983 // if the logger overrides the hostnames, map that to the logger name
984 for _, h := range ncl.hostnames {
985 if srv.Logs.LoggerNames == nil {
986 srv.Logs.LoggerNames = make(map[string]caddyhttp.StringArray)
987 }
988 srv.Logs.LoggerNames[h] = append(srv.Logs.LoggerNames[h], ncl.name)
989 }
990 } else {
991 // otherwise, map each host to the logger name
992 for _, h := range sblockLogHosts {
993 // strip the port from the host, if any
994 host, _, err := net.SplitHostPort(h)
995 if err != nil {
996 host = h
997 }
998 if srv.Logs.LoggerNames == nil {
999 srv.Logs.LoggerNames = make(map[string]caddyhttp.StringArray)
1000 }
1001 srv.Logs.LoggerNames[host] = append(srv.Logs.LoggerNames[host], ncl.name)
1002 }
1003 }
1004 }
1005 if srv.Logs != nil && len(sblock.pile["custom_log"]) == 0 {
1006 // server has access logs enabled, but this server block does not
1007 // enable access logs; therefore, all hosts of this server block
1008 // should not be access-logged
1009 if len(hosts) == 0 {
1010 // if the server block has a catch-all-hosts key, then we should
1011 // not log reqs to any host unless it appears in the map
1012 srv.Logs.SkipUnmappedHosts = true
1013 }
1014 srv.Logs.SkipHosts = append(srv.Logs.SkipHosts, sblockLogHosts...)
1015 }
1016 }
1017
1018 // sort for deterministic JSON output
1019 if srv.Logs != nil {
1020 slices.Sort(srv.Logs.SkipHosts)
1021 }
1022
1023 // a server cannot (natively) serve both HTTP and HTTPS at the
1024 // same time, so make sure the configuration isn't in conflict
1025 err := detectConflictingSchemes(srv, p.serverBlocks, options)
1026 if err != nil {
1027 return nil, err
1028 }
1029
1030 // hostnames whose site blocks configure no TLS settings of their own
1031 // still need shielding if a policy matching a wildcard hostname would
1032 // otherwise impose CLIENT AUTHENTICATION on them. Because connection
1033 // policies are first-match, the shield hoisted above the wildcard
1034 // policy must be a COPY of that policy with only client_authentication
1035 // removed: an empty policy would not just lift the client-auth
1036 // requirement but suppress every other setting the wildcard policy
1037 // carries (certificate selection, protocol bounds, ALPN, ...), which
1038 // the covered hostname does want to inherit - see issue #7860
1039 // the wildcard names each client-auth policy matches, decoded once and
1040 // index-aligned with srv.TLSConnPolicies; nil for a policy that needs
1041 // no client auth or matches no wildcard
1042 clientAuthWildcards := make([][]string, len(srv.TLSConnPolicies))
1043 for i, cp := range srv.TLSConnPolicies {
1044 if cp.ClientAuthentication == nil {
1045 continue
1046 }
1047 names, ok := sniNames(cp, "of existing connection policy ", warnings)
1048 if !ok {
1049 continue
1050 }
1051 clientAuthWildcards[i] = slices.DeleteFunc(names, func(name string) bool {
1052 return !strings.Contains(name, "*")
1053 })
1054 }
1055
1056 // each covering policy needs its OWN shield matching only the
1057 // hostnames it covers: the hostnames of a single site block may be
1058 // covered by DIFFERENT wildcards, and a shield hoisted above one
1059 // policy must not match the hostnames belonging to another, or it
1060 // would lift their client-auth requirement too
1061 shieldedHosts := make([][]string, len(srv.TLSConnPolicies))
1062 for _, ecp := range emptyConnPolicies {
1063 hosts, ok := sniNames(ecp, "", warnings)
1064 if !ok {
1065 continue // no SNI matcher to reason about
1066 }
1067 for _, host := range hosts {
1068 // connection policies are first-match, so only the first
1069 // covering policy is ever reached for this hostname
1070 for i, wildcards := range clientAuthWildcards {
1071 if slices.ContainsFunc(wildcards, func(name string) bool {
1072 return certmagic.MatchWildcard(host, name)
1073 }) {
1074 shieldedHosts[i] = append(shieldedHosts[i], host)
1075 break
1076 }
1077 }
1078 }
1079 }
1080
1081 // hoist the shields last to last, so that inserting one does not
1082 // shift the index of a covering policy still to be shielded
1083 for i, hosts := range slices.Backward(shieldedHosts) {
1084 if len(hosts) == 0 {
1085 continue
1086 }
1087 slices.Sort(hosts)
1088 shield := *srv.TLSConnPolicies[i]
1089 shield.ClientAuthentication = nil
1090 shield.MatchersRaw = caddy.ModuleMap{
1091 "sni": caddyconfig.JSON(slices.Compact(hosts), warnings),
1092 }
1093 srv.TLSConnPolicies = slices.Insert(srv.TLSConnPolicies, i, &shield)
1094 }
1095
1096 // a catch-all TLS conn policy is necessary to ensure TLS can
1097 // be offered to all hostnames of the server; even though only
1098 // one policy is needed to enable TLS for the server, that
1099 // policy might apply to only certain TLS handshakes; but when
1100 // using the Caddyfile, user would expect all handshakes to at
1101 // least have a matching connection policy, so here we append a
1102 // catch-all/default policy if there isn't one already (it's
1103 // important that it goes at the end) - see issue #3004:
1104 // https://github.com/caddyserver/caddy/issues/3004
1105 // TODO: maybe a smarter way to handle this might be to just make the
1106 // auto-HTTPS logic at provision-time detect if there is any connection
1107 // policy missing for any HTTPS-enabled hosts, if so, add it... maybe?
1108 if addressQualifiesForTLS &&
1109 !hasCatchAllTLSConnPolicy &&
1110 (len(srv.TLSConnPolicies) > 0 || !autoHTTPSWillAddConnPolicy || defaultSNI != "" || fallbackSNI != "") {
1111 srv.TLSConnPolicies = append(srv.TLSConnPolicies, &caddytls.ConnectionPolicy{
1112 DefaultSNI: defaultSNI,
1113 FallbackSNI: fallbackSNI,
1114 })
1115 }
1116
1117 // tidy things up a bit
1118 srv.TLSConnPolicies, err = consolidateConnPolicies(srv.TLSConnPolicies)
1119 if err != nil {
1120 return nil, fmt.Errorf("consolidating TLS connection policies for server %d: %v", i, err)
1121 }
1122 srv.Routes = consolidateRoutes(srv.Routes)
1123
1124 servers[fmt.Sprintf("srv%d", i)] = srv
1125 }
1126
1127 if err := applyServerOptions(servers, options, warnings); err != nil {
1128 return nil, fmt.Errorf("applying global server options: %v", err)
1129 }
1130
1131 return servers, nil
1132 }
1133
1134 func cloneConnectionPolicy(cp *caddytls.ConnectionPolicy) (*caddytls.ConnectionPolicy, error) {
1135 encoded, err := json.Marshal(cp)
1136 if err != nil {
1137 return nil, err
1138 }
1139
1140 cloned := new(caddytls.ConnectionPolicy)
1141 if err := json.Unmarshal(encoded, cloned); err != nil {
1142 return nil, err
1143 }
1144 return cloned, nil
1145 }
1146
1147 // sniNames returns the server names a connection policy's sni matcher matches.
1148 // The bool is false when the policy has no sni matcher, or when it does not
1149 // decode - the latter is unexpected enough to warn about rather than silently
1150 // skip, since callers use it to decide whether a hostname needs shielding.
1151 func sniNames(cp *caddytls.ConnectionPolicy, what string, warnings *[]caddyconfig.Warning) ([]string, bool) {
1152 raw, ok := cp.MatchersRaw["sni"]
1153 if !ok {
1154 return nil, false
1155 }
1156 var sni caddytls.MatchServerName
1157 if err := json.Unmarshal(raw, &sni); err != nil {
1158 *warnings = append(*warnings, caddyconfig.Warning{
1159 Message: fmt.Sprintf("decoding sni matcher %swhile checking wildcard coverage: %v", what, err),
1160 })
1161 return nil, false
1162 }
1163 return sni, true
1164 }
1165
1166 func detectConflictingSchemes(srv *caddyhttp.Server, serverBlocks []serverBlock, options map[string]any) error {
1167 httpPort := strconv.Itoa(caddyhttp.DefaultHTTPPort)
1168 if hp, ok := options["http_port"].(int); ok {
1169 httpPort = strconv.Itoa(hp)
1170 }
1171 httpsPort := strconv.Itoa(caddyhttp.DefaultHTTPSPort)
1172 if hsp, ok := options["https_port"].(int); ok {
1173 httpsPort = strconv.Itoa(hsp)
1174 }
1175
1176 var httpOrHTTPS string
1177 checkAndSetHTTP := func(addr Address) error {
1178 if httpOrHTTPS == "HTTPS" {
1179 errMsg := fmt.Errorf("server listening on %v is configured for HTTPS and cannot natively multiplex HTTP and HTTPS: %s",
1180 srv.Listen, addr.Original)
1181 if addr.Scheme == "" && addr.Host == "" {
1182 errMsg = fmt.Errorf("%s (try specifying https:// in the address)", errMsg)
1183 }
1184 return errMsg
1185 }
1186 if len(srv.TLSConnPolicies) > 0 {
1187 // any connection policies created for an HTTP server
1188 // is a logical conflict, as it would enable HTTPS
1189 return fmt.Errorf("server listening on %v is HTTP, but attempts to configure TLS connection policies", srv.Listen)
1190 }
1191 httpOrHTTPS = "HTTP"
1192 return nil
1193 }
1194 checkAndSetHTTPS := func(addr Address) error {
1195 if httpOrHTTPS == "HTTP" {
1196 return fmt.Errorf("server listening on %v is configured for HTTP and cannot natively multiplex HTTP and HTTPS: %s",
1197 srv.Listen, addr.Original)
1198 }
1199 httpOrHTTPS = "HTTPS"
1200 return nil
1201 }
1202
1203 for _, sblock := range serverBlocks {
1204 for _, addr := range sblock.parsedKeys {
1205 if addr.Scheme == "http" || addr.Port == httpPort {
1206 if err := checkAndSetHTTP(addr); err != nil {
1207 return err
1208 }
1209 } else if addr.Scheme == "https" || addr.Port == httpsPort || len(srv.TLSConnPolicies) > 0 {
1210 if err := checkAndSetHTTPS(addr); err != nil {
1211 return err
1212 }
1213 } else if addr.Host == "" {
1214 if err := checkAndSetHTTP(addr); err != nil {
1215 return err
1216 }
1217 }
1218 }
1219 }
1220
1221 return nil
1222 }
1223
1224 // consolidateConnPolicies sorts any catch-all policy to the end, removes empty TLS connection
1225 // policies, and combines equivalent ones for a cleaner overall output.
1226 func consolidateConnPolicies(cps caddytls.ConnectionPolicies) (caddytls.ConnectionPolicies, error) {
1227 // catch-all policies (those without any matcher) should be at the
1228 // end, otherwise it nullifies any more specific policies
1229 sort.SliceStable(cps, func(i, j int) bool {
1230 return cps[j].MatchersRaw == nil && cps[i].MatchersRaw != nil
1231 })
1232
1233 for i := 0; i < len(cps); i++ {
1234 // compare it to the others
1235 for j := 0; j < len(cps); j++ {
1236 if j == i {
1237 continue
1238 }
1239
1240 // if they're exactly equal in every way, just keep one of them
1241 if reflect.DeepEqual(cps[i], cps[j]) {
1242 cps = slices.Delete(cps, j, j+1)
1243 i--
1244 break
1245 }
1246
1247 // as a special case, if there are adjacent TLS conn policies that are identical except
1248 // by their matchers, and the matchers are specifically just ServerName ("sni") matchers
1249 // (by far the most common), we can combine them into a single policy
1250 if i == j-1 && len(cps[i].MatchersRaw) == 1 && len(cps[j].MatchersRaw) == 1 {
1251 if iSNIMatcherJSON, ok := cps[i].MatchersRaw["sni"]; ok {
1252 if jSNIMatcherJSON, ok := cps[j].MatchersRaw["sni"]; ok {
1253 // position of policies and the matcher criteria check out; if settings are
1254 // the same, then we can combine the policies; we have to unmarshal and
1255 // remarshal the matchers though
1256 if cps[i].SettingsEqual(*cps[j]) {
1257 var iSNIMatcher caddytls.MatchServerName
1258 if err := json.Unmarshal(iSNIMatcherJSON, &iSNIMatcher); err == nil {
1259 var jSNIMatcher caddytls.MatchServerName
1260 if err := json.Unmarshal(jSNIMatcherJSON, &jSNIMatcher); err == nil {
1261 iSNIMatcher = append(iSNIMatcher, jSNIMatcher...)
1262 cps[i].MatchersRaw["sni"], err = json.Marshal(iSNIMatcher)
1263 if err != nil {
1264 return nil, fmt.Errorf("recombining SNI matchers: %v", err)
1265 }
1266 cps = slices.Delete(cps, j, j+1)
1267 i--
1268 break
1269 }
1270 }
1271 }
1272 }
1273 }
1274 }
1275
1276 // if they have the same matcher, try to reconcile each field: either they must
1277 // be identical, or we have to be able to combine them safely
1278 if reflect.DeepEqual(cps[i].MatchersRaw, cps[j].MatchersRaw) {
1279 if len(cps[i].ALPN) > 0 &&
1280 len(cps[j].ALPN) > 0 &&
1281 !reflect.DeepEqual(cps[i].ALPN, cps[j].ALPN) {
1282 return nil, fmt.Errorf("two policies with same match criteria have conflicting ALPN: %v vs. %v",
1283 cps[i].ALPN, cps[j].ALPN)
1284 }
1285 if len(cps[i].CipherSuites) > 0 &&
1286 len(cps[j].CipherSuites) > 0 &&
1287 !reflect.DeepEqual(cps[i].CipherSuites, cps[j].CipherSuites) {
1288 return nil, fmt.Errorf("two policies with same match criteria have conflicting cipher suites: %v vs. %v",
1289 cps[i].CipherSuites, cps[j].CipherSuites)
1290 }
1291 if cps[i].ClientAuthentication != nil && cps[j].ClientAuthentication != nil &&
1292 !reflect.DeepEqual(cps[i].ClientAuthentication, cps[j].ClientAuthentication) {
1293 return nil, fmt.Errorf("two policies with same match criteria have conflicting client auth configuration: %+v vs. %+v",
1294 cps[i].ClientAuthentication, cps[j].ClientAuthentication)
1295 }
1296 if len(cps[i].Curves) > 0 &&
1297 len(cps[j].Curves) > 0 &&
1298 !reflect.DeepEqual(cps[i].Curves, cps[j].Curves) {
1299 return nil, fmt.Errorf("two policies with same match criteria have conflicting curves: %v vs. %v",
1300 cps[i].Curves, cps[j].Curves)
1301 }
1302 if cps[i].DefaultSNI != "" &&
1303 cps[j].DefaultSNI != "" &&
1304 cps[i].DefaultSNI != cps[j].DefaultSNI {
1305 return nil, fmt.Errorf("two policies with same match criteria have conflicting default SNI: %s vs. %s",
1306 cps[i].DefaultSNI, cps[j].DefaultSNI)
1307 }
1308 if cps[i].FallbackSNI != "" &&
1309 cps[j].FallbackSNI != "" &&
1310 cps[i].FallbackSNI != cps[j].FallbackSNI {
1311 return nil, fmt.Errorf("two policies with same match criteria have conflicting fallback SNI: %s vs. %s",
1312 cps[i].FallbackSNI, cps[j].FallbackSNI)
1313 }
1314 if cps[i].ProtocolMin != "" &&
1315 cps[j].ProtocolMin != "" &&
1316 cps[i].ProtocolMin != cps[j].ProtocolMin {
1317 return nil, fmt.Errorf("two policies with same match criteria have conflicting min protocol: %s vs. %s",
1318 cps[i].ProtocolMin, cps[j].ProtocolMin)
1319 }
1320 if cps[i].ProtocolMax != "" &&
1321 cps[j].ProtocolMax != "" &&
1322 cps[i].ProtocolMax != cps[j].ProtocolMax {
1323 return nil, fmt.Errorf("two policies with same match criteria have conflicting max protocol: %s vs. %s",
1324 cps[i].ProtocolMax, cps[j].ProtocolMax)
1325 }
1326 if cps[i].CertSelection != nil && cps[j].CertSelection != nil {
1327 // merging fields other than AnyTag is not implemented
1328 if !reflect.DeepEqual(cps[i].CertSelection.SerialNumber, cps[j].CertSelection.SerialNumber) ||
1329 !reflect.DeepEqual(cps[i].CertSelection.SubjectOrganization, cps[j].CertSelection.SubjectOrganization) ||
1330 cps[i].CertSelection.PublicKeyAlgorithm != cps[j].CertSelection.PublicKeyAlgorithm ||
1331 !reflect.DeepEqual(cps[i].CertSelection.AllTags, cps[j].CertSelection.AllTags) {
1332 return nil, fmt.Errorf("two policies with same match criteria have conflicting cert selections: %+v vs. %+v",
1333 cps[i].CertSelection, cps[j].CertSelection)
1334 }
1335 }
1336
1337 // by now we've decided that we can merge the two -- we'll keep i and drop j
1338
1339 if len(cps[i].ALPN) == 0 && len(cps[j].ALPN) > 0 {
1340 cps[i].ALPN = cps[j].ALPN
1341 }
1342 if len(cps[i].CipherSuites) == 0 && len(cps[j].CipherSuites) > 0 {
1343 cps[i].CipherSuites = cps[j].CipherSuites
1344 }
1345 if cps[i].ClientAuthentication == nil && cps[j].ClientAuthentication != nil {
1346 cps[i].ClientAuthentication = cps[j].ClientAuthentication
1347 }
1348 if len(cps[i].Curves) == 0 && len(cps[j].Curves) > 0 {
1349 cps[i].Curves = cps[j].Curves
1350 }
1351 if cps[i].DefaultSNI == "" && cps[j].DefaultSNI != "" {
1352 cps[i].DefaultSNI = cps[j].DefaultSNI
1353 }
1354 if cps[i].FallbackSNI == "" && cps[j].FallbackSNI != "" {
1355 cps[i].FallbackSNI = cps[j].FallbackSNI
1356 }
1357 if cps[i].ProtocolMin == "" && cps[j].ProtocolMin != "" {
1358 cps[i].ProtocolMin = cps[j].ProtocolMin
1359 }
1360 if cps[i].ProtocolMax == "" && cps[j].ProtocolMax != "" {
1361 cps[i].ProtocolMax = cps[j].ProtocolMax
1362 }
1363
1364 if cps[i].CertSelection == nil && cps[j].CertSelection != nil {
1365 // if j is the only one with a policy, move it over to i
1366 cps[i].CertSelection = cps[j].CertSelection
1367 } else if cps[i].CertSelection != nil && cps[j].CertSelection != nil {
1368 // if both have one, then combine AnyTag
1369 for _, tag := range cps[j].CertSelection.AnyTag {
1370 if !slices.Contains(cps[i].CertSelection.AnyTag, tag) {
1371 cps[i].CertSelection.AnyTag = append(cps[i].CertSelection.AnyTag, tag)
1372 }
1373 }
1374 }
1375
1376 cps = slices.Delete(cps, j, j+1)
1377 i--
1378 break
1379 }
1380 }
1381 }
1382
1383 return cps, nil
1384 }
1385
1386 // appendSubrouteToRouteList appends the routes in subroute
1387 // to the routeList, optionally qualified by matchers.
1388 func appendSubrouteToRouteList(routeList caddyhttp.RouteList,
1389 subroute *caddyhttp.Subroute,
1390 matcherSetsEnc []caddy.ModuleMap,
1391 p sbAddrAssociation,
1392 warnings *[]caddyconfig.Warning,
1393 ) caddyhttp.RouteList {
1394 // nothing to do if... there's nothing to do
1395 if len(matcherSetsEnc) == 0 && len(subroute.Routes) == 0 && subroute.Errors == nil {
1396 return routeList
1397 }
1398
1399 // No need to wrap the handlers in a subroute if this is the only server block
1400 // and there is no matcher for it (doing so would produce unnecessarily nested
1401 // JSON), *unless* there is a host matcher within this site block; if so, then
1402 // we still need to wrap in a subroute because otherwise the host matcher from
1403 // the inside of the site block would be a top-level host matcher, which is
1404 // subject to auto-HTTPS (cert management), and using a host matcher within
1405 // a site block is a valid, common pattern for excluding domains from cert
1406 // management, leading to unexpected behavior; see issue #5124.
1407 wrapInSubroute := true
1408 if len(matcherSetsEnc) == 0 && len(p.serverBlocks) == 1 {
1409 var hasHostMatcher bool
1410 outer:
1411 for _, route := range subroute.Routes {
1412 for _, ms := range route.MatcherSetsRaw {
1413 for matcherName := range ms {
1414 if matcherName == "host" {
1415 hasHostMatcher = true
1416 break outer
1417 }
1418 }
1419 }
1420 }
1421 wrapInSubroute = hasHostMatcher
1422 }
1423
1424 if wrapInSubroute {
1425 route := caddyhttp.Route{
1426 // the semantics of a site block in the Caddyfile dictate
1427 // that only the first matching one is evaluated, since
1428 // site blocks do not cascade nor inherit
1429 Terminal: true,
1430 }
1431 if len(matcherSetsEnc) > 0 {
1432 route.MatcherSetsRaw = matcherSetsEnc
1433 }
1434 if len(subroute.Routes) > 0 || subroute.Errors != nil {
1435 route.HandlersRaw = []json.RawMessage{
1436 caddyconfig.JSONModuleObject(subroute, "handler", "subroute", warnings),
1437 }
1438 }
1439 if len(route.MatcherSetsRaw) > 0 || len(route.HandlersRaw) > 0 {
1440 routeList = append(routeList, route)
1441 }
1442 } else {
1443 routeList = append(routeList, subroute.Routes...)
1444 }
1445
1446 return routeList
1447 }
1448
1449 // buildSubroute turns the config values, which are expected to be routes
1450 // into a clean and orderly subroute that has all the routes within it.
1451 // The order is the one belonging to the current adaptation; pass nil to
1452 // keep the routes in the order they were written in.
1453 func buildSubroute(routes []ConfigValue, groupCounter counter, order []string) (*caddyhttp.Subroute, error) {
1454 if order != nil {
1455 for _, val := range routes {
1456 if !slices.Contains(order, val.directive) {
1457 return nil, fmt.Errorf("directive '%s' is not an ordered HTTP handler, so it cannot be used here - try placing within a route block or using the order global option", val.directive)
1458 }
1459 }
1460
1461 sortRoutes(routes, order)
1462 }
1463
1464 subroute := new(caddyhttp.Subroute)
1465
1466 // some directives are mutually exclusive (only first matching
1467 // instance should be evaluated); this is done by putting their
1468 // routes in the same group
1469 mutuallyExclusiveDirs := map[string]*struct {
1470 count int
1471 groupName string
1472 }{
1473 // as a special case, group rewrite directives so that they are mutually exclusive;
1474 // this means that only the first matching rewrite will be evaluated, and that's
1475 // probably a good thing, since there should never be a need to do more than one
1476 // rewrite (I think?), and cascading rewrites smell bad... imagine these rewrites:
1477 // rewrite /docs/json/* /docs/json/index.html
1478 // rewrite /docs/* /docs/index.html
1479 // (We use this on the Caddy website, or at least we did once.) The first rewrite's
1480 // result is also matched by the second rewrite, making the first rewrite pointless.
1481 // See issue #2959.
1482 "rewrite": {},
1483
1484 // handle blocks are also mutually exclusive by definition
1485 "handle": {},
1486
1487 // root just sets a variable, so if it was not mutually exclusive, intersecting
1488 // root directives would overwrite previously-matched ones; they should not cascade
1489 "root": {},
1490 }
1491
1492 // we need to deterministically loop over each of these directives
1493 // in order to keep the group numbers consistent
1494 keys := make([]string, 0, len(mutuallyExclusiveDirs))
1495 for k := range mutuallyExclusiveDirs {
1496 keys = append(keys, k)
1497 }
1498 sort.Strings(keys)
1499
1500 for _, meDir := range keys {
1501 info := mutuallyExclusiveDirs[meDir]
1502
1503 // see how many instances of the directive there are
1504 for _, r := range routes {
1505 if r.directive == meDir {
1506 info.count++
1507 if info.count > 1 {
1508 break
1509 }
1510 }
1511 }
1512 // if there is more than one, put them in a group
1513 // (special case: "rewrite" directive must always be in
1514 // its own group--even if there is only one--because we
1515 // do not want a rewrite to be consolidated into other
1516 // adjacent routes that happen to have the same matcher,
1517 // see caddyserver/caddy#3108 - because the implied
1518 // intent of rewrite is to do an internal redirect,
1519 // we can't assume that the request will continue to
1520 // match the same matcher; anyway, giving a route a
1521 // unique group name should keep it from consolidating)
1522 if info.count > 1 || meDir == "rewrite" {
1523 info.groupName = groupCounter.nextGroup()
1524 }
1525 }
1526
1527 // add all the routes piled in from directives
1528 for _, r := range routes {
1529 // put this route into a group if it is mutually exclusive
1530 if info, ok := mutuallyExclusiveDirs[r.directive]; ok {
1531 route := r.Value.(caddyhttp.Route)
1532 route.Group = info.groupName
1533 r.Value = route
1534 }
1535
1536 switch route := r.Value.(type) {
1537 case caddyhttp.Subroute:
1538 // if a route-class config value is actually a Subroute handler
1539 // with nothing but a list of routes, then it is the intention
1540 // of the directive to keep these handlers together and in this
1541 // same order, but not necessarily in a subroute (if it wanted
1542 // to keep them in a subroute, the directive would have returned
1543 // a route with a Subroute as its handler); this is useful to
1544 // keep multiple handlers/routes together and in the same order
1545 // so that the sorting procedure we did above doesn't reorder them
1546 if route.Errors != nil {
1547 // if error handlers are also set, this is confusing; it's
1548 // probably supposed to be wrapped in a Route and encoded
1549 // as a regular handler route... programmer error.
1550 panic("found subroute with more than just routes; perhaps it should have been wrapped in a route?")
1551 }
1552 subroute.Routes = append(subroute.Routes, route.Routes...)
1553 case caddyhttp.Route:
1554 subroute.Routes = append(subroute.Routes, route)
1555 }
1556 }
1557
1558 subroute.Routes = consolidateRoutes(subroute.Routes)
1559
1560 return subroute, nil
1561 }
1562
1563 // normalizeDirectiveName ensures directives that should be sorted
1564 // at the same level are named the same before sorting happens.
1565 func normalizeDirectiveName(directive string) string {
1566 // As a special case, we want "handle_path" to be sorted
1567 // at the same level as "handle", so we force them to use
1568 // the same directive name after their parsing is complete.
1569 // See https://github.com/caddyserver/caddy/issues/3675#issuecomment-678042377
1570 if directive == "handle_path" {
1571 directive = "handle"
1572 }
1573 return directive
1574 }
1575
1576 // consolidateRoutes combines routes with the same properties
1577 // (same matchers, same Terminal and Group settings) for a
1578 // cleaner overall output.
1579 func consolidateRoutes(routes caddyhttp.RouteList) caddyhttp.RouteList {
1580 for i := 0; i < len(routes)-1; i++ {
1581 if reflect.DeepEqual(routes[i].MatcherSetsRaw, routes[i+1].MatcherSetsRaw) &&
1582 routes[i].Terminal == routes[i+1].Terminal &&
1583 routes[i].Group == routes[i+1].Group {
1584 // keep the handlers in the same order, then splice out repetitive route
1585 routes[i].HandlersRaw = append(routes[i].HandlersRaw, routes[i+1].HandlersRaw...)
1586 routes = append(routes[:i+1], routes[i+2:]...)
1587 i--
1588 }
1589 }
1590 return routes
1591 }
1592
1593 func matcherSetFromMatcherToken(
1594 tkn caddyfile.Token,
1595 matcherDefs map[string]caddy.ModuleMap,
1596 warnings *[]caddyconfig.Warning,
1597 ) (caddy.ModuleMap, bool, error) {
1598 // matcher tokens can be wildcards, simple path matchers,
1599 // or refer to a pre-defined matcher by some name
1600 if tkn.Text == "*" {
1601 // match all requests == no matchers, so nothing to do
1602 return nil, true, nil
1603 }
1604
1605 // convenient way to specify a single path match
1606 if strings.HasPrefix(tkn.Text, "/") {
1607 return caddy.ModuleMap{
1608 "path": caddyconfig.JSON(caddyhttp.MatchPath{tkn.Text}, warnings),
1609 }, true, nil
1610 }
1611
1612 // pre-defined matcher
1613 if strings.HasPrefix(tkn.Text, matcherPrefix) {
1614 m, ok := matcherDefs[tkn.Text]
1615 if !ok {
1616 return nil, false, fmt.Errorf("unrecognized matcher name: %+v", tkn.Text)
1617 }
1618 return m, true, nil
1619 }
1620
1621 return nil, false, nil
1622 }
1623
1624 func (st *ServerType) compileEncodedMatcherSets(sblock serverBlock) ([]caddy.ModuleMap, error) {
1625 type hostPathPair struct {
1626 hostm caddyhttp.MatchHost
1627 pathm caddyhttp.MatchPath
1628 }
1629
1630 // keep routes with common host and path matchers together
1631 var matcherPairs []*hostPathPair
1632
1633 var catchAllHosts bool
1634 for _, addr := range sblock.parsedKeys {
1635 // choose a matcher pair that should be shared by this
1636 // server block; if none exists yet, create one
1637 var chosenMatcherPair *hostPathPair
1638 for _, mp := range matcherPairs {
1639 if (len(mp.pathm) == 0 && addr.Path == "") ||
1640 (len(mp.pathm) == 1 && mp.pathm[0] == addr.Path) {
1641 chosenMatcherPair = mp
1642 break
1643 }
1644 }
1645 if chosenMatcherPair == nil {
1646 chosenMatcherPair = new(hostPathPair)
1647 if addr.Path != "" {
1648 chosenMatcherPair.pathm = []string{addr.Path}
1649 }
1650 matcherPairs = append(matcherPairs, chosenMatcherPair)
1651 }
1652
1653 // if one of the keys has no host (i.e. is a catch-all for
1654 // any hostname), then we need to null out the host matcher
1655 // entirely so that it matches all hosts
1656 if addr.Host == "" && !catchAllHosts {
1657 chosenMatcherPair.hostm = nil
1658 catchAllHosts = true
1659 }
1660 if catchAllHosts {
1661 continue
1662 }
1663
1664 // add this server block's keys to the matcher
1665 // pair if it doesn't already exist
1666 if addr.Host != "" && !slices.Contains(chosenMatcherPair.hostm, addr.Host) {
1667 chosenMatcherPair.hostm = append(chosenMatcherPair.hostm, addr.Host)
1668 }
1669 }
1670
1671 // iterate each pairing of host and path matchers and
1672 // put them into a map for JSON encoding
1673 var matcherSets []map[string]caddyhttp.RequestMatcherWithError
1674 for _, mp := range matcherPairs {
1675 matcherSet := make(map[string]caddyhttp.RequestMatcherWithError)
1676 if len(mp.hostm) > 0 {
1677 matcherSet["host"] = mp.hostm
1678 }
1679 if len(mp.pathm) > 0 {
1680 matcherSet["path"] = mp.pathm
1681 }
1682 if len(matcherSet) > 0 {
1683 matcherSets = append(matcherSets, matcherSet)
1684 }
1685 }
1686
1687 // finally, encode each of the matcher sets
1688 matcherSetsEnc := make([]caddy.ModuleMap, 0, len(matcherSets))
1689 for _, ms := range matcherSets {
1690 msEncoded, err := encodeMatcherSet(ms)
1691 if err != nil {
1692 return nil, fmt.Errorf("server block %v: %v", sblock.block.Keys, err)
1693 }
1694 matcherSetsEnc = append(matcherSetsEnc, msEncoded)
1695 }
1696
1697 return matcherSetsEnc, nil
1698 }
1699
1700 func parseMatcherDefinitions(d *caddyfile.Dispenser, matchers map[string]caddy.ModuleMap) error {
1701 d.Next() // advance to the first token
1702
1703 // this is the "name" for "named matchers"
1704 definitionName := d.Val()
1705
1706 if _, ok := matchers[definitionName]; ok {
1707 return fmt.Errorf("matcher is defined more than once: %s", definitionName)
1708 }
1709 matchers[definitionName] = make(caddy.ModuleMap)
1710
1711 // given a matcher name and the tokens following it, parse
1712 // the tokens as a matcher module and record it
1713 makeMatcher := func(matcherName string, tokens []caddyfile.Token) error {
1714 // create a new dispenser from the tokens
1715 dispenser := caddyfile.NewDispenser(tokens)
1716
1717 // set the matcher name (without @) in the dispenser context so
1718 // that matcher modules can access it to use it as their name
1719 // (e.g. regexp matchers which use the name for capture groups)
1720 dispenser.SetContext(caddyfile.MatcherNameCtxKey, definitionName[1:])
1721
1722 mod, err := caddy.GetModule("http.matchers." + matcherName)
1723 if err != nil {
1724 return fmt.Errorf("getting matcher module '%s': %v", matcherName, err)
1725 }
1726 unm, ok := mod.New().(caddyfile.Unmarshaler)
1727 if !ok {
1728 return fmt.Errorf("matcher module '%s' is not a Caddyfile unmarshaler", matcherName)
1729 }
1730 err = unm.UnmarshalCaddyfile(dispenser)
1731 if err != nil {
1732 return err
1733 }
1734
1735 if rm, ok := unm.(caddyhttp.RequestMatcherWithError); ok {
1736 matchers[definitionName][matcherName] = caddyconfig.JSON(rm, nil)
1737 return nil
1738 }
1739 // nolint:staticcheck
1740 if rm, ok := unm.(caddyhttp.RequestMatcher); ok {
1741 matchers[definitionName][matcherName] = caddyconfig.JSON(rm, nil)
1742 return nil
1743 }
1744 return fmt.Errorf("matcher module '%s' is not a request matcher", matcherName)
1745 }
1746
1747 // if the next token is quoted, we can assume it's not a matcher name
1748 // and that it's probably an 'expression' matcher
1749 if d.NextArg() {
1750 if d.Token().Quoted() {
1751 // since it was missing the matcher name, we insert a token
1752 // in front of the expression token itself; we use Clone() to
1753 // make the new token to keep the same the import location as
1754 // the next token, if this is within a snippet or imported file.
1755 // see https://github.com/caddyserver/caddy/issues/6287
1756 expressionToken := d.Token().Clone()
1757 expressionToken.Text = "expression"
1758 err := makeMatcher("expression", []caddyfile.Token{expressionToken, d.Token()})
1759 if err != nil {
1760 return err
1761 }
1762 return nil
1763 }
1764
1765 // if it wasn't quoted, then we need to rewind after calling
1766 // d.NextArg() so the below properly grabs the matcher name
1767 d.Prev()
1768 }
1769
1770 // in case there are multiple instances of the same matcher, concatenate
1771 // their tokens (we expect that UnmarshalCaddyfile should be able to
1772 // handle more than one segment); otherwise, we'd overwrite other
1773 // instances of the matcher in this set
1774 tokensByMatcherName := make(map[string][]caddyfile.Token)
1775 for nesting := d.Nesting(); d.NextArg() || d.NextBlock(nesting); {
1776 matcherName := d.Val()
1777 tokensByMatcherName[matcherName] = append(tokensByMatcherName[matcherName], d.NextSegment()...)
1778 }
1779 for matcherName, tokens := range tokensByMatcherName {
1780 err := makeMatcher(matcherName, tokens)
1781 if err != nil {
1782 return err
1783 }
1784 }
1785 return nil
1786 }
1787
1788 func encodeMatcherSet(matchers map[string]caddyhttp.RequestMatcherWithError) (caddy.ModuleMap, error) {
1789 msEncoded := make(caddy.ModuleMap)
1790 for matcherName, val := range matchers {
1791 jsonBytes, err := json.Marshal(val)
1792 if err != nil {
1793 return nil, fmt.Errorf("marshaling matcher set %#v: %v", matchers, err)
1794 }
1795 msEncoded[matcherName] = jsonBytes
1796 }
1797 return msEncoded, nil
1798 }
1799
1800 // WasReplacedPlaceholderShorthand checks if a token string was
1801 // likely a replaced shorthand of the known Caddyfile placeholder
1802 // replacement outputs. Useful to prevent some user-defined map
1803 // output destinations from overlapping with one of the
1804 // predefined shorthands.
1805 func WasReplacedPlaceholderShorthand(token string) string {
1806 prev := ""
1807 for i, item := range placeholderShorthands() {
1808 // only look at every 2nd item, which is the replacement
1809 if i%2 == 0 {
1810 prev = item
1811 continue
1812 }
1813 if strings.Trim(token, "{}") == strings.Trim(item, "{}") {
1814 // we return the original shorthand so it
1815 // can be used for an error message
1816 return prev
1817 }
1818 }
1819 return ""
1820 }
1821
1822 // tryInt tries to convert val to an integer. If it fails,
1823 // it downgrades the error to a warning and returns 0.
1824 func tryInt(val any, warnings *[]caddyconfig.Warning) int {
1825 intVal, ok := val.(int)
1826 if val != nil && !ok && warnings != nil {
1827 *warnings = append(*warnings, caddyconfig.Warning{Message: "not an integer type"})
1828 }
1829 return intVal
1830 }
1831
1832 func tryString(val any, warnings *[]caddyconfig.Warning) string {
1833 stringVal, ok := val.(string)
1834 if val != nil && !ok && warnings != nil {
1835 *warnings = append(*warnings, caddyconfig.Warning{Message: "not a string type"})
1836 }
1837 return stringVal
1838 }
1839
1840 func tryDuration(val any, warnings *[]caddyconfig.Warning) caddy.Duration {
1841 durationVal, ok := val.(caddy.Duration)
1842 if val != nil && !ok && warnings != nil {
1843 *warnings = append(*warnings, caddyconfig.Warning{Message: "not a duration type"})
1844 }
1845 return durationVal
1846 }
1847
1848 // listenersUseAnyPortOtherThan returns true if there are any
1849 // listeners in addresses that use a port which is not otherPort.
1850 // Mostly borrowed from unexported method in caddyhttp package.
1851 func listenersUseAnyPortOtherThan(addresses []string, otherPort string) bool {
1852 otherPortInt, err := strconv.Atoi(otherPort)
1853 if err != nil {
1854 return false
1855 }
1856 for _, lnAddr := range addresses {
1857 laddrs, err := caddy.ParseNetworkAddress(lnAddr)
1858 if err != nil {
1859 continue
1860 }
1861 if uint(otherPortInt) > laddrs.EndPort || uint(otherPortInt) < laddrs.StartPort {
1862 return true
1863 }
1864 }
1865 return false
1866 }
1867
1868 func mapContains[K comparable, V any](m map[K]V, keys []K) bool {
1869 if len(m) == 0 || len(keys) == 0 {
1870 return false
1871 }
1872 for _, key := range keys {
1873 if _, ok := m[key]; ok {
1874 return true
1875 }
1876 }
1877 return false
1878 }
1879
1880 // specificity returns len(s) minus any wildcards (*) and
1881 // placeholders ({...}). Basically, it's a length count
1882 // that penalizes the use of wildcards and placeholders.
1883 // This is useful for comparing hostnames and paths.
1884 // However, wildcards in paths are not a sure answer to
1885 // the question of specificity. For example,
1886 // '*.example.com' is clearly less specific than
1887 // 'a.example.com', but is '/a' more or less specific
1888 // than '/a*'?
1889 func specificity(s string) int {
1890 l := len(s) - strings.Count(s, "*")
1891 for len(s) > 0 {
1892 start := strings.Index(s, "{")
1893 if start < 0 {
1894 return l
1895 }
1896 end := strings.Index(s[start:], "}") + start + 1
1897 if end <= start {
1898 return l
1899 }
1900 l -= end - start
1901 s = s[end:]
1902 }
1903 return l
1904 }
1905
1906 type counter struct {
1907 n *int
1908 }
1909
1910 func (c counter) nextGroup() string {
1911 name := fmt.Sprintf("group%d", *c.n)
1912 *c.n++
1913 return name
1914 }
1915
1916 type namedCustomLog struct {
1917 name string
1918 hostnames []string
1919 log *caddy.CustomLog
1920 noHostname bool
1921 }
1922
1923 // addressWithProtocols associates a listen address with
1924 // the protocols to serve it with
1925 type addressWithProtocols struct {
1926 address string
1927 protocols []string
1928 }
1929
1930 // sbAddrAssociation is a mapping from a list of
1931 // addresses with protocols, and a list of server
1932 // blocks that are served on those addresses.
1933 type sbAddrAssociation struct {
1934 addressesWithProtocols []addressWithProtocols
1935 serverBlocks []serverBlock
1936 }
1937
1938 const (
1939 matcherPrefix = "@"
1940 namedRouteKey = "named_route"
1941 )
1942
1943 // Interface guard
1944 var _ caddyfile.ServerType = (*ServerType)(nil)