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
push: Implement HTTP/2 server push (#3573)
* push: Implement HTTP/2 server push (close #3551) * push: Abstract header ops by embedding into new struct type This will allow us to add more fields to customize headers in push-specific ways in the future. * push: Ensure Link resources are pushed before response is written * Change header name from X-Caddy-Push to Caddy-Push
Matt Holt committed
Jul 20, 2020 at 12:28 UTC
6cea1f239d01fc065bc6f4b22d765d89b6db0152
7 files changed
+506
-5
caddyconfig/httpcaddyfile/directives.go
+2
-1
@@ -55,10 +55,11 @@ var directiveOrder = []string{
55
"encode",
56
"templates",
57
58
- // special routing directives
58
+ // special routing & dispatching directives
59
"handle",
60
"handle_path",
61
"route",
62
+ "push",
63
64
// handlers that typically respond to requests
65
"respond",
modules/caddyhttp/headers/headers.go
+5
-4
@@ -54,15 +54,15 @@ func (Handler) CaddyModule() caddy.ModuleInfo {
54
}
55
56
// Provision sets up h's configuration.
57
-func (h *Handler) Provision(_ caddy.Context) error {
57
+func (h *Handler) Provision(ctx caddy.Context) error {
58
if h.Request != nil {
59
- err := h.Request.provision()
59
+ err := h.Request.Provision(ctx)
60
if err != nil {
61
return err
62
}
63
}
64
if h.Response != nil {
65
- err := h.Response.provision()
65
+ err := h.Response.Provision(ctx)
66
if err != nil {
67
return err
68
}
@@ -125,7 +125,8 @@ type HeaderOps struct {
125
Replace map[string][]Replacement `json:"replace,omitempty"`
126
}
127
128
-func (ops *HeaderOps) provision() error {
128
+// Provision sets up the header operations.
129
+func (ops *HeaderOps) Provision(_ caddy.Context) error {
130
for fieldName, replacements := range ops.Replace {
131
for i, r := range replacements {
132
if r.SearchRegexp != "" {
modules/caddyhttp/push/caddyfile.go
new
+99
@@ -0,0 +1,99 @@
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 push
16
+
17
+import (
18
+ "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
19
+ "github.com/caddyserver/caddy/v2/modules/caddyhttp"
20
+ "github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
21
+)
22
+
23
+func init() {
24
+ httpcaddyfile.RegisterHandlerDirective("push", parseCaddyfile)
25
+}
26
+
27
+// parseCaddyfile sets up the push handler. Syntax:
28
+//
29
+// push [<matcher>] [<resource>] {
30
+// [GET|HEAD] <resource>
31
+// headers {
32
+// [+]<field> [<value|regexp> [<replacement>]]
33
+// -<field>
34
+// }
35
+// }
36
+//
37
+// A single resource can be specified inline without opening a
38
+// block for the most common/simple case. Or, a block can be
39
+// opened and multiple resources can be specified, one per
40
+// line, optionally preceded by the method. The headers
41
+// subdirective can be used to customize the headers that
42
+// are set on each (synthetic) push request, using the same
43
+// syntax as the 'header' directive for request headers.
44
+// Placeholders are accepted in resource and header field
45
+// name and value and replacement tokens.
46
+func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
47
+ handler := new(Handler)
48
+
49
+ for h.Next() {
50
+ if h.NextArg() {
51
+ handler.Resources = append(handler.Resources, Resource{Target: h.Val()})
52
+ }
53
+
54
+ // optional block
55
+ for outerNesting := h.Nesting(); h.NextBlock(outerNesting); {
56
+ switch h.Val() {
57
+ case "headers":
58
+ if h.NextArg() {
59
+ return nil, h.ArgErr()
60
+ }
61
+ for innerNesting := h.Nesting(); h.NextBlock(innerNesting); {
62
+ // include current token, which we treat as an argument here
63
+ args := []string{h.Val()}
64
+ args = append(args, h.RemainingArgs()...)
65
+
66
+ if handler.Headers == nil {
67
+ handler.Headers = new(HeaderConfig)
68
+ }
69
+ switch len(args) {
70
+ case 1:
71
+ headers.CaddyfileHeaderOp(&handler.Headers.HeaderOps, args[0], "", "")
72
+ case 2:
73
+ headers.CaddyfileHeaderOp(&handler.Headers.HeaderOps, args[0], args[1], "")
74
+ case 3:
75
+ headers.CaddyfileHeaderOp(&handler.Headers.HeaderOps, args[0], args[1], args[2])
76
+ default:
77
+ return nil, h.ArgErr()
78
+ }
79
+ }
80
+
81
+ case "GET", "HEAD":
82
+ method := h.Val()
83
+ if !h.NextArg() {
84
+ return nil, h.ArgErr()
85
+ }
86
+ target := h.Val()
87
+ handler.Resources = append(handler.Resources, Resource{
88
+ Method: method,
89
+ Target: target,
90
+ })
91
+
92
+ default:
93
+ handler.Resources = append(handler.Resources, Resource{Target: h.Val()})
94
+ }
95
+ }
96
+ }
97
+
98
+ return handler, nil
99
+}
modules/caddyhttp/push/handler.go
new
+236
@@ -0,0 +1,236 @@
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 push
16
+
17
+import (
18
+ "fmt"
19
+ "net/http"
20
+ "strings"
21
+
22
+ "github.com/caddyserver/caddy/v2"
23
+ "github.com/caddyserver/caddy/v2/modules/caddyhttp"
24
+ "github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
25
+ "go.uber.org/zap"
26
+)
27
+
28
+func init() {
29
+ caddy.RegisterModule(Handler{})
30
+}
31
+
32
+// Handler is a middleware for manipulating the request body.
33
+type Handler struct {
34
+ Resources []Resource `json:"resources,omitempty"`
35
+ Headers *HeaderConfig `json:"headers,omitempty"`
36
+
37
+ logger *zap.Logger
38
+}
39
+
40
+// CaddyModule returns the Caddy module information.
41
+func (Handler) CaddyModule() caddy.ModuleInfo {
42
+ return caddy.ModuleInfo{
43
+ ID: "http.handlers.push",
44
+ New: func() caddy.Module { return new(Handler) },
45
+ }
46
+}
47
+
48
+// Provision sets up h.
49
+func (h *Handler) Provision(ctx caddy.Context) error {
50
+ h.logger = ctx.Logger(h)
51
+ if h.Headers != nil {
52
+ err := h.Headers.Provision(ctx)
53
+ if err != nil {
54
+ return fmt.Errorf("provisioning header operations: %v", err)
55
+ }
56
+ }
57
+ return nil
58
+}
59
+
60
+func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
61
+ pusher, ok := w.(http.Pusher)
62
+ if !ok {
63
+ return next.ServeHTTP(w, r)
64
+ }
65
+
66
+ // short-circuit recursive pushes
67
+ if _, ok := r.Header[pushHeader]; ok {
68
+ return next.ServeHTTP(w, r)
69
+ }
70
+
71
+ repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
72
+
73
+ // create header for push requests
74
+ hdr := h.initializePushHeaders(r, repl)
75
+
76
+ // push first!
77
+ for _, resource := range h.Resources {
78
+ h.logger.Debug("pushing resource",
79
+ zap.String("uri", r.RequestURI),
80
+ zap.String("push_method", resource.Method),
81
+ zap.String("push_target", resource.Target),
82
+ zap.Object("push_headers", caddyhttp.LoggableHTTPHeader(hdr)))
83
+ err := pusher.Push(repl.ReplaceAll(resource.Target, "."), &http.PushOptions{
84
+ Method: resource.Method,
85
+ Header: hdr,
86
+ })
87
+ if err != nil {
88
+ // usually this means either that push is not
89
+ // supported or concurrent streams are full
90
+ break
91
+ }
92
+ }
93
+
94
+ // wrap the response writer so that we can initiate push of any resources
95
+ // described in Link header fields before the response is written
96
+ lp := linkPusher{
97
+ ResponseWriterWrapper: &caddyhttp.ResponseWriterWrapper{ResponseWriter: w},
98
+ handler: h,
99
+ pusher: pusher,
100
+ header: hdr,
101
+ request: r,
102
+ }
103
+
104
+ // serve only after pushing!
105
+ if err := next.ServeHTTP(lp, r); err != nil {
106
+ return err
107
+ }
108
+
109
+ return nil
110
+}
111
+
112
+func (h Handler) initializePushHeaders(r *http.Request, repl *caddy.Replacer) http.Header {
113
+ hdr := make(http.Header)
114
+
115
+ // prevent recursive pushes
116
+ hdr.Set(pushHeader, "1")
117
+
118
+ // set initial header fields; since exactly how headers should
119
+ // be implemented for server push is not well-understood, we
120
+ // are being conservative for now like httpd is:
121
+ // https://httpd.apache.org/docs/2.4/en/howto/http2.html#push
122
+ // we only copy some well-known, safe headers that are likely
123
+ // crucial when requesting certain kinds of content
124
+ for _, fieldName := range safeHeaders {
125
+ if vals, ok := r.Header[fieldName]; ok {
126
+ hdr[fieldName] = vals
127
+ }
128
+ }
129
+
130
+ // user can customize the push request headers
131
+ if h.Headers != nil {
132
+ h.Headers.ApplyTo(hdr, repl)
133
+ }
134
+
135
+ return hdr
136
+}
137
+
138
+// servePreloadLinks parses Link headers from upstream and pushes
139
+// resources described by them. If a resource has the "nopush"
140
+// attribute or describes an external entity (meaning, the resource
141
+// URI includes a scheme), it will not be pushed.
142
+func (h Handler) servePreloadLinks(pusher http.Pusher, hdr http.Header, resources []string) {
143
+ for _, resource := range resources {
144
+ for _, resource := range parseLinkHeader(resource) {
145
+ if _, ok := resource.params["nopush"]; ok {
146
+ continue
147
+ }
148
+ if isRemoteResource(resource.uri) {
149
+ continue
150
+ }
151
+ err := pusher.Push(resource.uri, &http.PushOptions{
152
+ Header: hdr,
153
+ })
154
+ if err != nil {
155
+ return
156
+ }
157
+ }
158
+ }
159
+}
160
+
161
+// Resource represents a request for a resource to push.
162
+type Resource struct {
163
+ // Method is the request method, which must be GET or HEAD.
164
+ // Default is GET.
165
+ Method string `json:"method,omitempty"`
166
+
167
+ // Target is the path to the resource being pushed.
168
+ Target string `json:"target,omitempty"`
169
+}
170
+
171
+// HeaderConfig configures headers for synthetic push requests.
172
+type HeaderConfig struct {
173
+ headers.HeaderOps
174
+}
175
+
176
+// linkPusher is a http.ResponseWriter that intercepts
177
+// the WriteHeader() call to ensure that any resources
178
+// described by Link response headers get pushed before
179
+// the response is allowed to be written.
180
+type linkPusher struct {
181
+ *caddyhttp.ResponseWriterWrapper
182
+ handler Handler
183
+ pusher http.Pusher
184
+ header http.Header
185
+ request *http.Request
186
+}
187
+
188
+func (lp linkPusher) WriteHeader(statusCode int) {
189
+ if links, ok := lp.ResponseWriter.Header()["Link"]; ok {
190
+ // only initiate these pushes if it hasn't been done yet
191
+ if val := caddyhttp.GetVar(lp.request.Context(), pushedLink); val == nil {
192
+ lp.handler.logger.Debug("pushing Link resources", zap.Strings("linked", links))
193
+ caddyhttp.SetVar(lp.request.Context(), pushedLink, true)
194
+ lp.handler.servePreloadLinks(lp.pusher, lp.header, links)
195
+ }
196
+ }
197
+ lp.ResponseWriter.WriteHeader(statusCode)
198
+}
199
+
200
+// isRemoteResource returns true if resource starts with
201
+// a scheme or is a protocol-relative URI.
202
+func isRemoteResource(resource string) bool {
203
+ return strings.HasPrefix(resource, "//") ||
204
+ strings.HasPrefix(resource, "http://") ||
205
+ strings.HasPrefix(resource, "https://")
206
+}
207
+
208
+// safeHeaders is a list of header fields that are
209
+// safe to copy to push requests implicitly. It is
210
+// assumed that requests for certain kinds of content
211
+// would fail without these fields present.
212
+var safeHeaders = []string{
213
+ "Accept-Encoding",
214
+ "Accept-Language",
215
+ "Accept",
216
+ "Cache-Control",
217
+ "User-Agent",
218
+}
219
+
220
+// pushHeader is a header field that gets added to push requests
221
+// in order to avoid recursive/infinite pushes.
222
+const pushHeader = "Caddy-Push"
223
+
224
+// pushedLink is the key for the variable on the request
225
+// context that we use to remember whether we have already
226
+// pushed resources from Link headers yet; otherwise, if
227
+// multiple push handlers are invoked, it would repeat the
228
+// pushing of Link headers.
229
+const pushedLink = "http.handlers.push.pushed_link"
230
+
231
+// Interface guards
232
+var (
233
+ _ caddy.Provisioner = (*Handler)(nil)
234
+ _ caddyhttp.MiddlewareHandler = (*Handler)(nil)
235
+ _ caddyhttp.HTTPInterfaces = (*linkPusher)(nil)
236
+)
modules/caddyhttp/push/link.go
new
+78
@@ -0,0 +1,78 @@
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 push
16
+
17
+import (
18
+ "strings"
19
+)
20
+
21
+// linkResource contains the results of a parsed Link header.
22
+type linkResource struct {
23
+ uri string
24
+ params map[string]string
25
+}
26
+
27
+// parseLinkHeader is responsible for parsing Link header
28
+// and returning list of found resources.
29
+//
30
+// Accepted formats are:
31
+//
32
+// Link: <resource>; as=script
33
+// Link: <resource>; as=script,<resource>; as=style
34
+// Link: <resource>;<resource2>
35
+//
36
+// where <resource> begins with a forward slash (/).
37
+func parseLinkHeader(header string) []linkResource {
38
+ resources := []linkResource{}
39
+
40
+ if header == "" {
41
+ return resources
42
+ }
43
+
44
+ for _, link := range strings.Split(header, comma) {
45
+ l := linkResource{params: make(map[string]string)}
46
+
47
+ li, ri := strings.Index(link, "<"), strings.Index(link, ">")
48
+ if li == -1 || ri == -1 {
49
+ continue
50
+ }
51
+
52
+ l.uri = strings.TrimSpace(link[li+1 : ri])
53
+
54
+ for _, param := range strings.Split(strings.TrimSpace(link[ri+1:]), semicolon) {
55
+ parts := strings.SplitN(strings.TrimSpace(param), equal, 2)
56
+ key := strings.TrimSpace(parts[0])
57
+ if key == "" {
58
+ continue
59
+ }
60
+ if len(parts) == 1 {
61
+ l.params[key] = key
62
+ }
63
+ if len(parts) == 2 {
64
+ l.params[key] = strings.TrimSpace(parts[1])
65
+ }
66
+ }
67
+
68
+ resources = append(resources, l)
69
+ }
70
+
71
+ return resources
72
+}
73
+
74
+const (
75
+ comma = ","
76
+ semicolon = ";"
77
+ equal = "="
78
+)
modules/caddyhttp/push/link_test.go
new
+85
@@ -0,0 +1,85 @@
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
+package push
15
+
16
+import (
17
+ "reflect"
18
+ "testing"
19
+)
20
+
21
+func TestParseLinkHeader(t *testing.T) {
22
+ testCases := []struct {
23
+ header string
24
+ expectedResources []linkResource
25
+ }{
26
+ {
27
+ header: "</resource>; as=script",
28
+ expectedResources: []linkResource{{uri: "/resource", params: map[string]string{"as": "script"}}},
29
+ },
30
+ {
31
+ header: "</resource>",
32
+ expectedResources: []linkResource{{uri: "/resource", params: map[string]string{}}},
33
+ },
34
+ {
35
+ header: "</resource>; nopush",
36
+ expectedResources: []linkResource{{uri: "/resource", params: map[string]string{"nopush": "nopush"}}},
37
+ },
38
+ {
39
+ header: "</resource>;nopush;rel=next",
40
+ expectedResources: []linkResource{{uri: "/resource", params: map[string]string{"nopush": "nopush", "rel": "next"}}},
41
+ },
42
+ {
43
+ header: "</resource>;nopush;rel=next,</resource2>;nopush",
44
+ expectedResources: []linkResource{
45
+ {uri: "/resource", params: map[string]string{"nopush": "nopush", "rel": "next"}},
46
+ {uri: "/resource2", params: map[string]string{"nopush": "nopush"}},
47
+ },
48
+ },
49
+ {
50
+ header: "</resource>,</resource2>",
51
+ expectedResources: []linkResource{
52
+ {uri: "/resource", params: map[string]string{}},
53
+ {uri: "/resource2", params: map[string]string{}},
54
+ },
55
+ },
56
+ {
57
+ header: "malformed",
58
+ expectedResources: []linkResource{},
59
+ },
60
+ {
61
+ header: "<malformed",
62
+ expectedResources: []linkResource{},
63
+ },
64
+ {
65
+ header: ",",
66
+ expectedResources: []linkResource{},
67
+ },
68
+ {
69
+ header: ";",
70
+ expectedResources: []linkResource{},
71
+ },
72
+ {
73
+ header: "</resource> ; ",
74
+ expectedResources: []linkResource{{uri: "/resource", params: map[string]string{}}},
75
+ },
76
+ }
77
+
78
+ for i, test := range testCases {
79
+ actualResources := parseLinkHeader(test.header)
80
+ if !reflect.DeepEqual(actualResources, test.expectedResources) {
81
+ t.Errorf("Test %d (header: %s) - expected resources %v, got %v",
82
+ i, test.header, test.expectedResources, actualResources)
83
+ }
84
+ }
85
+}
modules/caddyhttp/standard/imports.go
+1
@@ -10,6 +10,7 @@ import (
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/push"
14
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/requestbody"
15
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
16
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy/fastcgi"