289 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			289 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /*!
 | |
|  * compression
 | |
|  * Copyright(c) 2010 Sencha Inc.
 | |
|  * Copyright(c) 2011 TJ Holowaychuk
 | |
|  * Copyright(c) 2014 Jonathan Ong
 | |
|  * Copyright(c) 2014-2015 Douglas Christopher Wilson
 | |
|  * MIT Licensed
 | |
|  */
 | |
| 
 | |
| 'use strict'
 | |
| 
 | |
| /**
 | |
|  * Module dependencies.
 | |
|  * @private
 | |
|  */
 | |
| 
 | |
| var accepts = require('accepts')
 | |
| var Buffer = require('safe-buffer').Buffer
 | |
| var bytes = require('bytes')
 | |
| var compressible = require('compressible')
 | |
| var debug = require('debug')('compression')
 | |
| var onHeaders = require('on-headers')
 | |
| var vary = require('vary')
 | |
| var zlib = require('zlib')
 | |
| 
 | |
| /**
 | |
|  * Module exports.
 | |
|  */
 | |
| 
 | |
| module.exports = compression
 | |
| module.exports.filter = shouldCompress
 | |
| 
 | |
| /**
 | |
|  * Module variables.
 | |
|  * @private
 | |
|  */
 | |
| 
 | |
| var cacheControlNoTransformRegExp = /(?:^|,)\s*?no-transform\s*?(?:,|$)/
 | |
| 
 | |
| /**
 | |
|  * Compress response data with gzip / deflate.
 | |
|  *
 | |
|  * @param {Object} [options]
 | |
|  * @return {Function} middleware
 | |
|  * @public
 | |
|  */
 | |
| 
 | |
| function compression (options) {
 | |
|   var opts = options || {}
 | |
| 
 | |
|   // options
 | |
|   var filter = opts.filter || shouldCompress
 | |
|   var threshold = bytes.parse(opts.threshold)
 | |
| 
 | |
|   if (threshold == null) {
 | |
|     threshold = 1024
 | |
|   }
 | |
| 
 | |
|   return function compression (req, res, next) {
 | |
|     var ended = false
 | |
|     var length
 | |
|     var listeners = []
 | |
|     var stream
 | |
| 
 | |
|     var _end = res.end
 | |
|     var _on = res.on
 | |
|     var _write = res.write
 | |
| 
 | |
|     // flush
 | |
|     res.flush = function flush () {
 | |
|       if (stream) {
 | |
|         stream.flush()
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // proxy
 | |
| 
 | |
|     res.write = function write (chunk, encoding) {
 | |
|       if (ended) {
 | |
|         return false
 | |
|       }
 | |
| 
 | |
|       if (!this._header) {
 | |
|         this._implicitHeader()
 | |
|       }
 | |
| 
 | |
|       return stream
 | |
|         ? stream.write(toBuffer(chunk, encoding))
 | |
|         : _write.call(this, chunk, encoding)
 | |
|     }
 | |
| 
 | |
|     res.end = function end (chunk, encoding) {
 | |
|       if (ended) {
 | |
|         return false
 | |
|       }
 | |
| 
 | |
|       if (!this._header) {
 | |
|         // estimate the length
 | |
|         if (!this.getHeader('Content-Length')) {
 | |
|           length = chunkLength(chunk, encoding)
 | |
|         }
 | |
| 
 | |
|         this._implicitHeader()
 | |
|       }
 | |
| 
 | |
|       if (!stream) {
 | |
|         return _end.call(this, chunk, encoding)
 | |
|       }
 | |
| 
 | |
|       // mark ended
 | |
|       ended = true
 | |
| 
 | |
|       // write Buffer for Node.js 0.8
 | |
|       return chunk
 | |
|         ? stream.end(toBuffer(chunk, encoding))
 | |
|         : stream.end()
 | |
|     }
 | |
| 
 | |
|     res.on = function on (type, listener) {
 | |
|       if (!listeners || type !== 'drain') {
 | |
|         return _on.call(this, type, listener)
 | |
|       }
 | |
| 
 | |
|       if (stream) {
 | |
|         return stream.on(type, listener)
 | |
|       }
 | |
| 
 | |
|       // buffer listeners for future stream
 | |
|       listeners.push([type, listener])
 | |
| 
 | |
|       return this
 | |
|     }
 | |
| 
 | |
|     function nocompress (msg) {
 | |
|       debug('no compression: %s', msg)
 | |
|       addListeners(res, _on, listeners)
 | |
|       listeners = null
 | |
|     }
 | |
| 
 | |
|     onHeaders(res, function onResponseHeaders () {
 | |
|       // determine if request is filtered
 | |
|       if (!filter(req, res)) {
 | |
|         nocompress('filtered')
 | |
|         return
 | |
|       }
 | |
| 
 | |
|       // determine if the entity should be transformed
 | |
|       if (!shouldTransform(req, res)) {
 | |
|         nocompress('no transform')
 | |
|         return
 | |
|       }
 | |
| 
 | |
|       // vary
 | |
|       vary(res, 'Accept-Encoding')
 | |
| 
 | |
|       // content-length below threshold
 | |
|       if (Number(res.getHeader('Content-Length')) < threshold || length < threshold) {
 | |
|         nocompress('size below threshold')
 | |
|         return
 | |
|       }
 | |
| 
 | |
|       var encoding = res.getHeader('Content-Encoding') || 'identity'
 | |
| 
 | |
|       // already encoded
 | |
|       if (encoding !== 'identity') {
 | |
|         nocompress('already encoded')
 | |
|         return
 | |
|       }
 | |
| 
 | |
|       // head
 | |
|       if (req.method === 'HEAD') {
 | |
|         nocompress('HEAD request')
 | |
|         return
 | |
|       }
 | |
| 
 | |
|       // compression method
 | |
|       var accept = accepts(req)
 | |
|       var method = accept.encoding(['gzip', 'deflate', 'identity'])
 | |
| 
 | |
|       // we really don't prefer deflate
 | |
|       if (method === 'deflate' && accept.encoding(['gzip'])) {
 | |
|         method = accept.encoding(['gzip', 'identity'])
 | |
|       }
 | |
| 
 | |
|       // negotiation failed
 | |
|       if (!method || method === 'identity') {
 | |
|         nocompress('not acceptable')
 | |
|         return
 | |
|       }
 | |
| 
 | |
|       // compression stream
 | |
|       debug('%s compression', method)
 | |
|       stream = method === 'gzip'
 | |
|         ? zlib.createGzip(opts)
 | |
|         : zlib.createDeflate(opts)
 | |
| 
 | |
|       // add buffered listeners to stream
 | |
|       addListeners(stream, stream.on, listeners)
 | |
| 
 | |
|       // header fields
 | |
|       res.setHeader('Content-Encoding', method)
 | |
|       res.removeHeader('Content-Length')
 | |
| 
 | |
|       // compression
 | |
|       stream.on('data', function onStreamData (chunk) {
 | |
|         if (_write.call(res, chunk) === false) {
 | |
|           stream.pause()
 | |
|         }
 | |
|       })
 | |
| 
 | |
|       stream.on('end', function onStreamEnd () {
 | |
|         _end.call(res)
 | |
|       })
 | |
| 
 | |
|       _on.call(res, 'drain', function onResponseDrain () {
 | |
|         stream.resume()
 | |
|       })
 | |
|     })
 | |
| 
 | |
|     next()
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Add bufferred listeners to stream
 | |
|  * @private
 | |
|  */
 | |
| 
 | |
| function addListeners (stream, on, listeners) {
 | |
|   for (var i = 0; i < listeners.length; i++) {
 | |
|     on.apply(stream, listeners[i])
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Get the length of a given chunk
 | |
|  */
 | |
| 
 | |
| function chunkLength (chunk, encoding) {
 | |
|   if (!chunk) {
 | |
|     return 0
 | |
|   }
 | |
| 
 | |
|   return !Buffer.isBuffer(chunk)
 | |
|     ? Buffer.byteLength(chunk, encoding)
 | |
|     : chunk.length
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Default filter function.
 | |
|  * @private
 | |
|  */
 | |
| 
 | |
| function shouldCompress (req, res) {
 | |
|   var type = res.getHeader('Content-Type')
 | |
| 
 | |
|   if (type === undefined || !compressible(type)) {
 | |
|     debug('%s not compressible', type)
 | |
|     return false
 | |
|   }
 | |
| 
 | |
|   return true
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Determine if the entity should be transformed.
 | |
|  * @private
 | |
|  */
 | |
| 
 | |
| function shouldTransform (req, res) {
 | |
|   var cacheControl = res.getHeader('Cache-Control')
 | |
| 
 | |
|   // Don't compress for Cache-Control: no-transform
 | |
|   // https://tools.ietf.org/html/rfc7234#section-5.2.2.4
 | |
|   return !cacheControl ||
 | |
|     !cacheControlNoTransformRegExp.test(cacheControl)
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Coerce arguments to Buffer
 | |
|  * @private
 | |
|  */
 | |
| 
 | |
| function toBuffer (chunk, encoding) {
 | |
|   return !Buffer.isBuffer(chunk)
 | |
|     ? Buffer.from(chunk, encoding)
 | |
|     : chunk
 | |
| }
 |