10
0

index.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  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 Negotiator = require('negotiator')
  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 negotiator = new Negotiator(req)
  141. var method = negotiator.encoding(['gzip', 'deflate', 'identity'], ['gzip'])
  142. // negotiation failed
  143. if (!method || method === 'identity') {
  144. nocompress('not acceptable')
  145. return
  146. }
  147. // compression stream
  148. debug('%s compression', method)
  149. stream = method === 'gzip'
  150. ? zlib.createGzip(opts)
  151. : zlib.createDeflate(opts)
  152. // add buffered listeners to stream
  153. addListeners(stream, stream.on, listeners)
  154. // header fields
  155. res.setHeader('Content-Encoding', method)
  156. res.removeHeader('Content-Length')
  157. // compression
  158. stream.on('data', function onStreamData (chunk) {
  159. if (_write.call(res, chunk) === false) {
  160. stream.pause()
  161. }
  162. })
  163. stream.on('end', function onStreamEnd () {
  164. _end.call(res)
  165. })
  166. _on.call(res, 'drain', function onResponseDrain () {
  167. stream.resume()
  168. })
  169. })
  170. next()
  171. }
  172. }
  173. /**
  174. * Add bufferred listeners to stream
  175. * @private
  176. */
  177. function addListeners (stream, on, listeners) {
  178. for (var i = 0; i < listeners.length; i++) {
  179. on.apply(stream, listeners[i])
  180. }
  181. }
  182. /**
  183. * Get the length of a given chunk
  184. */
  185. function chunkLength (chunk, encoding) {
  186. if (!chunk) {
  187. return 0
  188. }
  189. return !Buffer.isBuffer(chunk)
  190. ? Buffer.byteLength(chunk, encoding)
  191. : chunk.length
  192. }
  193. /**
  194. * Default filter function.
  195. * @private
  196. */
  197. function shouldCompress (req, res) {
  198. var type = res.getHeader('Content-Type')
  199. if (type === undefined || !compressible(type)) {
  200. debug('%s not compressible', type)
  201. return false
  202. }
  203. return true
  204. }
  205. /**
  206. * Determine if the entity should be transformed.
  207. * @private
  208. */
  209. function shouldTransform (req, res) {
  210. var cacheControl = res.getHeader('Cache-Control')
  211. // Don't compress for Cache-Control: no-transform
  212. // https://tools.ietf.org/html/rfc7234#section-5.2.2.4
  213. return !cacheControl ||
  214. !cacheControlNoTransformRegExp.test(cacheControl)
  215. }
  216. /**
  217. * Coerce arguments to Buffer
  218. * @private
  219. */
  220. function toBuffer (chunk, encoding) {
  221. return !Buffer.isBuffer(chunk)
  222. ? Buffer.from(chunk, encoding)
  223. : chunk
  224. }