Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 11, 01:00 UTC
| 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 | "go.uber.org/zap" |
| 23 | "go.uber.org/zap/zapcore" |
| 24 | |
| 25 | "github.com/caddyserver/caddy/v2" |
| 26 | "github.com/caddyserver/caddy/v2/modules/caddyhttp" |
| 27 | "github.com/caddyserver/caddy/v2/modules/caddyhttp/headers" |
| 28 | ) |
| 29 | |
| 30 | func init() { |
| 31 | caddy.RegisterModule(Handler{}) |
| 32 | } |
| 33 | |
| 34 | // Handler is a middleware for HTTP/2 server push. Note that |
| 35 | // HTTP/2 server push has been deprecated by some clients and |
| 36 | // its use is discouraged unless you can accurately predict |
| 37 | // which resources actually need to be pushed to the client; |
| 38 | // it can be difficult to know what the client already has |
| 39 | // cached. Pushing unnecessary resources results in worse |
| 40 | // performance. Consider using HTTP 103 Early Hints instead. |
| 41 | // |
| 42 | // This handler supports pushing from Link headers; in other |
| 43 | // words, if the eventual response has Link headers, this |
| 44 | // handler will push the resources indicated by those headers, |
| 45 | // even without specifying any resources in its config. |
| 46 | type Handler struct { |
| 47 | // The resources to push. |
| 48 | Resources []Resource `json:"resources,omitempty"` |
| 49 | |
| 50 | // Headers to modify for the push requests. |
| 51 | Headers *HeaderConfig `json:"headers,omitempty"` |
| 52 | |
| 53 | logger *zap.Logger |
| 54 | } |
| 55 | |
| 56 | // CaddyModule returns the Caddy module information. |
| 57 | func (Handler) CaddyModule() caddy.ModuleInfo { |
| 58 | return caddy.ModuleInfo{ |
| 59 | ID: "http.handlers.push", |
| 60 | New: func() caddy.Module { return new(Handler) }, |
| 61 | } |
| 62 | } |
| 63 | |
| 64 | // Provision sets up h. |
| 65 | func (h *Handler) Provision(ctx caddy.Context) error { |
| 66 | h.logger = ctx.Logger() |
| 67 | if h.Headers != nil { |
| 68 | err := h.Headers.Provision(ctx) |
| 69 | if err != nil { |
| 70 | return fmt.Errorf("provisioning header operations: %v", err) |
| 71 | } |
| 72 | } |
| 73 | return nil |
| 74 | } |
| 75 | |
| 76 | func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { |
| 77 | pusher, ok := w.(http.Pusher) |
| 78 | if !ok { |
| 79 | return next.ServeHTTP(w, r) |
| 80 | } |
| 81 | |
| 82 | // short-circuit recursive pushes |
| 83 | if _, ok := r.Header[pushHeader]; ok { |
| 84 | return next.ServeHTTP(w, r) |
| 85 | } |
| 86 | |
| 87 | repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) |
| 88 | server := r.Context().Value(caddyhttp.ServerCtxKey).(*caddyhttp.Server) |
| 89 | shouldLogCredentials := server.Logs != nil && server.Logs.ShouldLogCredentials |
| 90 | |
| 91 | // create header for push requests |
| 92 | hdr := h.initializePushHeaders(r, repl) |
| 93 | |
| 94 | // push first! |
| 95 | for _, resource := range h.Resources { |
| 96 | if c := h.logger.Check(zapcore.DebugLevel, "pushing resource"); c != nil { |
| 97 | c.Write( |
| 98 | zap.String("uri", r.RequestURI), |
| 99 | zap.String("push_method", resource.Method), |
| 100 | zap.String("push_target", resource.Target), |
| 101 | zap.Object("push_headers", caddyhttp.LoggableHTTPHeader{ |
| 102 | Header: hdr, |
| 103 | ShouldLogCredentials: shouldLogCredentials, |
| 104 | }), |
| 105 | ) |
| 106 | } |
| 107 | err := pusher.Push(repl.ReplaceAll(resource.Target, "."), &http.PushOptions{ |
| 108 | Method: resource.Method, |
| 109 | Header: hdr, |
| 110 | }) |
| 111 | if err != nil { |
| 112 | // usually this means either that push is not |
| 113 | // supported or concurrent streams are full |
| 114 | break |
| 115 | } |
| 116 | } |
| 117 | |
| 118 | // wrap the response writer so that we can initiate push of any resources |
| 119 | // described in Link header fields before the response is written |
| 120 | lp := linkPusher{ |
| 121 | ResponseWriterWrapper: &caddyhttp.ResponseWriterWrapper{ResponseWriter: w}, |
| 122 | handler: h, |
| 123 | pusher: pusher, |
| 124 | header: hdr, |
| 125 | request: r, |
| 126 | } |
| 127 | |
| 128 | // serve only after pushing! |
| 129 | if err := next.ServeHTTP(lp, r); err != nil { |
| 130 | return err |
| 131 | } |
| 132 | |
| 133 | return nil |
| 134 | } |
| 135 | |
| 136 | func (h Handler) initializePushHeaders(r *http.Request, repl *caddy.Replacer) http.Header { |
| 137 | hdr := make(http.Header) |
| 138 | |
| 139 | // prevent recursive pushes |
| 140 | hdr.Set(pushHeader, "1") |
| 141 | |
| 142 | // set initial header fields; since exactly how headers should |
| 143 | // be implemented for server push is not well-understood, we |
| 144 | // are being conservative for now like httpd is: |
| 145 | // https://httpd.apache.org/docs/2.4/en/howto/http2.html#push |
| 146 | // we only copy some well-known, safe headers that are likely |
| 147 | // crucial when requesting certain kinds of content |
| 148 | for _, fieldName := range safeHeaders { |
| 149 | if vals, ok := r.Header[fieldName]; ok { |
| 150 | hdr[fieldName] = vals |
| 151 | } |
| 152 | } |
| 153 | |
| 154 | // user can customize the push request headers |
| 155 | if h.Headers != nil { |
| 156 | h.Headers.ApplyTo(hdr, repl) |
| 157 | } |
| 158 | |
| 159 | return hdr |
| 160 | } |
| 161 | |
| 162 | // servePreloadLinks parses Link headers from upstream and pushes |
| 163 | // resources described by them. If a resource has the "nopush" |
| 164 | // attribute or describes an external entity (meaning, the resource |
| 165 | // URI includes a scheme), it will not be pushed. |
| 166 | func (h Handler) servePreloadLinks(pusher http.Pusher, hdr http.Header, resources []string) { |
| 167 | for _, resource := range resources { |
| 168 | for _, resource := range parseLinkHeader(resource) { |
| 169 | if _, ok := resource.params["nopush"]; ok { |
| 170 | continue |
| 171 | } |
| 172 | if isRemoteResource(resource.uri) { |
| 173 | continue |
| 174 | } |
| 175 | err := pusher.Push(resource.uri, &http.PushOptions{ |
| 176 | Header: hdr, |
| 177 | }) |
| 178 | if err != nil { |
| 179 | return |
| 180 | } |
| 181 | } |
| 182 | } |
| 183 | } |
| 184 | |
| 185 | // Resource represents a request for a resource to push. |
| 186 | type Resource struct { |
| 187 | // Method is the request method, which must be GET or HEAD. |
| 188 | // Default is GET. |
| 189 | Method string `json:"method,omitempty"` |
| 190 | |
| 191 | // Target is the path to the resource being pushed. |
| 192 | Target string `json:"target,omitempty"` |
| 193 | } |
| 194 | |
| 195 | // HeaderConfig configures headers for synthetic push requests. |
| 196 | type HeaderConfig struct { |
| 197 | headers.HeaderOps |
| 198 | } |
| 199 | |
| 200 | // linkPusher is a http.ResponseWriter that intercepts |
| 201 | // the WriteHeader() call to ensure that any resources |
| 202 | // described by Link response headers get pushed before |
| 203 | // the response is allowed to be written. |
| 204 | type linkPusher struct { |
| 205 | *caddyhttp.ResponseWriterWrapper |
| 206 | handler Handler |
| 207 | pusher http.Pusher |
| 208 | header http.Header |
| 209 | request *http.Request |
| 210 | } |
| 211 | |
| 212 | func (lp linkPusher) WriteHeader(statusCode int) { |
| 213 | if links, ok := lp.ResponseWriter.Header()["Link"]; ok { |
| 214 | // only initiate these pushes if it hasn't been done yet |
| 215 | if val := caddyhttp.GetVar(lp.request.Context(), pushedLink); val == nil { |
| 216 | if c := lp.handler.logger.Check(zapcore.DebugLevel, "pushing Link resources"); c != nil { |
| 217 | c.Write(zap.Strings("linked", links)) |
| 218 | } |
| 219 | caddyhttp.SetVar(lp.request.Context(), pushedLink, true) |
| 220 | lp.handler.servePreloadLinks(lp.pusher, lp.header, links) |
| 221 | } |
| 222 | } |
| 223 | lp.ResponseWriter.WriteHeader(statusCode) |
| 224 | } |
| 225 | |
| 226 | // isRemoteResource returns true if resource starts with |
| 227 | // a scheme or is a protocol-relative URI. |
| 228 | func isRemoteResource(resource string) bool { |
| 229 | return strings.HasPrefix(resource, "//") || |
| 230 | strings.HasPrefix(resource, "http://") || |
| 231 | strings.HasPrefix(resource, "https://") |
| 232 | } |
| 233 | |
| 234 | // safeHeaders is a list of header fields that are |
| 235 | // safe to copy to push requests implicitly. It is |
| 236 | // assumed that requests for certain kinds of content |
| 237 | // would fail without these fields present. |
| 238 | var safeHeaders = []string{ |
| 239 | "Accept-Encoding", |
| 240 | "Accept-Language", |
| 241 | "Accept", |
| 242 | "Cache-Control", |
| 243 | "User-Agent", |
| 244 | } |
| 245 | |
| 246 | // pushHeader is a header field that gets added to push requests |
| 247 | // in order to avoid recursive/infinite pushes. |
| 248 | const pushHeader = "Caddy-Push" |
| 249 | |
| 250 | // pushedLink is the key for the variable on the request |
| 251 | // context that we use to remember whether we have already |
| 252 | // pushed resources from Link headers yet; otherwise, if |
| 253 | // multiple push handlers are invoked, it would repeat the |
| 254 | // pushing of Link headers. |
| 255 | const pushedLink = "http.handlers.push.pushed_link" |
| 256 | |
| 257 | // Interface guards |
| 258 | var ( |
| 259 | _ caddy.Provisioner = (*Handler)(nil) |
| 260 | _ caddyhttp.MiddlewareHandler = (*Handler)(nil) |
| 261 | _ http.ResponseWriter = (*linkPusher)(nil) |
| 262 | _ http.Pusher = (*linkPusher)(nil) |
| 263 | ) |