caddy/modules/caddyhttp/push/handler.go

256 lines
7.7 KiB
Go

// 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 HTTP/2 server push. Note that
// HTTP/2 server push has been deprecated by some clients and
// its use is discouraged unless you can accurately predict
// which resources actually need to be pushed to the client;
// it can be difficult to know what the client already has
// cached. Pushing unnecessary resources results in worse
// performance. Consider using HTTP 103 Early Hints instead.
//
// This handler supports pushing from Link headers; in other
// words, if the eventual response has Link headers, this
// handler will push the resources indicated by those headers,
// even without specifying any resources in its config.
type Handler struct {
// The resources to push.
Resources []Resource `json:"resources,omitempty"`
// Headers to modify for the push requests.
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()
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)
server := r.Context().Value(caddyhttp.ServerCtxKey).(*caddyhttp.Server)
shouldLogCredentials := server.Logs != nil && server.Logs.ShouldLogCredentials
// 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{
Header: hdr,
ShouldLogCredentials: shouldLogCredentials,
}))
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)
)