@marcel91200 / caddy Mirror · read-only / commits / 6004d3f7
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

caddyhttp: Add 'map' handler (#3199)

* inital map implementation * resolve the value during middleware execution * use regex instead * pr feedback * renamed mmap to maphandler * refactored GetString implementation * fixed mispelling * additional feedback

Mark Sargent committed Jun 27, 2020 at 09:12 UTC 6004d3f779b8175d92d8eb7819ed800e8eddbff6
6 files changed +328
caddyconfig/httpcaddyfile/directives.go
+1
@@ -37,6 +37,7 @@ import (
37 // The header directive goes second so that headers
38 // can be manipulated before doing redirects.
39 var directiveOrder = []string{
40 + "map",
41 "root",
42
43 "header",
caddytest/integration/map_test.go new
+143
@@ -0,0 +1,143 @@
1 +package integration
2 +
3 +import (
4 + "bytes"
5 + "testing"
6 +
7 + "github.com/caddyserver/caddy/v2/caddytest"
8 +)
9 +
10 +func TestMap(t *testing.T) {
11 +
12 + // arrange
13 + tester := caddytest.NewTester(t)
14 + tester.InitServer(`{
15 + http_port 9080
16 + https_port 9443
17 + }
18 +
19 + localhost:9080 {
20 +
21 + map http.request.method dest-name {
22 + default unknown
23 + G.T get-called
24 + POST post-called
25 + }
26 +
27 + respond /version 200 {
28 + body "hello from localhost {dest-name}"
29 + }
30 + }
31 + `, "caddyfile")
32 +
33 + // 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")
36 +}
37 +
38 +func TestMapRespondWithDefault(t *testing.T) {
39 +
40 + // arrange
41 + tester := caddytest.NewTester(t)
42 + tester.InitServer(`{
43 + http_port 9080
44 + https_port 9443
45 + }
46 +
47 + localhost:9080 {
48 +
49 + map http.request.method dest-name {
50 + default unknown
51 + GET get-called
52 + }
53 +
54 + respond /version 200 {
55 + body "hello from localhost {dest-name}"
56 + }
57 + }
58 + `, "caddyfile")
59 +
60 + // act and assert
61 + tester.AssertGetResponse("http://localhost:9080/version", 200, "hello from localhost get-called")
62 + tester.AssertPostResponseBody("http://localhost:9080/version", []string{}, bytes.NewBuffer([]byte{}), 200, "hello from localhost unknown")
63 +}
64 +
65 +func TestMapAsJson(t *testing.T) {
66 +
67 + // arrange
68 + tester := caddytest.NewTester(t)
69 + tester.InitServer(`{
70 + "apps": {
71 + "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",
84 + "routes": [
85 + {
86 + "handle": [
87 + {
88 + "handler": "map",
89 + "source": "http.request.method",
90 + "destination": "dest-name",
91 + "default": "unknown",
92 + "items": [
93 + {
94 + "expression": "GET",
95 + "value": "get-called"
96 + },
97 + {
98 + "expression": "POST",
99 + "value": "post-called"
100 + }
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 + ]
120 + }
121 + ]
122 + }
123 + ],
124 + "match": [
125 + {
126 + "host": [
127 + "localhost"
128 + ]
129 + }
130 + ],
131 + "terminal": true
132 + }
133 + ]
134 + }
135 + }
136 + }
137 + }
138 + }
139 + `, "json")
140 +
141 + tester.AssertGetResponse("http://localhost:9080/version", 200, "hello from localhost get-called")
142 + tester.AssertPostResponseBody("http://localhost:9080/version", []string{}, bytes.NewBuffer([]byte{}), 200, "hello from localhost post-called")
143 +}
modules/caddyhttp/map/caddyfile.go new
+71
@@ -0,0 +1,71 @@
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 + "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
19 + "github.com/caddyserver/caddy/v2/modules/caddyhttp"
20 +)
21 +
22 +func init() {
23 + httpcaddyfile.RegisterHandlerDirective("map", parseCaddyfile)
24 +}
25 +
26 +// parseCaddyfile sets up the handler for a map from Caddyfile tokens. Syntax:
27 +//
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 +// ...
32 +// }
33 +//
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.
38 +//
39 +func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
40 + m := new(Handler)
41 +
42 + 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 + }
49 + }
50 +
51 + // load the rules
52 + 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()
58 + }
59 + m.Default = args[0]
60 + } else {
61 + args := h.RemainingArgs()
62 + if len(args) != 1 {
63 + return m, h.ArgErr()
64 + }
65 + m.Items = append(m.Items, Item{Expression: expression, Value: args[0]})
66 + }
67 + }
68 + }
69 +
70 + return m, nil
71 +}
modules/caddyhttp/map/map.go new
+105
@@ -0,0 +1,105 @@
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 + "net/http"
19 + "regexp"
20 +
21 + "github.com/caddyserver/caddy/v2"
22 + "github.com/caddyserver/caddy/v2/modules/caddyhttp"
23 +)
24 +
25 +func init() {
26 + caddy.RegisterModule(Handler{})
27 +}
28 +
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.
40 +//
41 +type Handler struct {
42 + // Source is a placeholder
43 + 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"`
50 +}
51 +
52 +// CaddyModule returns the Caddy module information.
53 +func (Handler) CaddyModule() caddy.ModuleInfo {
54 + return caddy.ModuleInfo{
55 + ID: "http.handlers.map",
56 + New: func() caddy.Module { return new(Handler) },
57 + }
58 +}
59 +
60 +// Provision will compile all regular expressions
61 +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 + }
65 + return nil
66 +}
67 +
68 +func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
69 + repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
70 +
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
81 + }
82 + }
83 +
84 + if !found && h.Default != "" {
85 + repl.Set(h.Destination, h.Default)
86 + }
87 + }
88 + return next.ServeHTTP(w, r)
89 +}
90 +
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
99 +}
100 +
101 +// Interface guards
102 +var (
103 + _ caddy.Provisioner = (*Handler)(nil)
104 + _ caddyhttp.MiddlewareHandler = (*Handler)(nil)
105 +)
modules/caddyhttp/standard/imports.go
+1
@@ -9,6 +9,7 @@ import (
9 _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode/zstd"
10 _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/fileserver"
11 _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
12 + _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/map"
13 _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/requestbody"
14 _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
15 _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy/fastcgi"
replacer.go
+7
@@ -66,6 +66,13 @@ func (r *Replacer) Get(variable string) (interface{}, bool) {
66 return nil, false
67 }
68
69 +// GetString is the same as Get, but coerces the value to a
70 +// string representation.
71 +func (r *Replacer) GetString(variable string) (string, bool) {
72 + s, found := r.Get(variable)
73 + return toString(s), found
74 +}
75 +
76 // Delete removes a variable with a static value
77 // that was created using Set.
78 func (r *Replacer) Delete(variable string) {