@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 199 lines 6.08 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 maphandler
16
17 import (
18 "fmt"
19 "net/http"
20 "regexp"
21 "slices"
22 "strings"
23
24 "github.com/caddyserver/caddy/v2"
25 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
26 )
27
28 func init() {
29 caddy.RegisterModule(Handler{})
30 }
31
32 // Handler implements a middleware that maps inputs to outputs. Specifically, it
33 // compares a source value against the map inputs, and for one that matches, it
34 // applies the output values to each destination. Destinations become placeholder
35 // names.
36 //
37 // Mapped placeholders are not evaluated until they are used, so even for very
38 // large mappings, this handler is quite efficient.
39 type Handler struct {
40 // Source is the placeholder from which to get the input value.
41 Source string `json:"source,omitempty"`
42
43 // Destinations are the names of placeholders in which to store the outputs.
44 // Destination values should be wrapped in braces, for example, {my_placeholder}.
45 Destinations []string `json:"destinations,omitempty"`
46
47 // Mappings from source values (inputs) to destination values (outputs).
48 // The first matching, non-nil mapping will be applied.
49 Mappings []Mapping `json:"mappings,omitempty"`
50
51 // If no mappings match or if the mapped output is null/nil, the associated
52 // default output will be applied (optional).
53 Defaults []string `json:"defaults,omitempty"`
54 }
55
56 // CaddyModule returns the Caddy module information.
57 func (Handler) CaddyModule() caddy.ModuleInfo {
58 return caddy.ModuleInfo{
59 ID: "http.handlers.map",
60 New: func() caddy.Module { return new(Handler) },
61 }
62 }
63
64 // Provision sets up h.
65 func (h *Handler) Provision(_ caddy.Context) error {
66 for j, dest := range h.Destinations {
67 if !strings.HasPrefix(dest, "{") || !strings.HasSuffix(dest, "}") ||
68 strings.Count(dest, "{") != 1 || strings.Count(dest, "}") != 1 ||
69 len(dest) < 3 {
70 return fmt.Errorf("destination %d must be a placeholder and only a placeholder, but got '%s'", j, dest)
71 }
72 h.Destinations[j] = dest[1 : len(dest)-1]
73 }
74
75 for i, m := range h.Mappings {
76 if m.InputRegexp == "" {
77 continue
78 }
79 var err error
80 h.Mappings[i].re, err = regexp.Compile(m.InputRegexp)
81 if err != nil {
82 return fmt.Errorf("compiling regexp for mapping %d: %v", i, err)
83 }
84 }
85
86 // TODO: improve efficiency even further by using an actual map type
87 // for the non-regexp mappings, OR sort them and do a binary search
88
89 return nil
90 }
91
92 // Validate ensures that h is configured properly.
93 func (h *Handler) Validate() error {
94 nDest, nDef := len(h.Destinations), len(h.Defaults)
95 if nDef > 0 && nDef != nDest {
96 return fmt.Errorf("%d destinations != %d defaults", nDest, nDef)
97 }
98
99 seenLiterals := make(map[string]int)
100 seenRegexps := make(map[string]int)
101 for i, m := range h.Mappings {
102 // prevent confusing/ambiguous mappings
103 if m.Input != "" && m.InputRegexp != "" {
104 return fmt.Errorf("mapping %d has both input and input_regexp fields specified, which is confusing", i)
105 }
106
107 // prevent duplicate mappings
108 input, seen := m.Input, seenLiterals
109 if m.InputRegexp != "" {
110 input, seen = m.InputRegexp, seenRegexps
111 }
112 if prev, ok := seen[input]; ok {
113 return fmt.Errorf("mapping %d has a duplicate input '%s' previously used with mapping %d", i, input, prev)
114 }
115 seen[input] = i
116
117 // ensure mappings have 1:1 output-to-destination correspondence
118 nOut := len(m.Outputs)
119 if nOut != nDest {
120 return fmt.Errorf("mapping %d has %d outputs but there are %d destinations defined", i, nOut, nDest)
121 }
122 }
123
124 return nil
125 }
126
127 func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
128 repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
129
130 // defer work until a variable is actually evaluated by using replacer's Map callback
131 repl.Map(func(key string) (any, bool) {
132 // return early if the variable is not even a configured destination
133 destIdx := slices.Index(h.Destinations, key)
134 if destIdx < 0 {
135 return nil, false
136 }
137
138 input := repl.ReplaceAll(h.Source, "")
139
140 // find the first mapping matching the input and return
141 // the requested destination/output value
142 for _, m := range h.Mappings {
143 output := m.Outputs[destIdx]
144 if output == nil {
145 continue
146 }
147 outputStr := caddy.ToString(output)
148
149 // evaluate regular expression if configured
150 if m.re != nil {
151 var result []byte
152 matches := m.re.FindStringSubmatchIndex(input)
153 if matches == nil {
154 continue
155 }
156 result = m.re.ExpandString(result, outputStr, input, matches)
157 return string(result), true
158 }
159
160 // otherwise simple string comparison
161 if input == m.Input {
162 return repl.ReplaceAll(outputStr, ""), true
163 }
164 }
165
166 // fall back to default if no match or if matched nil value
167 if len(h.Defaults) > destIdx {
168 return repl.ReplaceAll(h.Defaults[destIdx], ""), true
169 }
170
171 return nil, true
172 })
173
174 return next.ServeHTTP(w, r)
175 }
176
177 // Mapping describes a mapping from input to outputs.
178 type Mapping struct {
179 // The input value to match. Must be distinct from other mappings.
180 // Mutually exclusive to input_regexp.
181 Input string `json:"input,omitempty"`
182
183 // The input regular expression to match. Mutually exclusive to input.
184 InputRegexp string `json:"input_regexp,omitempty"`
185
186 // Upon a match with the input, each output is positionally correlated
187 // with each destination of the parent handler. An output that is null
188 // (nil) will be treated as if it was not mapped at all.
189 Outputs []any `json:"outputs,omitempty"`
190
191 re *regexp.Regexp
192 }
193
194 // Interface guards
195 var (
196 _ caddy.Provisioner = (*Handler)(nil)
197 _ caddy.Validator = (*Handler)(nil)
198 _ caddyhttp.MiddlewareHandler = (*Handler)(nil)
199 )