You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

169 lines
4.9 KiB

  1. package handlers
  2. import (
  3. "compress/gzip"
  4. "io"
  5. "net/http"
  6. )
  7. // Thanks to Andrew Gerrand for inspiration:
  8. // https://groups.google.com/d/msg/golang-nuts/eVnTcMwNVjM/4vYU8id9Q2UJ
  9. //
  10. // Also, node's Connect library implementation of the compress middleware:
  11. // https://github.com/senchalabs/connect/blob/master/lib/middleware/compress.js
  12. //
  13. // And StackOverflow's explanation of Vary: Accept-Encoding header:
  14. // http://stackoverflow.com/questions/7848796/what-does-varyaccept-encoding-mean
  15. // Internal gzipped writer that satisfies both the (body) writer in gzipped format,
  16. // and maintains the rest of the ResponseWriter interface for header manipulation.
  17. type gzipResponseWriter struct {
  18. io.Writer
  19. http.ResponseWriter
  20. r *http.Request // Keep a hold of the Request, for the filter function
  21. filtered bool // Has the request been run through the filter function?
  22. dogzip bool // Should we do GZIP compression for this request?
  23. filterFn func(http.ResponseWriter, *http.Request) bool
  24. }
  25. // Make sure the filter function is applied.
  26. func (w *gzipResponseWriter) applyFilter() {
  27. if !w.filtered {
  28. if w.dogzip = w.filterFn(w, w.r); w.dogzip {
  29. setGzipHeaders(w.Header())
  30. }
  31. w.filtered = true
  32. }
  33. }
  34. // Unambiguous Write() implementation (otherwise both ResponseWriter and Writer
  35. // want to claim this method).
  36. func (w *gzipResponseWriter) Write(b []byte) (int, error) {
  37. w.applyFilter()
  38. if w.dogzip {
  39. // Write compressed
  40. return w.Writer.Write(b)
  41. }
  42. // Write uncompressed
  43. return w.ResponseWriter.Write(b)
  44. }
  45. // Intercept the WriteHeader call to correctly set the GZIP headers.
  46. func (w *gzipResponseWriter) WriteHeader(code int) {
  47. w.applyFilter()
  48. w.ResponseWriter.WriteHeader(code)
  49. }
  50. // Implement WrapWriter interface
  51. func (w *gzipResponseWriter) WrappedWriter() http.ResponseWriter {
  52. return w.ResponseWriter
  53. }
  54. var (
  55. defaultFilterTypes = [...]string{
  56. "text",
  57. "javascript",
  58. "json",
  59. }
  60. )
  61. // Default filter to check if the response should be GZIPped.
  62. // By default, all text (html, css, xml, ...), javascript and json
  63. // content types are candidates for GZIP.
  64. func defaultFilter(w http.ResponseWriter, r *http.Request) bool {
  65. hdr := w.Header()
  66. for _, tp := range defaultFilterTypes {
  67. ok := HeaderMatch(hdr, "Content-Type", HmContains, tp)
  68. if ok {
  69. return true
  70. }
  71. }
  72. return false
  73. }
  74. // GZIPHandlerFunc is the same as GZIPHandler, it is just a convenience
  75. // signature that accepts a func(http.ResponseWriter, *http.Request) instead of
  76. // a http.Handler interface. It saves the boilerplate http.HandlerFunc() cast.
  77. func GZIPHandlerFunc(h http.HandlerFunc, filterFn func(http.ResponseWriter, *http.Request) bool) http.HandlerFunc {
  78. return GZIPHandler(h, filterFn)
  79. }
  80. // Gzip compression HTTP handler. If the client supports it, it compresses the response
  81. // written by the wrapped handler. The filter function is called when the response is about
  82. // to be written to determine if compression should be applied. If this argument is nil,
  83. // the default filter will GZIP only content types containing /json|text|javascript/.
  84. func GZIPHandler(h http.Handler, filterFn func(http.ResponseWriter, *http.Request) bool) http.HandlerFunc {
  85. if filterFn == nil {
  86. filterFn = defaultFilter
  87. }
  88. return func(w http.ResponseWriter, r *http.Request) {
  89. if _, ok := getGzipWriter(w); ok {
  90. // Self-awareness, gzip handler is already set up
  91. h.ServeHTTP(w, r)
  92. return
  93. }
  94. hdr := w.Header()
  95. setVaryHeader(hdr)
  96. // Do nothing on a HEAD request
  97. if r.Method == "HEAD" {
  98. h.ServeHTTP(w, r)
  99. return
  100. }
  101. if !acceptsGzip(r.Header) {
  102. // No gzip support from the client, return uncompressed
  103. h.ServeHTTP(w, r)
  104. return
  105. }
  106. // Prepare a gzip response container
  107. gz := gzip.NewWriter(w)
  108. gzw := &gzipResponseWriter{
  109. Writer: gz,
  110. ResponseWriter: w,
  111. r: r,
  112. filterFn: filterFn,
  113. }
  114. h.ServeHTTP(gzw, r)
  115. // Iff the handler completed successfully (no panic) and GZIP was indeed used, close the gzip writer,
  116. // which seems to generate a Write to the underlying writer.
  117. if gzw.dogzip {
  118. gz.Close()
  119. }
  120. }
  121. }
  122. // Add the vary by "accept-encoding" header if it is not already set.
  123. func setVaryHeader(hdr http.Header) {
  124. if !HeaderMatch(hdr, "Vary", HmContains, "accept-encoding") {
  125. hdr.Add("Vary", "Accept-Encoding")
  126. }
  127. }
  128. // Checks if the client accepts GZIP-encoded responses.
  129. func acceptsGzip(hdr http.Header) bool {
  130. ok := HeaderMatch(hdr, "Accept-Encoding", HmContains, "gzip")
  131. if !ok {
  132. ok = HeaderMatch(hdr, "Accept-Encoding", HmEquals, "*")
  133. }
  134. return ok
  135. }
  136. func setGzipHeaders(hdr http.Header) {
  137. // The content-type will be explicitly set somewhere down the path of handlers
  138. hdr.Set("Content-Encoding", "gzip")
  139. hdr.Del("Content-Length")
  140. }
  141. // Helper function to retrieve the gzip writer.
  142. func getGzipWriter(w http.ResponseWriter) (*gzipResponseWriter, bool) {
  143. gz, ok := GetResponseWriter(w, func(tst http.ResponseWriter) bool {
  144. _, ok := tst.(*gzipResponseWriter)
  145. return ok
  146. })
  147. if ok {
  148. return gz.(*gzipResponseWriter), true
  149. }
  150. return nil, false
  151. }