Matthias Andreas Benkard | 832a54e | 2019-01-29 09:27:38 +0100 | [diff] [blame] | 1 | package gziphandler |
| 2 | |
| 3 | import ( |
| 4 | "bufio" |
| 5 | "compress/gzip" |
| 6 | "fmt" |
| 7 | "io" |
| 8 | "net" |
| 9 | "net/http" |
| 10 | "strconv" |
| 11 | "strings" |
| 12 | "sync" |
| 13 | ) |
| 14 | |
| 15 | const ( |
| 16 | vary = "Vary" |
| 17 | acceptEncoding = "Accept-Encoding" |
| 18 | contentEncoding = "Content-Encoding" |
| 19 | contentType = "Content-Type" |
| 20 | contentLength = "Content-Length" |
| 21 | ) |
| 22 | |
| 23 | type codings map[string]float64 |
| 24 | |
| 25 | const ( |
| 26 | // DefaultQValue is the default qvalue to assign to an encoding if no explicit qvalue is set. |
| 27 | // This is actually kind of ambiguous in RFC 2616, so hopefully it's correct. |
| 28 | // The examples seem to indicate that it is. |
| 29 | DefaultQValue = 1.0 |
| 30 | |
| 31 | // 1500 bytes is the MTU size for the internet since that is the largest size allowed at the network layer. |
| 32 | // If you take a file that is 1300 bytes and compress it to 800 bytes, it’s still transmitted in that same 1500 byte packet regardless, so you’ve gained nothing. |
| 33 | // That being the case, you should restrict the gzip compression to files with a size greater than a single packet, 1400 bytes (1.4KB) is a safe value. |
| 34 | DefaultMinSize = 1400 |
| 35 | ) |
| 36 | |
| 37 | // gzipWriterPools stores a sync.Pool for each compression level for reuse of |
| 38 | // gzip.Writers. Use poolIndex to covert a compression level to an index into |
| 39 | // gzipWriterPools. |
| 40 | var gzipWriterPools [gzip.BestCompression - gzip.BestSpeed + 2]*sync.Pool |
| 41 | |
| 42 | func init() { |
| 43 | for i := gzip.BestSpeed; i <= gzip.BestCompression; i++ { |
| 44 | addLevelPool(i) |
| 45 | } |
| 46 | addLevelPool(gzip.DefaultCompression) |
| 47 | } |
| 48 | |
| 49 | // poolIndex maps a compression level to its index into gzipWriterPools. It |
| 50 | // assumes that level is a valid gzip compression level. |
| 51 | func poolIndex(level int) int { |
| 52 | // gzip.DefaultCompression == -1, so we need to treat it special. |
| 53 | if level == gzip.DefaultCompression { |
| 54 | return gzip.BestCompression - gzip.BestSpeed + 1 |
| 55 | } |
| 56 | return level - gzip.BestSpeed |
| 57 | } |
| 58 | |
| 59 | func addLevelPool(level int) { |
| 60 | gzipWriterPools[poolIndex(level)] = &sync.Pool{ |
| 61 | New: func() interface{} { |
| 62 | // NewWriterLevel only returns error on a bad level, we are guaranteeing |
| 63 | // that this will be a valid level so it is okay to ignore the returned |
| 64 | // error. |
| 65 | w, _ := gzip.NewWriterLevel(nil, level) |
| 66 | return w |
| 67 | }, |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | // GzipResponseWriter provides an http.ResponseWriter interface, which gzips |
| 72 | // bytes before writing them to the underlying response. This doesn't close the |
| 73 | // writers, so don't forget to do that. |
| 74 | // It can be configured to skip response smaller than minSize. |
| 75 | type GzipResponseWriter struct { |
| 76 | http.ResponseWriter |
| 77 | index int // Index for gzipWriterPools. |
| 78 | gw *gzip.Writer |
| 79 | |
| 80 | code int // Saves the WriteHeader value. |
| 81 | |
| 82 | minSize int // Specifed the minimum response size to gzip. If the response length is bigger than this value, it is compressed. |
| 83 | buf []byte // Holds the first part of the write before reaching the minSize or the end of the write. |
| 84 | |
| 85 | contentTypes []string // Only compress if the response is one of these content-types. All are accepted if empty. |
| 86 | } |
| 87 | |
| 88 | type GzipResponseWriterWithCloseNotify struct { |
| 89 | *GzipResponseWriter |
| 90 | } |
| 91 | |
| 92 | func (w GzipResponseWriterWithCloseNotify) CloseNotify() <-chan bool { |
| 93 | return w.ResponseWriter.(http.CloseNotifier).CloseNotify() |
| 94 | } |
| 95 | |
| 96 | // Write appends data to the gzip writer. |
| 97 | func (w *GzipResponseWriter) Write(b []byte) (int, error) { |
| 98 | // If content type is not set. |
| 99 | if _, ok := w.Header()[contentType]; !ok { |
| 100 | // It infer it from the uncompressed body. |
| 101 | w.Header().Set(contentType, http.DetectContentType(b)) |
| 102 | } |
| 103 | |
| 104 | // GZIP responseWriter is initialized. Use the GZIP responseWriter. |
| 105 | if w.gw != nil { |
| 106 | n, err := w.gw.Write(b) |
| 107 | return n, err |
| 108 | } |
| 109 | |
| 110 | // Save the write into a buffer for later use in GZIP responseWriter (if content is long enough) or at close with regular responseWriter. |
| 111 | // On the first write, w.buf changes from nil to a valid slice |
| 112 | w.buf = append(w.buf, b...) |
| 113 | |
| 114 | // If the global writes are bigger than the minSize and we're about to write |
| 115 | // a response containing a content type we want to handle, enable |
| 116 | // compression. |
| 117 | if len(w.buf) >= w.minSize && handleContentType(w.contentTypes, w) && w.Header().Get(contentEncoding) == "" { |
| 118 | err := w.startGzip() |
| 119 | if err != nil { |
| 120 | return 0, err |
| 121 | } |
| 122 | } |
| 123 | |
| 124 | return len(b), nil |
| 125 | } |
| 126 | |
| 127 | // startGzip initialize any GZIP specific informations. |
| 128 | func (w *GzipResponseWriter) startGzip() error { |
| 129 | |
| 130 | // Set the GZIP header. |
| 131 | w.Header().Set(contentEncoding, "gzip") |
| 132 | |
| 133 | // if the Content-Length is already set, then calls to Write on gzip |
| 134 | // will fail to set the Content-Length header since its already set |
| 135 | // See: https://github.com/golang/go/issues/14975. |
| 136 | w.Header().Del(contentLength) |
| 137 | |
| 138 | // Write the header to gzip response. |
| 139 | if w.code != 0 { |
| 140 | w.ResponseWriter.WriteHeader(w.code) |
| 141 | } |
| 142 | |
| 143 | // Initialize the GZIP response. |
| 144 | w.init() |
| 145 | |
| 146 | // Flush the buffer into the gzip response. |
| 147 | n, err := w.gw.Write(w.buf) |
| 148 | |
| 149 | // This should never happen (per io.Writer docs), but if the write didn't |
| 150 | // accept the entire buffer but returned no specific error, we have no clue |
| 151 | // what's going on, so abort just to be safe. |
| 152 | if err == nil && n < len(w.buf) { |
| 153 | return io.ErrShortWrite |
| 154 | } |
| 155 | |
| 156 | w.buf = nil |
| 157 | return err |
| 158 | } |
| 159 | |
| 160 | // WriteHeader just saves the response code until close or GZIP effective writes. |
| 161 | func (w *GzipResponseWriter) WriteHeader(code int) { |
| 162 | if w.code == 0 { |
| 163 | w.code = code |
| 164 | } |
| 165 | } |
| 166 | |
| 167 | // init graps a new gzip writer from the gzipWriterPool and writes the correct |
| 168 | // content encoding header. |
| 169 | func (w *GzipResponseWriter) init() { |
| 170 | // Bytes written during ServeHTTP are redirected to this gzip writer |
| 171 | // before being written to the underlying response. |
| 172 | gzw := gzipWriterPools[w.index].Get().(*gzip.Writer) |
| 173 | gzw.Reset(w.ResponseWriter) |
| 174 | w.gw = gzw |
| 175 | } |
| 176 | |
| 177 | // Close will close the gzip.Writer and will put it back in the gzipWriterPool. |
| 178 | func (w *GzipResponseWriter) Close() error { |
| 179 | if w.gw == nil { |
| 180 | // Gzip not trigged yet, write out regular response. |
| 181 | if w.code != 0 { |
| 182 | w.ResponseWriter.WriteHeader(w.code) |
| 183 | } |
| 184 | if w.buf != nil { |
| 185 | _, writeErr := w.ResponseWriter.Write(w.buf) |
| 186 | // Returns the error if any at write. |
| 187 | if writeErr != nil { |
| 188 | return fmt.Errorf("gziphandler: write to regular responseWriter at close gets error: %q", writeErr.Error()) |
| 189 | } |
| 190 | } |
| 191 | return nil |
| 192 | } |
| 193 | |
| 194 | err := w.gw.Close() |
| 195 | gzipWriterPools[w.index].Put(w.gw) |
| 196 | w.gw = nil |
| 197 | return err |
| 198 | } |
| 199 | |
| 200 | // Flush flushes the underlying *gzip.Writer and then the underlying |
| 201 | // http.ResponseWriter if it is an http.Flusher. This makes GzipResponseWriter |
| 202 | // an http.Flusher. |
| 203 | func (w *GzipResponseWriter) Flush() { |
| 204 | if w.gw == nil { |
| 205 | // Only flush once startGzip has been called. |
| 206 | // |
| 207 | // Flush is thus a no-op until the written body |
| 208 | // exceeds minSize. |
| 209 | return |
| 210 | } |
| 211 | |
| 212 | w.gw.Flush() |
| 213 | |
| 214 | if fw, ok := w.ResponseWriter.(http.Flusher); ok { |
| 215 | fw.Flush() |
| 216 | } |
| 217 | } |
| 218 | |
| 219 | // Hijack implements http.Hijacker. If the underlying ResponseWriter is a |
| 220 | // Hijacker, its Hijack method is returned. Otherwise an error is returned. |
| 221 | func (w *GzipResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { |
| 222 | if hj, ok := w.ResponseWriter.(http.Hijacker); ok { |
| 223 | return hj.Hijack() |
| 224 | } |
| 225 | return nil, nil, fmt.Errorf("http.Hijacker interface is not supported") |
| 226 | } |
| 227 | |
| 228 | // verify Hijacker interface implementation |
| 229 | var _ http.Hijacker = &GzipResponseWriter{} |
| 230 | |
| 231 | // MustNewGzipLevelHandler behaves just like NewGzipLevelHandler except that in |
| 232 | // an error case it panics rather than returning an error. |
| 233 | func MustNewGzipLevelHandler(level int) func(http.Handler) http.Handler { |
| 234 | wrap, err := NewGzipLevelHandler(level) |
| 235 | if err != nil { |
| 236 | panic(err) |
| 237 | } |
| 238 | return wrap |
| 239 | } |
| 240 | |
| 241 | // NewGzipLevelHandler returns a wrapper function (often known as middleware) |
| 242 | // which can be used to wrap an HTTP handler to transparently gzip the response |
| 243 | // body if the client supports it (via the Accept-Encoding header). Responses will |
| 244 | // be encoded at the given gzip compression level. An error will be returned only |
| 245 | // if an invalid gzip compression level is given, so if one can ensure the level |
| 246 | // is valid, the returned error can be safely ignored. |
| 247 | func NewGzipLevelHandler(level int) (func(http.Handler) http.Handler, error) { |
| 248 | return NewGzipLevelAndMinSize(level, DefaultMinSize) |
| 249 | } |
| 250 | |
| 251 | // NewGzipLevelAndMinSize behave as NewGzipLevelHandler except it let the caller |
| 252 | // specify the minimum size before compression. |
| 253 | func NewGzipLevelAndMinSize(level, minSize int) (func(http.Handler) http.Handler, error) { |
| 254 | return GzipHandlerWithOpts(CompressionLevel(level), MinSize(minSize)) |
| 255 | } |
| 256 | |
| 257 | func GzipHandlerWithOpts(opts ...option) (func(http.Handler) http.Handler, error) { |
| 258 | c := &config{ |
| 259 | level: gzip.DefaultCompression, |
| 260 | minSize: DefaultMinSize, |
| 261 | } |
| 262 | |
| 263 | for _, o := range opts { |
| 264 | o(c) |
| 265 | } |
| 266 | |
| 267 | if err := c.validate(); err != nil { |
| 268 | return nil, err |
| 269 | } |
| 270 | |
| 271 | return func(h http.Handler) http.Handler { |
| 272 | index := poolIndex(c.level) |
| 273 | |
| 274 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 275 | w.Header().Add(vary, acceptEncoding) |
| 276 | if acceptsGzip(r) { |
| 277 | gw := &GzipResponseWriter{ |
| 278 | ResponseWriter: w, |
| 279 | index: index, |
| 280 | minSize: c.minSize, |
| 281 | contentTypes: c.contentTypes, |
| 282 | } |
| 283 | defer gw.Close() |
| 284 | |
| 285 | if _, ok := w.(http.CloseNotifier); ok { |
| 286 | gwcn := GzipResponseWriterWithCloseNotify{gw} |
| 287 | h.ServeHTTP(gwcn, r) |
| 288 | } else { |
| 289 | h.ServeHTTP(gw, r) |
| 290 | } |
| 291 | |
| 292 | } else { |
| 293 | h.ServeHTTP(w, r) |
| 294 | } |
| 295 | }) |
| 296 | }, nil |
| 297 | } |
| 298 | |
| 299 | // Used for functional configuration. |
| 300 | type config struct { |
| 301 | minSize int |
| 302 | level int |
| 303 | contentTypes []string |
| 304 | } |
| 305 | |
| 306 | func (c *config) validate() error { |
| 307 | if c.level != gzip.DefaultCompression && (c.level < gzip.BestSpeed || c.level > gzip.BestCompression) { |
| 308 | return fmt.Errorf("invalid compression level requested: %d", c.level) |
| 309 | } |
| 310 | |
| 311 | if c.minSize < 0 { |
| 312 | return fmt.Errorf("minimum size must be more than zero") |
| 313 | } |
| 314 | |
| 315 | return nil |
| 316 | } |
| 317 | |
| 318 | type option func(c *config) |
| 319 | |
| 320 | func MinSize(size int) option { |
| 321 | return func(c *config) { |
| 322 | c.minSize = size |
| 323 | } |
| 324 | } |
| 325 | |
| 326 | func CompressionLevel(level int) option { |
| 327 | return func(c *config) { |
| 328 | c.level = level |
| 329 | } |
| 330 | } |
| 331 | |
| 332 | func ContentTypes(types []string) option { |
| 333 | return func(c *config) { |
| 334 | c.contentTypes = []string{} |
| 335 | for _, v := range types { |
| 336 | c.contentTypes = append(c.contentTypes, strings.ToLower(v)) |
| 337 | } |
| 338 | } |
| 339 | } |
| 340 | |
| 341 | // GzipHandler wraps an HTTP handler, to transparently gzip the response body if |
| 342 | // the client supports it (via the Accept-Encoding header). This will compress at |
| 343 | // the default compression level. |
| 344 | func GzipHandler(h http.Handler) http.Handler { |
| 345 | wrapper, _ := NewGzipLevelHandler(gzip.DefaultCompression) |
| 346 | return wrapper(h) |
| 347 | } |
| 348 | |
| 349 | // acceptsGzip returns true if the given HTTP request indicates that it will |
| 350 | // accept a gzipped response. |
| 351 | func acceptsGzip(r *http.Request) bool { |
| 352 | acceptedEncodings, _ := parseEncodings(r.Header.Get(acceptEncoding)) |
| 353 | return acceptedEncodings["gzip"] > 0.0 |
| 354 | } |
| 355 | |
| 356 | // returns true if we've been configured to compress the specific content type. |
| 357 | func handleContentType(contentTypes []string, w http.ResponseWriter) bool { |
| 358 | // If contentTypes is empty we handle all content types. |
| 359 | if len(contentTypes) == 0 { |
| 360 | return true |
| 361 | } |
| 362 | |
| 363 | ct := strings.ToLower(w.Header().Get(contentType)) |
| 364 | for _, c := range contentTypes { |
| 365 | if c == ct { |
| 366 | return true |
| 367 | } |
| 368 | } |
| 369 | |
| 370 | return false |
| 371 | } |
| 372 | |
| 373 | // parseEncodings attempts to parse a list of codings, per RFC 2616, as might |
| 374 | // appear in an Accept-Encoding header. It returns a map of content-codings to |
| 375 | // quality values, and an error containing the errors encountered. It's probably |
| 376 | // safe to ignore those, because silently ignoring errors is how the internet |
| 377 | // works. |
| 378 | // |
| 379 | // See: http://tools.ietf.org/html/rfc2616#section-14.3. |
| 380 | func parseEncodings(s string) (codings, error) { |
| 381 | c := make(codings) |
| 382 | var e []string |
| 383 | |
| 384 | for _, ss := range strings.Split(s, ",") { |
| 385 | coding, qvalue, err := parseCoding(ss) |
| 386 | |
| 387 | if err != nil { |
| 388 | e = append(e, err.Error()) |
| 389 | } else { |
| 390 | c[coding] = qvalue |
| 391 | } |
| 392 | } |
| 393 | |
| 394 | // TODO (adammck): Use a proper multi-error struct, so the individual errors |
| 395 | // can be extracted if anyone cares. |
| 396 | if len(e) > 0 { |
| 397 | return c, fmt.Errorf("errors while parsing encodings: %s", strings.Join(e, ", ")) |
| 398 | } |
| 399 | |
| 400 | return c, nil |
| 401 | } |
| 402 | |
| 403 | // parseCoding parses a single conding (content-coding with an optional qvalue), |
| 404 | // as might appear in an Accept-Encoding header. It attempts to forgive minor |
| 405 | // formatting errors. |
| 406 | func parseCoding(s string) (coding string, qvalue float64, err error) { |
| 407 | for n, part := range strings.Split(s, ";") { |
| 408 | part = strings.TrimSpace(part) |
| 409 | qvalue = DefaultQValue |
| 410 | |
| 411 | if n == 0 { |
| 412 | coding = strings.ToLower(part) |
| 413 | } else if strings.HasPrefix(part, "q=") { |
| 414 | qvalue, err = strconv.ParseFloat(strings.TrimPrefix(part, "q="), 64) |
| 415 | |
| 416 | if qvalue < 0.0 { |
| 417 | qvalue = 0.0 |
| 418 | } else if qvalue > 1.0 { |
| 419 | qvalue = 1.0 |
| 420 | } |
| 421 | } |
| 422 | } |
| 423 | |
| 424 | if coding == "" { |
| 425 | err = fmt.Errorf("empty content-coding") |
| 426 | } |
| 427 | |
| 428 | return |
| 429 | } |