@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 686 lines 21.7 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 "encoding/json"
19 "maps"
20 "net"
21 "slices"
22 "sort"
23 "strconv"
24 "strings"
25
26 "github.com/caddyserver/caddy/v2"
27 "github.com/caddyserver/caddy/v2/caddyconfig"
28 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
29 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
30 )
31
32 // defaultDirectiveOrder specifies the default order
33 // to apply directives in HTTP routes. This must only
34 // consist of directives that are included in Caddy's
35 // standard distribution.
36 //
37 // e.g. The 'root' directive goes near the start in
38 // case rewrites or redirects depend on existence of
39 // files, i.e. the file matcher, which must know the
40 // root first.
41 //
42 // e.g. The 'header' directive goes before 'redir' so
43 // that headers can be manipulated before doing redirects.
44 //
45 // e.g. The 'respond' directive is near the end because it
46 // writes a response and terminates the middleware chain.
47 var defaultDirectiveOrder = []string{
48 "tracing",
49
50 // set variables that may be used by other directives
51 "map",
52 "vars",
53 "fs",
54 "root",
55 "log_append",
56 "skip_log", // TODO: deprecated, renamed to log_skip
57 "log_skip",
58 "log_name",
59 "log_level",
60
61 "header",
62 "copy_response_headers", // only in reverse_proxy's handle_response
63 "request_body",
64 "timeouts", // wraps the response writer, so keep it close to the real writer, ahead of encode/push/etc.
65
66 "redir",
67
68 // incoming request manipulation
69 "method",
70 "rewrite",
71 "uri",
72 "try_files",
73
74 // middleware handlers; some wrap responses
75 "basicauth", // TODO: deprecated, renamed to basic_auth
76 "basic_auth",
77 "forward_auth",
78 "request_header",
79 "encode",
80 "push",
81 "intercept",
82 "templates",
83
84 // special routing & dispatching directives
85 "invoke",
86 "handle",
87 "handle_path",
88 "route",
89
90 // handlers that typically respond to requests
91 "abort",
92 "error",
93 "copy_response", // only in reverse_proxy's handle_response
94 "respond",
95 "metrics",
96 "reverse_proxy",
97 "php_fastcgi",
98 "file_server",
99 "acme_server",
100 }
101
102 // directiveOrder specifies the order to apply directives
103 // in HTTP routes, after being modified by the plugins.
104 // It is only written during plugin registration, i.e.
105 // before any adaptation runs; adaptations only read it,
106 // since the user's "order" global option is kept per
107 // adaptation in options["order"] instead.
108 var directiveOrder = defaultDirectiveOrder
109
110 // directiveOrderFor returns the directive order to use for the
111 // adaptation these options belong to. That's whatever the "order"
112 // global option built up, or the plugin-registered order if the
113 // Caddyfile didn't use it.
114 func directiveOrderFor(options map[string]any) []string {
115 if order, ok := options["order"].([]string); ok {
116 return order
117 }
118 return directiveOrder
119 }
120
121 // RegisterDirective registers a unique directive dir with an
122 // associated unmarshaling (setup) function. When directive dir
123 // is encountered in a Caddyfile, setupFunc will be called to
124 // unmarshal its tokens.
125 func RegisterDirective(dir string, setupFunc UnmarshalFunc) {
126 if _, ok := registeredDirectives[dir]; ok {
127 panic("directive " + dir + " already registered")
128 }
129 registeredDirectives[dir] = setupFunc
130 }
131
132 // RegisterHandlerDirective is like RegisterDirective, but for
133 // directives which specifically output only an HTTP handler.
134 // Directives registered with this function will always have
135 // an optional matcher token as the first argument.
136 func RegisterHandlerDirective(dir string, setupFunc UnmarshalHandlerFunc) {
137 RegisterDirective(dir, func(h Helper) ([]ConfigValue, error) {
138 if !h.Next() {
139 return nil, h.ArgErr()
140 }
141
142 matcherSet, err := h.ExtractMatcherSet()
143 if err != nil {
144 return nil, err
145 }
146
147 val, err := setupFunc(h)
148 if err != nil {
149 return nil, err
150 }
151
152 return h.NewRoute(matcherSet, val), nil
153 })
154 }
155
156 // RegisterDirectiveOrder registers the default order for a
157 // directive from a plugin.
158 //
159 // This is useful when a plugin has a well-understood place
160 // it should run in the middleware pipeline, and it allows
161 // users to avoid having to define the order themselves.
162 //
163 // The directive dir may be placed in the position relative
164 // to ('before' or 'after') a directive included in Caddy's
165 // standard distribution. It cannot be relative to another
166 // plugin's directive.
167 //
168 // EXPERIMENTAL: This API may change or be removed.
169 func RegisterDirectiveOrder(dir string, position Positional, standardDir string) {
170 // check if directive was already ordered
171 if slices.Contains(directiveOrder, dir) {
172 panic("directive '" + dir + "' already ordered")
173 }
174
175 if position != Before && position != After {
176 panic("the 2nd argument must be either 'before' or 'after', got '" + position + "'")
177 }
178
179 // check if directive exists in standard distribution, since
180 // we can't allow plugins to depend on one another; we can't
181 // guarantee the order that plugins are loaded in.
182 foundStandardDir := slices.Contains(defaultDirectiveOrder, standardDir)
183 if !foundStandardDir {
184 panic("the 3rd argument '" + standardDir + "' must be a directive that exists in the standard distribution of Caddy")
185 }
186
187 // insert directive into proper position, into a copy so that we
188 // never write into the array backing defaultDirectiveOrder
189 newOrder := slices.Clone(directiveOrder)
190 if i := slices.Index(newOrder, standardDir); i >= 0 {
191 if position == After {
192 i++
193 }
194 newOrder = slices.Insert(newOrder, i, dir)
195 }
196 directiveOrder = newOrder
197 }
198
199 // RegisterGlobalOption registers a unique global option opt with
200 // an associated unmarshaling (setup) function. When the global
201 // option opt is encountered in a Caddyfile, setupFunc will be
202 // called to unmarshal its tokens.
203 func RegisterGlobalOption(opt string, setupFunc UnmarshalGlobalFunc) {
204 if _, ok := registeredGlobalOptions[opt]; ok {
205 panic("global option " + opt + " already registered")
206 }
207 registeredGlobalOptions[opt] = setupFunc
208 }
209
210 // Helper is a type which helps setup a value from
211 // Caddyfile tokens.
212 type Helper struct {
213 *caddyfile.Dispenser
214 // State stores intermediate variables during caddyfile adaptation.
215 State map[string]any
216 // BlockState stores intermediate variables scoped to the current block.
217 // It propagates down, but unlike state not back up from child to parent.
218 BlockState map[string]any
219 options map[string]any
220 warnings *[]caddyconfig.Warning
221 matcherDefs map[string]caddy.ModuleMap
222 parentBlock caddyfile.ServerBlock
223 groupCounter counter
224 }
225
226 // Option gets the option keyed by name.
227 func (h Helper) Option(name string) any {
228 return h.options[name]
229 }
230
231 // IsNamedRoute returns true if the directive is in a named route.
232 // Named routes are adapted before the site blocks, with their own
233 // State, so values a directive keeps in State aren't shared with
234 // the site blocks.
235 func (h Helper) IsNamedRoute() bool {
236 return h.parentBlock.IsNamedRoute
237 }
238
239 // Caddyfiles returns the list of config files from
240 // which tokens in the current server block were loaded.
241 func (h Helper) Caddyfiles() []string {
242 // first obtain set of names of files involved
243 // in this server block, without duplicates
244 files := make(map[string]struct{})
245 for _, segment := range h.parentBlock.Segments {
246 for _, token := range segment {
247 files[token.File] = struct{}{}
248 }
249 }
250 // then convert the set into a slice
251 filesSlice := make([]string, 0, len(files))
252 for file := range files {
253 filesSlice = append(filesSlice, file)
254 }
255 sort.Strings(filesSlice)
256 return filesSlice
257 }
258
259 // JSON converts val into JSON. Any errors are added to warnings.
260 func (h Helper) JSON(val any) json.RawMessage {
261 return caddyconfig.JSON(val, h.warnings)
262 }
263
264 // MatcherToken assumes the next argument token is (possibly) a matcher,
265 // and if so, returns the matcher set along with a true value. If the next
266 // token is not a matcher, nil and false is returned. Note that a true
267 // value may be returned with a nil matcher set if it is a catch-all.
268 func (h Helper) MatcherToken() (caddy.ModuleMap, bool, error) {
269 if !h.NextArg() {
270 return nil, false, nil
271 }
272 return matcherSetFromMatcherToken(h.Dispenser.Token(), h.matcherDefs, h.warnings)
273 }
274
275 // ExtractMatcherSet is like MatcherToken, except this is a higher-level
276 // method that returns the matcher set described by the matcher token,
277 // or nil if there is none, and deletes the matcher token from the
278 // dispenser and resets it as if this look-ahead never happened. Useful
279 // when wrapping a route (one or more handlers) in a user-defined matcher.
280 func (h Helper) ExtractMatcherSet() (caddy.ModuleMap, error) {
281 matcherSet, hasMatcher, err := h.MatcherToken()
282 if err != nil {
283 return nil, err
284 }
285 if hasMatcher {
286 // strip matcher token; we don't need to
287 // use the return value here because a
288 // new dispenser should have been made
289 // solely for this directive's tokens,
290 // with no other uses of same slice
291 h.Dispenser.Delete()
292 }
293 h.Dispenser.Reset() // pretend this lookahead never happened
294 return matcherSet, nil
295 }
296
297 // NewRoute returns config values relevant to creating a new HTTP route.
298 func (h Helper) NewRoute(matcherSet caddy.ModuleMap,
299 handler caddyhttp.MiddlewareHandler,
300 ) []ConfigValue {
301 mod, err := caddy.GetModule(caddy.GetModuleID(handler))
302 if err != nil {
303 *h.warnings = append(*h.warnings, caddyconfig.Warning{
304 File: h.File(),
305 Line: h.Line(),
306 Message: err.Error(),
307 })
308 return nil
309 }
310 var matcherSetsRaw []caddy.ModuleMap
311 if matcherSet != nil {
312 matcherSetsRaw = append(matcherSetsRaw, matcherSet)
313 }
314 return []ConfigValue{
315 {
316 Class: "route",
317 Value: caddyhttp.Route{
318 MatcherSetsRaw: matcherSetsRaw,
319 HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(handler, "handler", mod.ID.Name(), h.warnings)},
320 },
321 },
322 }
323 }
324
325 // GroupRoutes adds the routes (caddyhttp.Route type) in vals to the
326 // same group, if there is more than one route in vals.
327 func (h Helper) GroupRoutes(vals []ConfigValue) {
328 // ensure there's at least two routes; group of one is pointless
329 var count int
330 for _, v := range vals {
331 if _, ok := v.Value.(caddyhttp.Route); ok {
332 count++
333 if count > 1 {
334 break
335 }
336 }
337 }
338 if count < 2 {
339 return
340 }
341
342 // now that we know the group will have some effect, do it
343 groupName := h.groupCounter.nextGroup()
344 for i := range vals {
345 if route, ok := vals[i].Value.(caddyhttp.Route); ok {
346 route.Group = groupName
347 vals[i].Value = route
348 }
349 }
350 }
351
352 // WithDispenser returns a new instance based on d. All others Helper
353 // fields are copied, so typically maps are shared with this new instance.
354 func (h Helper) WithDispenser(d *caddyfile.Dispenser) Helper {
355 h.Dispenser = d
356 return h
357 }
358
359 // ParseSegmentAsSubroute parses the segment such that its subdirectives
360 // are themselves treated as directives, from which a subroute is built
361 // and returned.
362 func ParseSegmentAsSubroute(h Helper) (caddyhttp.MiddlewareHandler, error) {
363 allResults, err := parseSegmentAsConfig(h)
364 if err != nil {
365 return nil, err
366 }
367
368 return buildSubroute(allResults, h.groupCounter, directiveOrderFor(h.options))
369 }
370
371 // parseSegmentAsConfig parses the segment such that its subdirectives
372 // are themselves treated as directives, including named matcher definitions,
373 // and the raw Config structs are returned.
374 func parseSegmentAsConfig(h Helper) ([]ConfigValue, error) {
375 var allResults []ConfigValue
376
377 for h.Next() {
378 // don't allow non-matcher args on the first line
379 if h.NextArg() {
380 return nil, h.ArgErr()
381 }
382
383 // slice the linear list of tokens into top-level segments
384 var segments []caddyfile.Segment
385 for nesting := h.Nesting(); h.NextBlock(nesting); {
386 segments = append(segments, h.NextSegment())
387 }
388
389 // copy existing matcher definitions so we can augment
390 // new ones that are defined only in this scope
391 matcherDefs := make(map[string]caddy.ModuleMap, len(h.matcherDefs))
392 maps.Copy(matcherDefs, h.matcherDefs)
393
394 // find and extract any embedded matcher definitions in this scope
395 for i := 0; i < len(segments); i++ {
396 seg := segments[i]
397 if strings.HasPrefix(seg.Directive(), matcherPrefix) {
398 // parse, then add the matcher to matcherDefs
399 err := parseMatcherDefinitions(caddyfile.NewDispenser(seg), matcherDefs)
400 if err != nil {
401 return nil, err
402 }
403 // remove the matcher segment (consumed), then step back the loop
404 segments = append(segments[:i], segments[i+1:]...)
405 i--
406 }
407 }
408
409 // clone BlockState once for the entire block so sibling directives
410 // can share state, but changes don't leak to the parent scope
411 subBlockState := make(map[string]any, len(h.BlockState))
412 maps.Copy(subBlockState, h.BlockState)
413
414 // with matchers ready to go, evaluate each directive's segment
415 for _, seg := range segments {
416 dir := seg.Directive()
417 dirFunc, ok := registeredDirectives[dir]
418 if !ok {
419 return nil, h.Errf("unrecognized directive: %s - are you sure your Caddyfile structure (nesting and braces) is correct?", dir)
420 }
421
422 subHelper := h
423 subHelper.Dispenser = caddyfile.NewDispenser(seg)
424 subHelper.matcherDefs = matcherDefs
425 subHelper.BlockState = subBlockState
426
427 results, err := dirFunc(subHelper)
428 if err != nil {
429 return nil, h.Errf("parsing caddyfile tokens for '%s': %v", dir, err)
430 }
431
432 dir = normalizeDirectiveName(dir)
433
434 for _, result := range results {
435 result.directive = dir
436 allResults = append(allResults, result)
437 }
438 }
439 }
440
441 return allResults, nil
442 }
443
444 // ConfigValue represents a value to be added to the final
445 // configuration, or a value to be consulted when building
446 // the final configuration.
447 type ConfigValue struct {
448 // The kind of value this is. As the config is
449 // being built, the adapter will look in the
450 // "pile" for values belonging to a certain
451 // class when it is setting up a certain part
452 // of the config. The associated value will be
453 // type-asserted and placed accordingly.
454 Class string
455
456 // The value to be used when building the config.
457 // Generally its type is associated with the
458 // name of the Class.
459 Value any
460
461 directive string
462 }
463
464 func sortRoutes(routes []ConfigValue, order []string) {
465 dirPositions := make(map[string]int)
466 for i, dir := range order {
467 dirPositions[dir] = i
468 }
469
470 sort.SliceStable(routes, func(i, j int) bool {
471 // if the directives are different, just use the established directive order
472 iDir, jDir := routes[i].directive, routes[j].directive
473 if iDir != jDir {
474 return dirPositions[iDir] < dirPositions[jDir]
475 }
476
477 // some directives involve setting values which can overwrite
478 // each other, so it makes most sense to reverse the order so
479 // that the least-specific matcher is first, allowing the last
480 // matching one to win; we swap i and j instead of using !,
481 // so matchers that can't be compared keep their order
482 if iDir == "vars" || iDir == "log_level" {
483 i, j = j, i
484 }
485
486 // directives are the same; sub-sort by path matcher length if there's
487 // only one matcher set and one path (this is a very common case and
488 // usually -- but not always -- helpful/expected, oh well; user can
489 // always take manual control of order using handler or route blocks)
490 iRoute, ok := routes[i].Value.(caddyhttp.Route)
491 if !ok {
492 return false
493 }
494 jRoute, ok := routes[j].Value.(caddyhttp.Route)
495 if !ok {
496 return false
497 }
498
499 // decode the path matchers if there is just one matcher set
500 var iPM, jPM caddyhttp.MatchPath
501 if len(iRoute.MatcherSetsRaw) == 1 {
502 _ = json.Unmarshal(iRoute.MatcherSetsRaw[0]["path"], &iPM)
503 }
504 if len(jRoute.MatcherSetsRaw) == 1 {
505 _ = json.Unmarshal(jRoute.MatcherSetsRaw[0]["path"], &jPM)
506 }
507
508 // if there is only one path in the path matcher, sort by longer path
509 // (more specific) first; missing path matchers or multi-matchers are
510 // treated as zero-length paths
511 var iPathLen, jPathLen int
512 if len(iPM) == 1 {
513 iPathLen = len(iPM[0])
514 }
515 if len(jPM) == 1 {
516 jPathLen = len(jPM[0])
517 }
518
519 sortByPath := func() bool {
520 // we can only confidently compare path lengths if both
521 // directives have a single path to match (issue #5037)
522 if iPathLen > 0 && jPathLen > 0 {
523 // trim the trailing wildcard if there is one
524 iPathTrimmed := strings.TrimSuffix(iPM[0], "*")
525 jPathTrimmed := strings.TrimSuffix(jPM[0], "*")
526
527 // if both paths are the same except for a trailing wildcard,
528 // sort by the shorter path first (which is more specific)
529 if iPathTrimmed == jPathTrimmed {
530 return iPathLen < jPathLen
531 }
532
533 // we use the trimmed length to compare the paths
534 // https://github.com/caddyserver/caddy/issues/7012#issuecomment-2870142195
535 // credit to https://github.com/Hellio404
536 // for sorts with many items, mixing matchers w/ and w/o wildcards will confuse the sort and result in incorrect orders
537 iPathLen = len(iPathTrimmed)
538 jPathLen = len(jPathTrimmed)
539
540 // if both paths have the same length, sort lexically
541 // https://github.com/caddyserver/caddy/pull/7015#issuecomment-2871993588
542 if iPathLen == jPathLen {
543 return iPathTrimmed < jPathTrimmed
544 }
545
546 // sort most-specific (longest) path first
547 return iPathLen > jPathLen
548 }
549
550 // if both directives don't have a single path to compare,
551 // sort whichever one has a matcher first; if both have
552 // a matcher, sort equally (stable sort preserves order)
553 return len(iRoute.MatcherSetsRaw) > 0 && len(jRoute.MatcherSetsRaw) == 0
554 }()
555
556 // most-specific matcher first (or last for vars and log_level, see above)
557 return sortByPath
558 })
559 }
560
561 // serverBlock pairs a Caddyfile server block with
562 // a "pile" of config values, keyed by class name,
563 // as well as its parsed keys for convenience.
564 type serverBlock struct {
565 block caddyfile.ServerBlock
566 pile map[string][]ConfigValue // config values obtained from directives
567 parsedKeys []Address
568 }
569
570 // hostsFromKeys returns a list of all the non-empty hostnames found in
571 // the keys of the server block sb. If logger mode is false, a key with
572 // an empty hostname portion will return an empty slice, since that
573 // server block is interpreted to effectively match all hosts. An empty
574 // string is never added to the slice.
575 //
576 // If loggerMode is true, then the non-standard ports of keys will be
577 // joined to the hostnames. This is to effectively match the Host
578 // header of requests that come in for that key.
579 //
580 // The resulting slice is not sorted but will never have duplicates.
581 func (sb serverBlock) hostsFromKeys(loggerMode bool) []string {
582 // ensure each entry in our list is unique
583 hostMap := make(map[string]struct{})
584 for _, addr := range sb.parsedKeys {
585 if addr.Host == "" {
586 if !loggerMode {
587 // server block contains a key like ":443", i.e. the host portion
588 // is empty / catch-all, which means to match all hosts
589 return []string{}
590 }
591 // never append an empty string
592 continue
593 }
594 if loggerMode &&
595 addr.Port != "" &&
596 addr.Port != strconv.Itoa(caddyhttp.DefaultHTTPPort) &&
597 addr.Port != strconv.Itoa(caddyhttp.DefaultHTTPSPort) {
598 hostMap[net.JoinHostPort(addr.Host, addr.Port)] = struct{}{}
599 } else {
600 hostMap[addr.Host] = struct{}{}
601 }
602 }
603
604 // convert map to slice
605 sblockHosts := make([]string, 0, len(hostMap))
606 for host := range hostMap {
607 sblockHosts = append(sblockHosts, host)
608 }
609
610 return sblockHosts
611 }
612
613 func (sb serverBlock) hostsFromKeysNotHTTP(httpPort string) []string {
614 // ensure each entry in our list is unique
615 hostMap := make(map[string]struct{})
616 for _, addr := range sb.parsedKeys {
617 if addr.Host == "" {
618 continue
619 }
620 if addr.Scheme != "http" && addr.Port != httpPort {
621 hostMap[addr.Host] = struct{}{}
622 }
623 }
624
625 // convert map to slice
626 sblockHosts := make([]string, 0, len(hostMap))
627 for host := range hostMap {
628 sblockHosts = append(sblockHosts, host)
629 }
630
631 return sblockHosts
632 }
633
634 // hasHostCatchAllKey returns true if sb has a key that
635 // omits a host portion, i.e. it "catches all" hosts.
636 func (sb serverBlock) hasHostCatchAllKey() bool {
637 return slices.ContainsFunc(sb.parsedKeys, func(addr Address) bool {
638 return addr.Host == ""
639 })
640 }
641
642 // isAllHTTP returns true if all sb keys explicitly specify
643 // the http:// scheme
644 func (sb serverBlock) isAllHTTP() bool {
645 return !slices.ContainsFunc(sb.parsedKeys, func(addr Address) bool {
646 return addr.Scheme != "http"
647 })
648 }
649
650 // Positional are the supported modes for ordering directives.
651 type Positional string
652
653 const (
654 Before Positional = "before"
655 After Positional = "after"
656 First Positional = "first"
657 Last Positional = "last"
658 )
659
660 type (
661 // UnmarshalFunc is a function which can unmarshal Caddyfile
662 // tokens into zero or more config values using a Helper type.
663 // These are passed in a call to RegisterDirective.
664 UnmarshalFunc func(h Helper) ([]ConfigValue, error)
665
666 // UnmarshalHandlerFunc is like UnmarshalFunc, except the
667 // output of the unmarshaling is an HTTP handler. This
668 // function does not need to deal with HTTP request matching
669 // which is abstracted away. Since writing HTTP handlers
670 // with Caddyfile support is very common, this is a more
671 // convenient way to add a handler to the chain since a lot
672 // of the details common to HTTP handlers are taken care of
673 // for you. These are passed to a call to
674 // RegisterHandlerDirective.
675 UnmarshalHandlerFunc func(h Helper) (caddyhttp.MiddlewareHandler, error)
676
677 // UnmarshalGlobalFunc is a function which can unmarshal Caddyfile
678 // tokens from a global option. It is passed the tokens to parse and
679 // existing value from the previous instance of this global option
680 // (if any). It returns the value to associate with this global option.
681 UnmarshalGlobalFunc func(d *caddyfile.Dispenser, existingVal any) (any, error)
682 )
683
684 var registeredDirectives = make(map[string]UnmarshalFunc)
685
686 var registeredGlobalOptions = make(map[string]UnmarshalGlobalFunc)