Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 14, 05:15 UTC
map: Reimplement; multiple outputs; optimize
Matthew Holt committed
Oct 2, 2020 at 14:23 UTC
25d2b4bf2927bf69ddce582d33339ef7d13cb6bf
6 files changed
+245
-146
caddyconfig/httpcaddyfile/builtins.go
+2
-2
@@ -75,10 +75,10 @@ func parseBind(h Helper) ([]ConfigValue, error) {
75
// load <paths...>
76
// ca <acme_ca_endpoint>
77
// ca_root <pem_file>
78
-// dns <provider_name>
78
+// dns <provider_name> [...]
79
// on_demand
80
// eab <key_id> <mac_key>
81
-// issuer <module_name> ...
81
+// issuer <module_name> [...]
82
// }
83
//
84
func parseTLS(h Helper) ([]ConfigValue, error) {
caddytest/caddytest.go
+1
-1
@@ -430,7 +430,7 @@ func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expe
430
431
body := string(bytes)
432
433
- if !strings.Contains(body, expectedBody) {
433
+ if body != expectedBody {
434
tc.t.Errorf("requesting \"%s\" expected response body \"%s\" but got \"%s\"", req.RequestURI, expectedBody, body)
435
}
436
caddytest/integration/map_test.go
+62
-69
@@ -8,7 +8,6 @@ import (
8
)
9
10
func TestMap(t *testing.T) {
11
-
11
// arrange
12
tester := caddytest.NewTester(t)
13
tester.InitServer(`{
@@ -18,25 +17,24 @@ func TestMap(t *testing.T) {
17
18
localhost:9080 {
19
21
- map http.request.method dest-name {
20
+ map {http.request.method} {dest-1} {dest-2} {
21
default unknown
23
- G.T get-called
24
- POST post-called
22
+ ~G.T get-called
23
+ POST post-called foobar
24
}
25
26
respond /version 200 {
28
- body "hello from localhost {dest-name}"
27
+ body "hello from localhost {dest-1} {dest-2}"
28
}
29
}
30
`, "caddyfile")
31
32
// act and assert
34
- tester.AssertGetResponse("http://localhost:9080/version", 200, "hello from localhost get-called")
35
- tester.AssertPostResponseBody("http://localhost:9080/version", []string{}, bytes.NewBuffer([]byte{}), 200, "hello from localhost post-called")
33
+ tester.AssertGetResponse("http://localhost:9080/version", 200, "hello from localhost get-called ")
34
+ tester.AssertPostResponseBody("http://localhost:9080/version", []string{}, bytes.NewBuffer([]byte{}), 200, "hello from localhost post-called foobar")
35
}
36
37
func TestMapRespondWithDefault(t *testing.T) {
39
-
38
// arrange
39
tester := caddytest.NewTester(t)
40
tester.InitServer(`{
@@ -46,9 +44,9 @@ func TestMapRespondWithDefault(t *testing.T) {
44
45
localhost:9080 {
46
49
- map http.request.method dest-name {
47
+ map {http.request.method} {dest-name} {
48
default unknown
51
- GET get-called
49
+ GET get-called
50
}
51
52
respond /version 200 {
@@ -63,80 +61,75 @@ func TestMapRespondWithDefault(t *testing.T) {
61
}
62
63
func TestMapAsJson(t *testing.T) {
66
-
64
// arrange
65
tester := caddytest.NewTester(t)
69
- tester.InitServer(`{
66
+ tester.InitServer(`
67
+ {
68
"apps": {
69
"http": {
72
- "http_port": 9080,
73
- "https_port": 9443,
74
- "servers": {
75
- "srv0": {
76
- "listen": [
77
- ":9080"
78
- ],
79
- "routes": [
80
- {
81
- "handle": [
82
- {
83
- "handler": "subroute",
70
+ "http_port": 9080,
71
+ "https_port": 9443,
72
+ "servers": {
73
+ "srv0": {
74
+ "listen": [
75
+ ":9080"
76
+ ],
77
"routes": [
78
{
86
- "handle": [
87
- {
88
- "handler": "map",
89
- "source": "http.request.method",
90
- "destination": "dest-name",
91
- "default": "unknown",
92
- "items": [
79
+ "handle": [
80
{
94
- "expression": "GET",
95
- "value": "get-called"
96
- },
81
+ "handler": "subroute",
82
+ "routes": [
83
+ {
84
+ "handle": [
85
+ {
86
+ "handler": "map",
87
+ "source": "{http.request.method}",
88
+ "destinations": ["dest-name"],
89
+ "defaults": ["unknown"],
90
+ "mappings": [
91
+ {
92
+ "input": "GET",
93
+ "outputs": ["get-called"]
94
+ },
95
+ {
96
+ "input": "POST",
97
+ "outputs": ["post-called"]
98
+ }
99
+ ]
100
+ }
101
+ ]
102
+ },
103
+ {
104
+ "handle": [
105
+ {
106
+ "body": "hello from localhost {dest-name}",
107
+ "handler": "static_response",
108
+ "status_code": 200
109
+ }
110
+ ],
111
+ "match": [
112
+ {
113
+ "path": ["/version"]
114
+ }
115
+ ]
116
+ }
117
+ ]
118
+ }
119
+ ],
120
+ "match": [
121
{
98
- "expression": "POST",
99
- "value": "post-called"
122
+ "host": ["localhost"]
123
}
101
- ]
102
- }
103
- ]
104
- },
105
- {
106
- "handle": [
107
- {
108
- "body": "hello from localhost {dest-name}",
109
- "handler": "static_response",
110
- "status_code": 200
111
- }
112
- ],
113
- "match": [
114
- {
115
- "path": [
116
- "/version"
117
- ]
118
- }
119
- ]
124
+ ],
125
+ "terminal": true
126
}
127
]
122
- }
123
- ],
124
- "match": [
125
- {
126
- "host": [
127
- "localhost"
128
- ]
129
- }
130
- ],
131
- "terminal": true
128
}
133
- ]
129
}
130
}
136
- }
137
- }
131
}
139
- `, "json")
132
+ }`, "json")
133
134
tester.AssertGetResponse("http://localhost:9080/version", 200, "hello from localhost get-called")
135
tester.AssertPostResponseBody("http://localhost:9080/version", []string{}, bytes.NewBuffer([]byte{}), 200, "hello from localhost post-called")
caddytest/integration/sni_test.go
+2
-3
@@ -99,7 +99,7 @@ func TestDefaultSNI(t *testing.T) {
99
100
// act and assert
101
// makes a request with no sni
102
- tester.AssertGetResponse("https://127.0.0.1:9443/version", 200, "hello from a")
102
+ tester.AssertGetResponse("https://127.0.0.1:9443/version", 200, "hello from a.caddy.localhost")
103
}
104
105
func TestDefaultSNIWithNamedHostAndExplicitIP(t *testing.T) {
@@ -204,7 +204,6 @@ func TestDefaultSNIWithNamedHostAndExplicitIP(t *testing.T) {
204
}
205
206
func TestDefaultSNIWithPortMappingOnly(t *testing.T) {
207
-
207
// arrange
208
tester := caddytest.NewTester(t)
209
tester.InitServer(`
@@ -273,7 +272,7 @@ func TestDefaultSNIWithPortMappingOnly(t *testing.T) {
272
273
// act and assert
274
// makes a request with no sni
276
- tester.AssertGetResponse("https://127.0.0.1:9443/version", 200, "hello from a")
275
+ tester.AssertGetResponse("https://127.0.0.1:9443/version", 200, "hello from a.caddy.localhost")
276
}
277
278
func TestHttpOnlyOnDomainWithSNI(t *testing.T) {
modules/caddyhttp/map/caddyfile.go
+57
-29
@@ -15,6 +15,8 @@
15
package maphandler
16
17
import (
18
+ "strings"
19
+
20
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
21
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
22
)
@@ -23,49 +25,75 @@ func init() {
25
httpcaddyfile.RegisterHandlerDirective("map", parseCaddyfile)
26
}
27
26
-// parseCaddyfile sets up the handler for a map from Caddyfile tokens. Syntax:
28
+// parseCaddyfile sets up the map handler from Caddyfile tokens. Syntax:
29
//
28
-// map <source> <dest> {
29
-// [default <default>] - used if not match is found
30
-// [<regexp> <replacement>] - regular expression to match against the source find and the matching replacement value
31
-// ...
30
+// map [<matcher>] <source> <destinations...> {
31
+// [~]<input> <outputs...>
32
+// default <defaults...>
33
// }
34
//
34
-// The map takes a source variable and maps it into the dest variable. The mapping process
35
-// will check the source variable for the first successful match against a list of regular expressions.
36
-// If a successful match is found the dest variable will contain the replacement value.
37
-// If no successful match is found and the default is specified then the dest will contain the default value.
35
+// If the input value is prefixed with a tilde (~), then the input will be parsed as a
36
+// regular expression.
37
//
38
+// The number of outputs for each mapping must not be more than the number of destinations.
39
+// However, for convenience, there may be fewer outputs than destinations and any missing
40
+// outputs will be filled in implicitly.
41
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
40
- m := new(Handler)
42
+ var handler Handler
43
44
for h.Next() {
43
- // first see if source and dest are configured
44
- if h.NextArg() {
45
- m.Source = h.Val()
46
- if h.NextArg() {
47
- m.Destination = h.Val()
48
- }
45
+ // source
46
+ if !h.NextArg() {
47
+ return nil, h.ArgErr()
48
+ }
49
+ handler.Source = h.Val()
50
+
51
+ // destinations
52
+ handler.Destinations = h.RemainingArgs()
53
+ if len(handler.Destinations) == 0 {
54
+ return nil, h.Err("missing destination argument(s)")
55
}
56
51
- // load the rules
57
+ // mappings
58
for h.NextBlock(0) {
53
- expression := h.Val()
54
- if expression == "default" {
55
- args := h.RemainingArgs()
56
- if len(args) != 1 {
57
- return m, h.ArgErr()
59
+ // defaults are a special case
60
+ if h.Val() == "default" {
61
+ if len(handler.Defaults) > 0 {
62
+ return nil, h.Err("defaults already defined")
63
}
59
- m.Default = args[0]
60
- } else {
61
- args := h.RemainingArgs()
62
- if len(args) != 1 {
63
- return m, h.ArgErr()
64
+ handler.Defaults = h.RemainingArgs()
65
+ for len(handler.Defaults) < len(handler.Destinations) {
66
+ handler.Defaults = append(handler.Defaults, "")
67
}
65
- m.Items = append(m.Items, Item{Expression: expression, Value: args[0]})
68
+ continue
69
+ }
70
+
71
+ // every other line maps one input to one or more outputs
72
+ in := h.Val()
73
+ outs := h.RemainingArgs()
74
+
75
+ // cannot have more outputs than destinations
76
+ if len(outs) > len(handler.Destinations) {
77
+ return nil, h.Err("too many outputs")
78
+ }
79
+
80
+ // for convenience, can have fewer outputs than destinations, but the
81
+ // underlying handler won't accept that, so we fill in empty values
82
+ for len(outs) < len(handler.Destinations) {
83
+ outs = append(outs, "")
84
}
85
+
86
+ // create the mapping
87
+ mapping := Mapping{Outputs: outs}
88
+ if strings.HasPrefix(in, "~") {
89
+ mapping.InputRegexp = in[1:]
90
+ } else {
91
+ mapping.Input = in
92
+ }
93
+
94
+ handler.Mappings = append(handler.Mappings, mapping)
95
}
96
}
97
70
- return m, nil
98
+ return handler, nil
99
}
modules/caddyhttp/map/map.go
+121
-42
@@ -15,8 +15,11 @@
15
package maphandler
16
17
import (
18
+ "fmt"
19
+ "log"
20
"net/http"
21
"regexp"
22
+ "strings"
23
24
"github.com/caddyserver/caddy/v2"
25
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
@@ -26,27 +29,26 @@ func init() {
29
caddy.RegisterModule(Handler{})
30
}
31
29
-// Handler is a middleware that maps a source placeholder to a destination
30
-// placeholder.
31
-//
32
-// The mapping process happens early in the request handling lifecycle so that
33
-// the Destination placeholder is calculated and available for substitution.
34
-// The Items array contains pairs of regex expressions and values, the
35
-// Source is matched against the expression, if they match then the destination
36
-// placeholder is set to the value.
37
-//
38
-// The Default is optional, if no Item expression is matched then the value of
39
-// the Default will be used.
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 {
42
- // Source is a placeholder
40
+ // Source is the placeholder from which to get the input value.
41
Source string `json:"source,omitempty"`
44
- // Destination is a new placeholder
45
- Destination string `json:"destination,omitempty"`
46
- // Default is an optional value to use if no other was found
47
- Default string `json:"default,omitempty"`
48
- // Items is an array of regex expressions and values
49
- Items []Item `json:"items,omitempty"`
42
+
43
+ // Destinations are the placeholders in which to store the outputs.
44
+ Destinations []string `json:"destinations,omitempty"`
45
+
46
+ // Mappings from source values (inputs) to destination values (outputs).
47
+ // The first matching mapping will be applied.
48
+ Mappings []Mapping `json:"mappings,omitempty"`
49
+
50
+ // If no mappings match, the default value will be applied (optional).
51
+ Defaults []string
52
}
53
54
// CaddyModule returns the Caddy module information.
@@ -57,10 +59,52 @@ func (Handler) CaddyModule() caddy.ModuleInfo {
59
}
60
}
61
60
-// Provision will compile all regular expressions
62
+// Provision sets up h.
63
func (h *Handler) Provision(_ caddy.Context) error {
62
- for i := 0; i < len(h.Items); i++ {
63
- h.Items[i].compiled = regexp.MustCompile(h.Items[i].Expression)
64
+ for j, dest := range h.Destinations {
65
+ h.Destinations[j] = strings.Trim(dest, "{}")
66
+ }
67
+
68
+ for i, m := range h.Mappings {
69
+ if m.InputRegexp == "" {
70
+ continue
71
+ }
72
+ if m.Input != "" {
73
+ return fmt.Errorf("mapping %d has both input and input_regexp fields specified, which is confusing", i)
74
+ }
75
+ var err error
76
+ h.Mappings[i].re, err = regexp.Compile(m.InputRegexp)
77
+ if err != nil {
78
+ return fmt.Errorf("compiling regexp for mapping %d: %v", i, err)
79
+ }
80
+ }
81
+
82
+ // TODO: improve efficiency even further by using an actual map type
83
+ // for the non-regexp mappings, OR sort them and do a binary search
84
+
85
+ return nil
86
+}
87
+
88
+// Validate ensures that h is configured properly.
89
+func (h *Handler) Validate() error {
90
+ nDest, nDef := len(h.Destinations), len(h.Defaults)
91
+ if nDef > 0 && nDef != nDest {
92
+ return fmt.Errorf("%d destinations != %d defaults", nDest, nDef)
93
+ }
94
+
95
+ seen := make(map[string]int)
96
+ for i, m := range h.Mappings {
97
+ // prevent duplicate mappings
98
+ if prev, ok := seen[m.Input]; ok {
99
+ return fmt.Errorf("mapping %d has a duplicate input '%s' previously used with mapping %d", i, m.Input, prev)
100
+ }
101
+ seen[m.Input] = i
102
+
103
+ // ensure mappings have 1:1 output-to-destination correspondence
104
+ nOut := len(m.Outputs)
105
+ if nOut != nDest {
106
+ return fmt.Errorf("mapping %d has %d outputs but there are %d destinations defined", i, nOut, nDest)
107
+ }
108
}
109
return nil
110
}
@@ -68,38 +112,73 @@ func (h *Handler) Provision(_ caddy.Context) error {
112
func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
113
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
114
71
- // get the source value, if the source value was not found do no
72
- // replacement.
73
- val, ok := repl.GetString(h.Source)
74
- if ok {
75
- found := false
76
- for i := 0; i < len(h.Items); i++ {
77
- if h.Items[i].compiled.MatchString(val) {
78
- found = true
79
- repl.Set(h.Destination, h.Items[i].Value)
80
- break
115
+ // defer work until a variable is actually evaluated by using replacer's Map callback
116
+ repl.Map(func(key string) (interface{}, bool) {
117
+ // return early if the variable is not even a configured destination
118
+ destIdx := h.destinationIndex(key)
119
+ if destIdx < 0 {
120
+ return nil, false
121
+ }
122
+
123
+ input := repl.ReplaceAll(h.Source, "")
124
+
125
+ // find the first mapping matching the input and return
126
+ // the requested destination/output value
127
+ for _, m := range h.Mappings {
128
+ log.Printf("MAPPING: %+v", m)
129
+ if m.re != nil {
130
+ if m.re.MatchString(input) {
131
+ return m.Outputs[destIdx], true
132
+ }
133
+ continue
134
+ }
135
+ if input == m.Input {
136
+ log.Printf("RETURNING: %s", m.Outputs[destIdx])
137
+ return m.Outputs[destIdx], true
138
}
139
}
140
84
- if !found && h.Default != "" {
85
- repl.Set(h.Destination, h.Default)
141
+ // fall back to default if no match
142
+ if len(h.Defaults) > destIdx {
143
+ return h.Defaults[destIdx], true
144
}
87
- }
145
+
146
+ return nil, true
147
+ })
148
+
149
return next.ServeHTTP(w, r)
150
}
151
91
-// Item defines each entry in the map
92
-type Item struct {
93
- // Expression is the regular expression searched for
94
- Expression string `json:"expression,omitempty"`
95
- // Value to use once the expression has been found
96
- Value string `json:"value,omitempty"`
97
- // compiled expression, internal use
98
- compiled *regexp.Regexp
152
+// destinationIndex returns the positional index of the destination
153
+// is name is a known destination; otherwise it returns -1.
154
+func (h Handler) destinationIndex(name string) int {
155
+ for i, dest := range h.Destinations {
156
+ if dest == name {
157
+ return i
158
+ }
159
+ }
160
+ return -1
161
+}
162
+
163
+// Mapping describes a mapping from input to outputs.
164
+type Mapping struct {
165
+ // The input value to match. Must be distinct from other mappings.
166
+ // Mutually exclusive to input_regexp.
167
+ Input string `json:"input,omitempty"`
168
+
169
+ // The input regular expression to match. Mutually exclusive to input.
170
+ InputRegexp string `json:"input_regexp,omitempty"`
171
+
172
+ // Upon a match with the input, each output is positionally correlated
173
+ // with each destination of the parent handler.
174
+ Outputs []string `json:"outputs,omitempty"`
175
+
176
+ re *regexp.Regexp
177
}
178
179
// Interface guards
180
var (
181
_ caddy.Provisioner = (*Handler)(nil)
182
+ _ caddy.Validator = (*Handler)(nil)
183
_ caddyhttp.MiddlewareHandler = (*Handler)(nil)
184
)