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

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"