@marcel91200 / caddy Mirror · read-only / commits / 25d2b4bf
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 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 )