@marcel91200 / caddy Mirror · read-only
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 11, 01:00 UTC

master
go 263 lines 7.89 KB
Raw
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 )