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.

288 lines
5.8 KiB

4 years ago
  1. /*!
  2. * compression
  3. * Copyright(c) 2010 Sencha Inc.
  4. * Copyright(c) 2011 TJ Holowaychuk
  5. * Copyright(c) 2014 Jonathan Ong
  6. * Copyright(c) 2014-2015 Douglas Christopher Wilson
  7. * MIT Licensed
  8. */
  9. 'use strict'
  10. /**
  11. * Module dependencies.
  12. * @private
  13. */
  14. var accepts = require('accepts')
  15. var Buffer = require('safe-buffer').Buffer
  16. var bytes = require('bytes')
  17. var compressible = require('compressible')
  18. var debug = require('debug')('compression')
  19. var onHeaders = require('on-headers')
  20. var vary = require('vary')
  21. var zlib = require('zlib')
  22. /**
  23. * Module exports.
  24. */
  25. module.exports = compression
  26. module.exports.filter = shouldCompress
  27. /**
  28. * Module variables.
  29. * @private
  30. */
  31. var cacheControlNoTransformRegExp = /(?:^|,)\s*?no-transform\s*?(?:,|$)/
  32. /**
  33. * Compress response data with gzip / deflate.
  34. *
  35. * @param {Object} [options]
  36. * @return {Function} middleware
  37. * @public
  38. */
  39. function compression (options) {
  40. var opts = options || {}
  41. // options
  42. var filter = opts.filter || shouldCompress
  43. var threshold = bytes.parse(opts.threshold)
  44. if (threshold == null) {
  45. threshold = 1024
  46. }
  47. return function compression (req, res, next) {
  48. var ended = false
  49. var length
  50. var listeners = []
  51. var stream
  52. var _end = res.end
  53. var _on = res.on
  54. var _write = res.write
  55. // flush
  56. res.flush = function flush () {
  57. if (stream) {
  58. stream.flush()
  59. }
  60. }
  61. // proxy
  62. res.write = function write (chunk, encoding) {
  63. if (ended) {
  64. return false
  65. }
  66. if (!this._header) {
  67. this._implicitHeader()
  68. }
  69. return stream
  70. ? stream.write(toBuffer(chunk, encoding))
  71. : _write.call(this, chunk, encoding)
  72. }
  73. res.end = function end (chunk, encoding) {
  74. if (ended) {
  75. return false
  76. }
  77. if (!this._header) {
  78. // estimate the length
  79. if (!this.getHeader('Content-Length')) {
  80. length = chunkLength(chunk, encoding)
  81. }
  82. this._implicitHeader()
  83. }
  84. if (!stream) {
  85. return _end.call(this, chunk, encoding)
  86. }
  87. // mark ended
  88. ended = true
  89. // write Buffer for Node.js 0.8
  90. return chunk
  91. ? stream.end(toBuffer(chunk, encoding))
  92. : stream.end()
  93. }
  94. res.on = function on (type, listener) {
  95. if (!listeners || type !== 'drain') {
  96. return _on.call(this, type, listener)
  97. }
  98. if (stream) {
  99. return stream.on(type, listener)
  100. }
  101. // buffer listeners for future stream
  102. listeners.push([type, listener])
  103. return this
  104. }
  105. function nocompress (msg) {
  106. debug('no compression: %s', msg)
  107. addListeners(res, _on, listeners)
  108. listeners = null
  109. }
  110. onHeaders(res, function onResponseHeaders () {
  111. // determine if request is filtered
  112. if (!filter(req, res)) {
  113. nocompress('filtered')
  114. return
  115. }
  116. // determine if the entity should be transformed
  117. if (!shouldTransform(req, res)) {
  118. nocompress('no transform')
  119. return
  120. }
  121. // vary
  122. vary(res, 'Accept-Encoding')
  123. // content-length below threshold
  124. if (Number(res.getHeader('Content-Length')) < threshold || length < threshold) {
  125. nocompress('size below threshold')
  126. return
  127. }
  128. var encoding = res.getHeader('Content-Encoding') || 'identity'
  129. // already encoded
  130. if (encoding !== 'identity') {
  131. nocompress('already encoded')
  132. return
  133. }
  134. // head
  135. if (req.method === 'HEAD') {
  136. nocompress('HEAD request')
  137. return
  138. }
  139. // compression method
  140. var accept = accepts(req)
  141. var method = accept.encoding(['gzip', 'deflate', 'identity'])
  142. // we really don't prefer deflate
  143. if (method === 'deflate' && accept.encoding(['gzip'])) {
  144. method = accept.encoding(['gzip', 'identity'])
  145. }
  146. // negotiation failed
  147. if (!method || method === 'identity') {
  148. nocompress('not acceptable')
  149. return
  150. }
  151. // compression stream
  152. debug('%s compression', method)
  153. stream = method === 'gzip'
  154. ? zlib.createGzip(opts)
  155. : zlib.createDeflate(opts)
  156. // add buffered listeners to stream
  157. addListeners(stream, stream.on, listeners)
  158. // header fields
  159. res.setHeader('Content-Encoding', method)
  160. res.removeHeader('Content-Length')
  161. // compression
  162. stream.on('data', function onStreamData (chunk) {
  163. if (_write.call(res, chunk) === false) {
  164. stream.pause()
  165. }
  166. })
  167. stream.on('end', function onStreamEnd () {
  168. _end.call(res)
  169. })
  170. _on.call(res, 'drain', function onResponseDrain () {
  171. stream.resume()
  172. })
  173. })
  174. next()
  175. }
  176. }
  177. /**
  178. * Add bufferred listeners to stream
  179. * @private
  180. */
  181. function addListeners (stream, on, listeners) {
  182. for (var i = 0; i < listeners.length; i++) {
  183. on.apply(stream, listeners[i])
  184. }
  185. }
  186. /**
  187. * Get the length of a given chunk
  188. */
  189. function chunkLength (chunk, encoding) {
  190. if (!chunk) {
  191. return 0
  192. }
  193. return !Buffer.isBuffer(chunk)
  194. ? Buffer.byteLength(chunk, encoding)
  195. : chunk.length
  196. }
  197. /**
  198. * Default filter function.
  199. * @private
  200. */
  201. function shouldCompress (req, res) {
  202. var type = res.getHeader('Content-Type')
  203. if (type === undefined || !compressible(type)) {
  204. debug('%s not compressible', type)
  205. return false
  206. }
  207. return true
  208. }
  209. /**
  210. * Determine if the entity should be transformed.
  211. * @private
  212. */
  213. function shouldTransform (req, res) {
  214. var cacheControl = res.getHeader('Cache-Control')
  215. // Don't compress for Cache-Control: no-transform
  216. // https://tools.ietf.org/html/rfc7234#section-5.2.2.4
  217. return !cacheControl ||
  218. !cacheControlNoTransformRegExp.test(cacheControl)
  219. }
  220. /**
  221. * Coerce arguments to Buffer
  222. * @private
  223. */
  224. function toBuffer (chunk, encoding) {
  225. return !Buffer.isBuffer(chunk)
  226. ? Buffer.from(chunk, encoding)
  227. : chunk
  228. }