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 9.7 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  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. internal: 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. ctx.Resp = proxyWriter.internal
  118. }
  119. }
  120. // ProxyResponseWriter is a wrapped macaron ResponseWriter that may compress its contents
  121. type ProxyResponseWriter struct {
  122. writer io.WriteCloser
  123. internal macaron.ResponseWriter
  124. stopped bool
  125. code int
  126. buf []byte
  127. }
  128. // Header returns the header map
  129. func (proxy *ProxyResponseWriter) Header() http.Header {
  130. return proxy.internal.Header()
  131. }
  132. // Status returns the status code of the response or 0 if the response has not been written.
  133. func (proxy *ProxyResponseWriter) Status() int {
  134. if proxy.code != 0 {
  135. return proxy.code
  136. }
  137. return proxy.internal.Status()
  138. }
  139. // Written returns whether or not the ResponseWriter has been written.
  140. func (proxy *ProxyResponseWriter) Written() bool {
  141. if proxy.code != 0 {
  142. return true
  143. }
  144. return proxy.internal.Written()
  145. }
  146. // Size returns the size of the response body.
  147. func (proxy *ProxyResponseWriter) Size() int {
  148. return proxy.internal.Size()
  149. }
  150. // Before allows for a function to be called before the ResponseWriter has been written to. This is
  151. // useful for setting headers or any other operations that must happen before a response has been written.
  152. func (proxy *ProxyResponseWriter) Before(before macaron.BeforeFunc) {
  153. proxy.internal.Before(before)
  154. }
  155. // Write appends data to the proxied gzip writer.
  156. func (proxy *ProxyResponseWriter) Write(b []byte) (int, error) {
  157. // if writer is initialized, use the writer
  158. if proxy.writer != nil {
  159. return proxy.writer.Write(b)
  160. }
  161. proxy.buf = append(proxy.buf, b...)
  162. var (
  163. contentLength, _ = strconv.Atoi(proxy.Header().Get(contentLengthHeader))
  164. contentType = proxy.Header().Get(contentTypeHeader)
  165. contentEncoding = proxy.Header().Get(contentEncodingHeader)
  166. )
  167. // OK if an encoding hasn't been chosen, and content length > 1400
  168. // and content type isn't a compressed type
  169. if contentEncoding == "" &&
  170. (contentLength == 0 || contentLength >= MinSize) &&
  171. (contentType == "" || !compressedContentType(contentType)) {
  172. // If current buffer is less than the min size and a Content-Length isn't set, then wait
  173. if len(proxy.buf) < MinSize && contentLength == 0 {
  174. return len(b), nil
  175. }
  176. // If the Content-Length is larger than minSize or the current buffer is larger than minSize, then continue.
  177. if contentLength >= MinSize || len(proxy.buf) >= MinSize {
  178. // if we don't know the content type, infer it
  179. if contentType == "" {
  180. contentType = http.DetectContentType(proxy.buf)
  181. proxy.Header().Set(contentTypeHeader, contentType)
  182. }
  183. // If the Content-Type is not compressed - Compress!
  184. if !compressedContentType(contentType) {
  185. if err := proxy.startGzip(); err != nil {
  186. return 0, err
  187. }
  188. return len(b), nil
  189. }
  190. }
  191. }
  192. // If we got here, we should not GZIP this response.
  193. if err := proxy.startPlain(); err != nil {
  194. return 0, err
  195. }
  196. return len(b), nil
  197. }
  198. func (proxy *ProxyResponseWriter) startGzip() error {
  199. // Set the content-encoding and vary headers.
  200. proxy.Header().Set(contentEncodingHeader, "gzip")
  201. proxy.Header().Set(varyHeader, acceptEncodingHeader)
  202. // if the Content-Length is already set, then calls to Write on gzip
  203. // will fail to set the Content-Length header since its already set
  204. // See: https://github.com/golang/go/issues/14975.
  205. proxy.Header().Del(contentLengthHeader)
  206. // Write the header to gzip response.
  207. if proxy.code != 0 {
  208. proxy.internal.WriteHeader(proxy.code)
  209. // Ensure that no other WriteHeader's happen
  210. proxy.code = 0
  211. }
  212. // Initialize and flush the buffer into the gzip response if there are any bytes.
  213. // If there aren't any, we shouldn't initialize it yet because on Close it will
  214. // write the gzip header even if nothing was ever written.
  215. if len(proxy.buf) > 0 {
  216. // Initialize the GZIP response.
  217. proxy.writer = writerPool.Get(proxy.internal)
  218. return proxy.writeBuf()
  219. }
  220. return nil
  221. }
  222. func (proxy *ProxyResponseWriter) startPlain() error {
  223. if proxy.code != 0 {
  224. proxy.internal.WriteHeader(proxy.code)
  225. proxy.code = 0
  226. }
  227. proxy.stopped = true
  228. proxy.writer = noopCloser{proxy.internal}
  229. return proxy.writeBuf()
  230. }
  231. func (proxy *ProxyResponseWriter) writeBuf() error {
  232. if proxy.buf == nil {
  233. return nil
  234. }
  235. n, err := proxy.writer.Write(proxy.buf)
  236. // This should never happen (per io.Writer docs), but if the write didn't
  237. // accept the entire buffer but returned no specific error, we have no clue
  238. // what's going on, so abort just to be safe.
  239. if err == nil && n < len(proxy.buf) {
  240. err = io.ErrShortWrite
  241. }
  242. proxy.buf = nil
  243. return err
  244. }
  245. // WriteHeader will ensure that we have setup the writer before we write the header
  246. func (proxy *ProxyResponseWriter) WriteHeader(code int) {
  247. if proxy.code == 0 {
  248. proxy.code = code
  249. }
  250. }
  251. // Close the writer
  252. func (proxy *ProxyResponseWriter) Close() error {
  253. if proxy.stopped {
  254. return nil
  255. }
  256. if proxy.writer == nil {
  257. err := proxy.startPlain()
  258. if err != nil {
  259. return fmt.Errorf("GzipMiddleware: write to regular responseWriter at close gets error: %q", err.Error())
  260. }
  261. }
  262. err := proxy.writer.Close()
  263. if poolWriter, ok := proxy.writer.(*gzip.Writer); ok {
  264. writerPool.Put(poolWriter)
  265. }
  266. proxy.writer = nil
  267. proxy.stopped = true
  268. return err
  269. }
  270. // Flush the writer
  271. func (proxy *ProxyResponseWriter) Flush() {
  272. if proxy.writer == nil {
  273. return
  274. }
  275. if gw, ok := proxy.writer.(*gzip.Writer); ok {
  276. gw.Flush()
  277. }
  278. proxy.internal.Flush()
  279. }
  280. // Hijack implements http.Hijacker. If the underlying ResponseWriter is a
  281. // Hijacker, its Hijack method is returned. Otherwise an error is returned.
  282. func (proxy *ProxyResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
  283. hijacker, ok := proxy.internal.(http.Hijacker)
  284. if !ok {
  285. return nil, nil, fmt.Errorf("the ResponseWriter doesn't support the Hijacker interface")
  286. }
  287. return hijacker.Hijack()
  288. }
  289. // verify Hijacker interface implementation
  290. var _ http.Hijacker = &ProxyResponseWriter{}
  291. func compressedContentType(contentType string) bool {
  292. switch contentType {
  293. case "application/zip":
  294. return true
  295. case "application/x-gzip":
  296. return true
  297. case "application/gzip":
  298. return true
  299. default:
  300. return false
  301. }
  302. }