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

gzip.go 8.7 kB

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