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.

gzip.go 8.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. // Copyright 2019 The Gitea Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package gzip
  5. import (
  6. "bufio"
  7. "fmt"
  8. "io"
  9. "net"
  10. "net/http"
  11. "regexp"
  12. "strconv"
  13. "strings"
  14. "sync"
  15. "gitea.com/macaron/macaron"
  16. "github.com/klauspost/compress/gzip"
  17. )
  18. const (
  19. acceptEncodingHeader = "Accept-Encoding"
  20. contentEncodingHeader = "Content-Encoding"
  21. contentLengthHeader = "Content-Length"
  22. contentTypeHeader = "Content-Type"
  23. rangeHeader = "Range"
  24. varyHeader = "Vary"
  25. )
  26. const (
  27. // MinSize is the minimum size of content we will compress
  28. MinSize = 1400
  29. )
  30. // noopClosers are io.Writers with a shim to prevent early closure
  31. type noopCloser struct {
  32. io.Writer
  33. }
  34. func (noopCloser) Close() error { return nil }
  35. // WriterPool is a gzip writer pool to reduce workload on creation of
  36. // gzip writers
  37. type WriterPool struct {
  38. pool sync.Pool
  39. compressionLevel int
  40. }
  41. // NewWriterPool creates a new pool
  42. func NewWriterPool(compressionLevel int) *WriterPool {
  43. return &WriterPool{pool: sync.Pool{
  44. // New will return nil, we'll manage the creation of new
  45. // writers in the middleware
  46. New: func() interface{} { return nil },
  47. },
  48. compressionLevel: compressionLevel}
  49. }
  50. // Get a writer from the pool - or create one if not available
  51. func (wp *WriterPool) Get(rw macaron.ResponseWriter) *gzip.Writer {
  52. ret := wp.pool.Get()
  53. if ret == nil {
  54. ret, _ = gzip.NewWriterLevel(rw, wp.compressionLevel)
  55. } else {
  56. ret.(*gzip.Writer).Reset(rw)
  57. }
  58. return ret.(*gzip.Writer)
  59. }
  60. // Put returns a writer to the pool
  61. func (wp *WriterPool) Put(w *gzip.Writer) {
  62. wp.pool.Put(w)
  63. }
  64. var writerPool WriterPool
  65. // Options represents the configuration for the gzip middleware
  66. type Options struct {
  67. CompressionLevel int
  68. }
  69. func validateCompressionLevel(level int) bool {
  70. return level == gzip.DefaultCompression ||
  71. level == gzip.ConstantCompression ||
  72. (level >= gzip.BestSpeed && level <= gzip.BestCompression)
  73. }
  74. func validate(options []Options) Options {
  75. // Default to level 4 compression (Best results seem to be between 4 and 6)
  76. opt := Options{CompressionLevel: 4}
  77. if len(options) > 0 {
  78. opt = options[0]
  79. }
  80. if !validateCompressionLevel(opt.CompressionLevel) {
  81. opt.CompressionLevel = 4
  82. }
  83. return opt
  84. }
  85. // Middleware creates a macaron.Handler to proxy the response
  86. func Middleware(options ...Options) macaron.Handler {
  87. opt := validate(options)
  88. writerPool = *NewWriterPool(opt.CompressionLevel)
  89. regex := regexp.MustCompile(`bytes=(\d+)\-.*`)
  90. return func(ctx *macaron.Context) {
  91. // If the client won't accept gzip or x-gzip don't compress
  92. if !strings.Contains(ctx.Req.Header.Get(acceptEncodingHeader), "gzip") &&
  93. !strings.Contains(ctx.Req.Header.Get(acceptEncodingHeader), "x-gzip") {
  94. return
  95. }
  96. // If the client is asking for a specific range of bytes - don't compress
  97. if rangeHdr := ctx.Req.Header.Get(rangeHeader); rangeHdr != "" {
  98. match := regex.FindStringSubmatch(rangeHdr)
  99. if len(match) > 1 {
  100. return
  101. }
  102. }
  103. // OK we should proxy the response writer
  104. // We are still not necessarily going to compress...
  105. proxyWriter := &ProxyResponseWriter{
  106. ResponseWriter: ctx.Resp,
  107. }
  108. defer proxyWriter.Close()
  109. ctx.Resp = proxyWriter
  110. ctx.MapTo(proxyWriter, (*http.ResponseWriter)(nil))
  111. // Check if render middleware has been registered,
  112. // if yes, we need to modify ResponseWriter for it as well.
  113. if _, ok := ctx.Render.(*macaron.DummyRender); !ok {
  114. ctx.Render.SetResponseWriter(proxyWriter)
  115. }
  116. ctx.Next()
  117. }
  118. }
  119. // ProxyResponseWriter is a wrapped macaron ResponseWriter that may compress its contents
  120. type ProxyResponseWriter struct {
  121. writer io.WriteCloser
  122. macaron.ResponseWriter
  123. stopped bool
  124. code int
  125. buf []byte
  126. }
  127. // Write appends data to the proxied gzip writer.
  128. func (proxy *ProxyResponseWriter) Write(b []byte) (int, error) {
  129. // if writer is initialized, use the writer
  130. if proxy.writer != nil {
  131. return proxy.writer.Write(b)
  132. }
  133. proxy.buf = append(proxy.buf, b...)
  134. var (
  135. contentLength, _ = strconv.Atoi(proxy.Header().Get(contentLengthHeader))
  136. contentType = proxy.Header().Get(contentTypeHeader)
  137. contentEncoding = proxy.Header().Get(contentEncodingHeader)
  138. )
  139. // OK if an encoding hasn't been chosen, and content length > 1400
  140. // and content type isn't a compressed type
  141. if contentEncoding == "" &&
  142. (contentLength == 0 || contentLength >= MinSize) &&
  143. (contentType == "" || !compressedContentType(contentType)) {
  144. // If current buffer is less than the min size and a Content-Length isn't set, then wait
  145. if len(proxy.buf) < MinSize && contentLength == 0 {
  146. return len(b), nil
  147. }
  148. // If the Content-Length is larger than minSize or the current buffer is larger than minSize, then continue.
  149. if contentLength >= MinSize || len(proxy.buf) >= MinSize {
  150. // if we don't know the content type, infer it
  151. if contentType == "" {
  152. contentType = http.DetectContentType(proxy.buf)
  153. proxy.Header().Set(contentTypeHeader, contentType)
  154. }
  155. // If the Content-Type is not compressed - Compress!
  156. if !compressedContentType(contentType) {
  157. if err := proxy.startGzip(); err != nil {
  158. return 0, err
  159. }
  160. return len(b), nil
  161. }
  162. }
  163. }
  164. // If we got here, we should not GZIP this response.
  165. if err := proxy.startPlain(); err != nil {
  166. return 0, err
  167. }
  168. return len(b), nil
  169. }
  170. func (proxy *ProxyResponseWriter) startGzip() error {
  171. // Set the content-encoding and vary headers.
  172. proxy.Header().Set(contentEncodingHeader, "gzip")
  173. proxy.Header().Set(varyHeader, acceptEncodingHeader)
  174. // if the Content-Length is already set, then calls to Write on gzip
  175. // will fail to set the Content-Length header since its already set
  176. // See: https://github.com/golang/go/issues/14975.
  177. proxy.Header().Del(contentLengthHeader)
  178. // Write the header to gzip response.
  179. if proxy.code != 0 {
  180. proxy.ResponseWriter.WriteHeader(proxy.code)
  181. // Ensure that no other WriteHeader's happen
  182. proxy.code = 0
  183. }
  184. // Initialize and flush the buffer into the gzip response if there are any bytes.
  185. // If there aren't any, we shouldn't initialize it yet because on Close it will
  186. // write the gzip header even if nothing was ever written.
  187. if len(proxy.buf) > 0 {
  188. // Initialize the GZIP response.
  189. proxy.writer = writerPool.Get(proxy.ResponseWriter)
  190. return proxy.writeBuf()
  191. }
  192. return nil
  193. }
  194. func (proxy *ProxyResponseWriter) startPlain() error {
  195. if proxy.code != 0 {
  196. proxy.ResponseWriter.WriteHeader(proxy.code)
  197. proxy.code = 0
  198. }
  199. proxy.stopped = true
  200. proxy.writer = noopCloser{proxy.ResponseWriter}
  201. return proxy.writeBuf()
  202. }
  203. func (proxy *ProxyResponseWriter) writeBuf() error {
  204. if proxy.buf == nil {
  205. return nil
  206. }
  207. n, err := proxy.writer.Write(proxy.buf)
  208. // This should never happen (per io.Writer docs), but if the write didn't
  209. // accept the entire buffer but returned no specific error, we have no clue
  210. // what's going on, so abort just to be safe.
  211. if err == nil && n < len(proxy.buf) {
  212. err = io.ErrShortWrite
  213. }
  214. proxy.buf = nil
  215. return err
  216. }
  217. // WriteHeader will ensure that we have setup the writer before we write the header
  218. func (proxy *ProxyResponseWriter) WriteHeader(code int) {
  219. if proxy.code == 0 {
  220. proxy.code = code
  221. }
  222. }
  223. // Close the writer
  224. func (proxy *ProxyResponseWriter) Close() error {
  225. if proxy.stopped {
  226. return nil
  227. }
  228. if proxy.writer == nil {
  229. err := proxy.startPlain()
  230. if err != nil {
  231. return fmt.Errorf("GzipMiddleware: write to regular responseWriter at close gets error: %q", err.Error())
  232. }
  233. }
  234. err := proxy.writer.Close()
  235. if poolWriter, ok := proxy.writer.(*gzip.Writer); ok {
  236. writerPool.Put(poolWriter)
  237. }
  238. proxy.writer = nil
  239. proxy.stopped = true
  240. return err
  241. }
  242. // Flush the writer
  243. func (proxy *ProxyResponseWriter) Flush() {
  244. if proxy.writer == nil {
  245. return
  246. }
  247. if gw, ok := proxy.writer.(*gzip.Writer); ok {
  248. gw.Flush()
  249. }
  250. proxy.ResponseWriter.Flush()
  251. }
  252. // Hijack implements http.Hijacker. If the underlying ResponseWriter is a
  253. // Hijacker, its Hijack method is returned. Otherwise an error is returned.
  254. func (proxy *ProxyResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
  255. hijacker, ok := proxy.ResponseWriter.(http.Hijacker)
  256. if !ok {
  257. return nil, nil, fmt.Errorf("the ResponseWriter doesn't support the Hijacker interface")
  258. }
  259. return hijacker.Hijack()
  260. }
  261. // verify Hijacker interface implementation
  262. var _ http.Hijacker = &ProxyResponseWriter{}
  263. func compressedContentType(contentType string) bool {
  264. switch contentType {
  265. case "application/zip":
  266. return true
  267. case "application/x-gzip":
  268. return true
  269. case "application/gzip":
  270. return true
  271. default:
  272. return false
  273. }
  274. }