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

Performance improvements to Replacer implementation (placeholders) (#2674)

Closes #2673

Matt Holt committed Jul 16, 2019 at 12:27 UTC b44a22a9d45443aa736a04495dfe716b62b58adc
2 files changed +199 -94
modules/caddyhttp/replacer.go
+106 -49
@@ -15,86 +15,143 @@
15 package caddyhttp
16
17 import (
18 - "fmt"
18 "net"
19 "net/http"
20 + "net/textproto"
21 "path"
22 + "strconv"
23 "strings"
24
25 "github.com/caddyserver/caddy/v2"
26 )
27
27 -// TODO: A simple way to format or escape or encode each value would be nice
28 -// ... TODO: Should we just use templates? :-/ yeesh...
29 -
28 func addHTTPVarsToReplacer(repl caddy.Replacer, req *http.Request, w http.ResponseWriter) {
31 - httpVars := func() map[string]string {
32 - m := make(map[string]string)
29 + httpVars := func(key string) (string, bool) {
30 if req != nil {
34 - m["http.request.host"] = func() string {
31 + if strings.HasPrefix(key, queryReplPrefix) {
32 + vals := req.URL.Query()[key[len(queryReplPrefix):]]
33 + // always return true, since the query param might
34 + // be present only in some requests
35 + return strings.Join(vals, ","), true
36 + }
37 +
38 + if strings.HasPrefix(key, reqHeaderReplPrefix) {
39 + field := key[len(reqHeaderReplPrefix):]
40 + vals := req.Header[textproto.CanonicalMIMEHeaderKey(field)]
41 + // always return true, since the header field might
42 + // be present only in some requests
43 + return strings.Join(vals, ","), true
44 + }
45 +
46 + if strings.HasPrefix(key, cookieReplPrefix) {
47 + name := key[len(cookieReplPrefix):]
48 + for _, cookie := range req.Cookies() {
49 + if strings.EqualFold(name, cookie.Name) {
50 + // always return true, since the cookie might
51 + // be present only in some requests
52 + return cookie.Value, true
53 + }
54 + }
55 + }
56 +
57 + switch key {
58 + case "http.request.host":
59 host, _, err := net.SplitHostPort(req.Host)
60 if err != nil {
37 - return req.Host // OK; there probably was no port
61 + return req.Host, true // OK; there probably was no port
62 }
39 - return host
40 - }()
41 - m["http.request.hostport"] = req.Host // may include both host and port
42 - m["http.request.method"] = req.Method
43 - m["http.request.port"] = func() string {
44 - // if there is no port, there will be an error; in
45 - // that case, port is the empty string anyway
63 + return host, true
64 + case "http.request.hostport":
65 + return req.Host, true
66 + case "http.request.method":
67 + return req.Method, true
68 + case "http.request.port":
69 _, port, _ := net.SplitHostPort(req.Host)
47 - return port
48 - }()
49 - m["http.request.scheme"] = func() string {
70 + return port, true
71 + case "http.request.scheme":
72 if req.TLS != nil {
51 - return "https"
73 + return "https", true
74 }
53 - return "http"
54 - }()
55 - m["http.request.uri"] = req.URL.RequestURI()
56 - m["http.request.uri.path"] = req.URL.Path
57 - m["http.request.uri.path.file"] = func() string {
75 + return "http", true
76 + case "http.request.uri":
77 + return req.URL.RequestURI(), true
78 + case "http.request.uri.path":
79 + return req.URL.Path, true
80 + case "http.request.uri.path.file":
81 _, file := path.Split(req.URL.Path)
59 - return file
60 - }()
61 - m["http.request.uri.path.dir"] = func() string {
82 + return file, true
83 + case "http.request.uri.path.dir":
84 dir, _ := path.Split(req.URL.Path)
63 - return dir
64 - }()
65 - m["http.request.uri.query"] = req.URL.RawQuery
66 -
67 - for param, vals := range req.URL.Query() {
68 - m["http.request.uri.query."+param] = strings.Join(vals, ",")
85 + return dir, true
86 + case "http.request.uri.query":
87 + return req.URL.RawQuery, true
88 }
70 - for field, vals := range req.Header {
71 - m["http.request.header."+strings.ToLower(field)] = strings.Join(vals, ",")
72 - }
73 - for _, cookie := range req.Cookies() {
74 - m["http.request.cookie."+cookie.Name] = cookie.Value
89 +
90 + if strings.HasPrefix(key, respHeaderReplPrefix) {
91 + field := key[len(respHeaderReplPrefix):]
92 + vals := w.Header()[textproto.CanonicalMIMEHeaderKey(field)]
93 + // always return true, since the header field might
94 + // be present only in some requests
95 + return strings.Join(vals, ","), true
96 }
97
77 - hostLabels := strings.Split(req.Host, ".")
78 - for i, label := range hostLabels {
79 - key := fmt.Sprintf("http.request.host.labels.%d", len(hostLabels)-i-1)
80 - m[key] = label
98 + if strings.HasPrefix(key, hostLabelReplPrefix) {
99 + idxStr := key[len(hostLabelReplPrefix):]
100 + idx, err := strconv.Atoi(idxStr)
101 + if err != nil {
102 + return "", false
103 + }
104 + hostLabels := strings.Split(req.Host, ".")
105 + if idx < 0 {
106 + return "", false
107 + }
108 + if idx >= len(hostLabels) {
109 + return "", true
110 + }
111 + return hostLabels[idx], true
112 }
113
83 - pathParts := strings.Split(req.URL.Path, "/")
84 - for i, label := range pathParts {
85 - key := fmt.Sprintf("http.request.uri.path.%d", i)
86 - m[key] = label
114 + if strings.HasPrefix(key, pathPartsReplPrefix) {
115 + idxStr := key[len(pathPartsReplPrefix):]
116 + idx, err := strconv.Atoi(idxStr)
117 + if err != nil {
118 + return "", false
119 + }
120 + pathParts := strings.Split(req.URL.Path, "/")
121 + if len(pathParts) > 0 && pathParts[0] == "" {
122 + pathParts = pathParts[1:]
123 + }
124 + if idx < 0 {
125 + return "", false
126 + }
127 + if idx >= len(pathParts) {
128 + return "", true
129 + }
130 + return pathParts[idx], true
131 }
132 }
133
134 if w != nil {
91 - for field, vals := range w.Header() {
92 - m["http.response.header."+strings.ToLower(field)] = strings.Join(vals, ",")
135 + if strings.HasPrefix(key, respHeaderReplPrefix) {
136 + field := key[len(respHeaderReplPrefix):]
137 + vals := w.Header()[textproto.CanonicalMIMEHeaderKey(field)]
138 + // always return true, since the header field might
139 + // be present only in some responses
140 + return strings.Join(vals, ","), true
141 }
142 }
143
96 - return m
144 + return "", false
145 }
146
147 repl.Map(httpVars)
148 }
149 +
150 +const (
151 + queryReplPrefix = "http.request.uri.query."
152 + reqHeaderReplPrefix = "http.request.header."
153 + cookieReplPrefix = "http.request.cookie."
154 + hostLabelReplPrefix = "http.request.host.labels."
155 + pathPartsReplPrefix = "http.request.uri.path."
156 + respHeaderReplPrefix = "http.response.header."
157 +)
replacer.go
+93 -45
@@ -25,7 +25,7 @@ import (
25 type Replacer interface {
26 Set(variable, value string)
27 Delete(variable string)
28 - Map(func() map[string]string)
28 + Map(ReplacementFunc)
29 ReplaceAll(input, empty string) string
30 }
31
@@ -34,23 +34,22 @@ func NewReplacer() Replacer {
34 rep := &replacer{
35 static: make(map[string]string),
36 }
37 - rep.providers = []ReplacementsFunc{
38 - defaultReplacements,
39 - func() map[string]string { return rep.static },
37 + rep.providers = []ReplacementFunc{
38 + globalDefaultReplacements,
39 + rep.fromStatic,
40 }
41 return rep
42 }
43
44 type replacer struct {
45 - providers []ReplacementsFunc
45 + providers []ReplacementFunc
46 static map[string]string
47 }
48
49 -// Map augments the map of replacements with those returned
50 -// by the given replacements function. The function is only
51 -// executed at replace-time.
52 -func (r *replacer) Map(replacements func() map[string]string) {
53 - r.providers = append(r.providers, replacements)
49 +// Map adds mapFunc to the list of value providers.
50 +// mapFunc will be executed only at replace-time.
51 +func (r *replacer) Map(mapFunc ReplacementFunc) {
52 + r.providers = append(r.providers, mapFunc)
53 }
54
55 // Set sets a custom variable to a static value.
@@ -64,55 +63,104 @@ func (r *replacer) Delete(variable string) {
63 delete(r.static, variable)
64 }
65
67 -// ReplaceAll replaces placeholders in input with their values.
68 -// Values that are empty string will be substituted with the
69 -// empty parameter.
66 +// fromStatic provides values from r.static.
67 +func (r *replacer) fromStatic(key string) (val string, ok bool) {
68 + val, ok = r.static[key]
69 + return
70 +}
71 +
72 +// ReplaceAll efficiently replaces placeholders in input with
73 +// their values. Unrecognized placeholders will not be replaced.
74 +// Values that are empty string will be substituted with empty.
75 func (r *replacer) ReplaceAll(input, empty string) string {
71 - if !strings.Contains(input, phOpen) {
76 + if !strings.Contains(input, string(phOpen)) {
77 return input
78 }
74 - for _, replacements := range r.providers {
75 - for key, val := range replacements() {
76 - if val == "" {
77 - val = empty
79 +
80 + var sb strings.Builder
81 +
82 + // it is reasonable to assume that the output
83 + // will be approximately as long as the input
84 + sb.Grow(len(input))
85 +
86 + // iterate the input to find each placeholder
87 + var lastWriteCursor int
88 + for i := 0; i < len(input); i++ {
89 + if input[i] != phOpen {
90 + continue
91 + }
92 +
93 + // write the substring from the last cursor to this point
94 + sb.WriteString(input[lastWriteCursor:i])
95 +
96 + // find the end of the placeholder
97 + end := strings.Index(input[i:], string(phClose)) + i
98 +
99 + // trim opening bracket
100 + key := input[i+1 : end]
101 +
102 + // try to get a value for this key; if
103 + // the key is not recognized, do not
104 + // perform any replacement
105 + var found bool
106 + for _, mapFunc := range r.providers {
107 + if val, ok := mapFunc(key); ok {
108 + found = true
109 + if val != "" {
110 + sb.WriteString(val)
111 + } else if empty != "" {
112 + sb.WriteString(empty)
113 + }
114 + break
115 }
79 - input = strings.ReplaceAll(input, phOpen+key+phClose, val)
116 }
117 + if !found {
118 + continue
119 + }
120 +
121 + // advance cursor to end of placeholder
122 + i = end + 1
123 + lastWriteCursor = i
124 }
82 - return input
125 +
126 + // flush any unwritten remainder
127 + sb.WriteString(input[lastWriteCursor:])
128 +
129 + return sb.String()
130 }
131
85 -// ReplacementsFunc is a function that returns replacements,
86 -// which is variable names mapped to their values. The
87 -// function will be evaluated only at replace-time to ensure
88 -// the most current values are mapped.
89 -type ReplacementsFunc func() map[string]string
90 -
91 -var defaultReplacements = func() map[string]string {
92 - m := map[string]string{
93 - "system.hostname": func() string {
94 - // OK if there is an error; just return empty string
95 - name, _ := os.Hostname()
96 - return name
97 - }(),
98 - "system.slash": string(filepath.Separator),
99 - "system.os": runtime.GOOS,
100 - "system.arch": runtime.GOARCH,
132 +// ReplacementFunc is a function that returns a replacement
133 +// for the given key along with true if the function is able
134 +// to service that key (even if the value is blank). If the
135 +// function does not recognize the key, false should be
136 +// returned.
137 +type ReplacementFunc func(key string) (val string, ok bool)
138 +
139 +func globalDefaultReplacements(key string) (string, bool) {
140 + // check environment variable
141 + const envPrefix = "env."
142 + if strings.HasPrefix(key, envPrefix) {
143 + val := os.Getenv(key[len(envPrefix):])
144 + return val, val != ""
145 }
146
103 - // add environment variables
104 - for _, keyval := range os.Environ() {
105 - parts := strings.SplitN(keyval, "=", 2)
106 - if len(parts) != 2 {
107 - continue
108 - }
109 - m["env."+strings.ToLower(parts[0])] = parts[1]
147 + switch key {
148 + case "system.hostname":
149 + // OK if there is an error; just return empty string
150 + name, _ := os.Hostname()
151 + return name, true
152 + case "system.slash":
153 + return string(filepath.Separator), true
154 + case "system.os":
155 + return runtime.GOOS, true
156 + case "system.arch":
157 + return runtime.GOARCH, true
158 }
159
112 - return m
160 + return "", false
161 }
162
163 // ReplacerCtxKey is the context key for a replacer.
164 const ReplacerCtxKey CtxKey = "replacer"
165
118 -const phOpen, phClose = "{", "}"
166 +const phOpen, phClose = '{', '}'