-
-
Notifications
You must be signed in to change notification settings - Fork 4.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
Showing
7 changed files
with
506 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
// Copyright 2015 Matthew Holt and The Caddy Authors | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
package push | ||
|
||
import ( | ||
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" | ||
"github.com/caddyserver/caddy/v2/modules/caddyhttp" | ||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/headers" | ||
) | ||
|
||
func init() { | ||
httpcaddyfile.RegisterHandlerDirective("push", parseCaddyfile) | ||
} | ||
|
||
// parseCaddyfile sets up the push handler. Syntax: | ||
// | ||
// push [<matcher>] [<resource>] { | ||
// [GET|HEAD] <resource> | ||
// headers { | ||
// [+]<field> [<value|regexp> [<replacement>]] | ||
// -<field> | ||
// } | ||
// } | ||
// | ||
// A single resource can be specified inline without opening a | ||
// block for the most common/simple case. Or, a block can be | ||
// opened and multiple resources can be specified, one per | ||
// line, optionally preceded by the method. The headers | ||
// subdirective can be used to customize the headers that | ||
// are set on each (synthetic) push request, using the same | ||
// syntax as the 'header' directive for request headers. | ||
// Placeholders are accepted in resource and header field | ||
// name and value and replacement tokens. | ||
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { | ||
handler := new(Handler) | ||
|
||
for h.Next() { | ||
if h.NextArg() { | ||
handler.Resources = append(handler.Resources, Resource{Target: h.Val()}) | ||
} | ||
|
||
// optional block | ||
for outerNesting := h.Nesting(); h.NextBlock(outerNesting); { | ||
switch h.Val() { | ||
case "headers": | ||
if h.NextArg() { | ||
return nil, h.ArgErr() | ||
} | ||
for innerNesting := h.Nesting(); h.NextBlock(innerNesting); { | ||
// include current token, which we treat as an argument here | ||
args := []string{h.Val()} | ||
args = append(args, h.RemainingArgs()...) | ||
|
||
if handler.Headers == nil { | ||
handler.Headers = new(HeaderConfig) | ||
} | ||
switch len(args) { | ||
case 1: | ||
headers.CaddyfileHeaderOp(&handler.Headers.HeaderOps, args[0], "", "") | ||
case 2: | ||
headers.CaddyfileHeaderOp(&handler.Headers.HeaderOps, args[0], args[1], "") | ||
case 3: | ||
headers.CaddyfileHeaderOp(&handler.Headers.HeaderOps, args[0], args[1], args[2]) | ||
default: | ||
return nil, h.ArgErr() | ||
} | ||
} | ||
|
||
case "GET", "HEAD": | ||
method := h.Val() | ||
if !h.NextArg() { | ||
return nil, h.ArgErr() | ||
} | ||
target := h.Val() | ||
handler.Resources = append(handler.Resources, Resource{ | ||
Method: method, | ||
Target: target, | ||
}) | ||
|
||
default: | ||
handler.Resources = append(handler.Resources, Resource{Target: h.Val()}) | ||
} | ||
} | ||
} | ||
|
||
return handler, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,236 @@ | ||
// Copyright 2015 Matthew Holt and The Caddy Authors | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
package push | ||
|
||
import ( | ||
"fmt" | ||
"net/http" | ||
"strings" | ||
|
||
"github.com/caddyserver/caddy/v2" | ||
"github.com/caddyserver/caddy/v2/modules/caddyhttp" | ||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/headers" | ||
"go.uber.org/zap" | ||
) | ||
|
||
func init() { | ||
caddy.RegisterModule(Handler{}) | ||
} | ||
|
||
// Handler is a middleware for manipulating the request body. | ||
type Handler struct { | ||
Resources []Resource `json:"resources,omitempty"` | ||
Headers *HeaderConfig `json:"headers,omitempty"` | ||
|
||
logger *zap.Logger | ||
} | ||
|
||
// CaddyModule returns the Caddy module information. | ||
func (Handler) CaddyModule() caddy.ModuleInfo { | ||
return caddy.ModuleInfo{ | ||
ID: "http.handlers.push", | ||
New: func() caddy.Module { return new(Handler) }, | ||
} | ||
} | ||
|
||
// Provision sets up h. | ||
func (h *Handler) Provision(ctx caddy.Context) error { | ||
h.logger = ctx.Logger(h) | ||
if h.Headers != nil { | ||
err := h.Headers.Provision(ctx) | ||
if err != nil { | ||
return fmt.Errorf("provisioning header operations: %v", err) | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { | ||
pusher, ok := w.(http.Pusher) | ||
if !ok { | ||
return next.ServeHTTP(w, r) | ||
} | ||
|
||
// short-circuit recursive pushes | ||
if _, ok := r.Header[pushHeader]; ok { | ||
return next.ServeHTTP(w, r) | ||
} | ||
|
||
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) | ||
|
||
// create header for push requests | ||
hdr := h.initializePushHeaders(r, repl) | ||
|
||
// push first! | ||
for _, resource := range h.Resources { | ||
h.logger.Debug("pushing resource", | ||
zap.String("uri", r.RequestURI), | ||
zap.String("push_method", resource.Method), | ||
zap.String("push_target", resource.Target), | ||
zap.Object("push_headers", caddyhttp.LoggableHTTPHeader(hdr))) | ||
err := pusher.Push(repl.ReplaceAll(resource.Target, "."), &http.PushOptions{ | ||
Method: resource.Method, | ||
Header: hdr, | ||
}) | ||
if err != nil { | ||
// usually this means either that push is not | ||
// supported or concurrent streams are full | ||
break | ||
} | ||
} | ||
|
||
// wrap the response writer so that we can initiate push of any resources | ||
// described in Link header fields before the response is written | ||
lp := linkPusher{ | ||
ResponseWriterWrapper: &caddyhttp.ResponseWriterWrapper{ResponseWriter: w}, | ||
handler: h, | ||
pusher: pusher, | ||
header: hdr, | ||
request: r, | ||
} | ||
|
||
// serve only after pushing! | ||
if err := next.ServeHTTP(lp, r); err != nil { | ||
return err | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (h Handler) initializePushHeaders(r *http.Request, repl *caddy.Replacer) http.Header { | ||
hdr := make(http.Header) | ||
|
||
// prevent recursive pushes | ||
hdr.Set(pushHeader, "1") | ||
|
||
// set initial header fields; since exactly how headers should | ||
// be implemented for server push is not well-understood, we | ||
// are being conservative for now like httpd is: | ||
// https://httpd.apache.org/docs/2.4/en/howto/http2.html#push | ||
// we only copy some well-known, safe headers that are likely | ||
// crucial when requesting certain kinds of content | ||
for _, fieldName := range safeHeaders { | ||
if vals, ok := r.Header[fieldName]; ok { | ||
hdr[fieldName] = vals | ||
} | ||
} | ||
|
||
// user can customize the push request headers | ||
if h.Headers != nil { | ||
h.Headers.ApplyTo(hdr, repl) | ||
} | ||
|
||
return hdr | ||
} | ||
|
||
// servePreloadLinks parses Link headers from upstream and pushes | ||
// resources described by them. If a resource has the "nopush" | ||
// attribute or describes an external entity (meaning, the resource | ||
// URI includes a scheme), it will not be pushed. | ||
func (h Handler) servePreloadLinks(pusher http.Pusher, hdr http.Header, resources []string) { | ||
for _, resource := range resources { | ||
for _, resource := range parseLinkHeader(resource) { | ||
if _, ok := resource.params["nopush"]; ok { | ||
continue | ||
} | ||
if isRemoteResource(resource.uri) { | ||
continue | ||
} | ||
err := pusher.Push(resource.uri, &http.PushOptions{ | ||
Header: hdr, | ||
}) | ||
if err != nil { | ||
return | ||
} | ||
} | ||
} | ||
} | ||
|
||
// Resource represents a request for a resource to push. | ||
type Resource struct { | ||
// Method is the request method, which must be GET or HEAD. | ||
// Default is GET. | ||
Method string `json:"method,omitempty"` | ||
|
||
// Target is the path to the resource being pushed. | ||
Target string `json:"target,omitempty"` | ||
} | ||
|
||
// HeaderConfig configures headers for synthetic push requests. | ||
type HeaderConfig struct { | ||
headers.HeaderOps | ||
} | ||
|
||
// linkPusher is a http.ResponseWriter that intercepts | ||
// the WriteHeader() call to ensure that any resources | ||
// described by Link response headers get pushed before | ||
// the response is allowed to be written. | ||
type linkPusher struct { | ||
*caddyhttp.ResponseWriterWrapper | ||
handler Handler | ||
pusher http.Pusher | ||
header http.Header | ||
request *http.Request | ||
} | ||
|
||
func (lp linkPusher) WriteHeader(statusCode int) { | ||
if links, ok := lp.ResponseWriter.Header()["Link"]; ok { | ||
// only initiate these pushes if it hasn't been done yet | ||
if val := caddyhttp.GetVar(lp.request.Context(), pushedLink); val == nil { | ||
lp.handler.logger.Debug("pushing Link resources", zap.Strings("linked", links)) | ||
caddyhttp.SetVar(lp.request.Context(), pushedLink, true) | ||
lp.handler.servePreloadLinks(lp.pusher, lp.header, links) | ||
} | ||
} | ||
lp.ResponseWriter.WriteHeader(statusCode) | ||
} | ||
|
||
// isRemoteResource returns true if resource starts with | ||
// a scheme or is a protocol-relative URI. | ||
func isRemoteResource(resource string) bool { | ||
return strings.HasPrefix(resource, "//") || | ||
strings.HasPrefix(resource, "http://") || | ||
strings.HasPrefix(resource, "https://") | ||
} | ||
|
||
// safeHeaders is a list of header fields that are | ||
// safe to copy to push requests implicitly. It is | ||
// assumed that requests for certain kinds of content | ||
// would fail without these fields present. | ||
var safeHeaders = []string{ | ||
"Accept-Encoding", | ||
"Accept-Language", | ||
"Accept", | ||
"Cache-Control", | ||
"User-Agent", | ||
} | ||
|
||
// pushHeader is a header field that gets added to push requests | ||
// in order to avoid recursive/infinite pushes. | ||
const pushHeader = "Caddy-Push" | ||
|
||
// pushedLink is the key for the variable on the request | ||
// context that we use to remember whether we have already | ||
// pushed resources from Link headers yet; otherwise, if | ||
// multiple push handlers are invoked, it would repeat the | ||
// pushing of Link headers. | ||
const pushedLink = "http.handlers.push.pushed_link" | ||
|
||
// Interface guards | ||
var ( | ||
_ caddy.Provisioner = (*Handler)(nil) | ||
_ caddyhttp.MiddlewareHandler = (*Handler)(nil) | ||
_ caddyhttp.HTTPInterfaces = (*linkPusher)(nil) | ||
) |
Oops, something went wrong.