mirror of
				https://github.com/caddyserver/caddy.git
				synced 2025-10-31 10:37:24 -04:00 
			
		
		
		
	Ideally I'd just remove the parameter to caddy.Context.Logger(), but this would break most Caddy plugins. Instead, I'm making it variadic and marking it as partially deprecated. In the future, I might completely remove the parameter once most plugins have updated.
		
			
				
	
	
		
			256 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			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)
 | |
| )
 |