326 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			326 lines
		
	
	
		
			7.0 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 Negotiator = require('negotiator')
 | 
						|
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
 | 
						|
 | 
						|
/**
 | 
						|
 * @const
 | 
						|
 * whether current node version has brotli support
 | 
						|
 */
 | 
						|
var hasBrotliSupport = 'createBrotliCompress' in zlib
 | 
						|
 | 
						|
/**
 | 
						|
 * Module variables.
 | 
						|
 * @private
 | 
						|
 */
 | 
						|
var cacheControlNoTransformRegExp = /(?:^|,)\s*?no-transform\s*?(?:,|$)/
 | 
						|
var SUPPORTED_ENCODING = hasBrotliSupport ? ['br', 'gzip', 'deflate', 'identity'] : ['gzip', 'deflate', 'identity']
 | 
						|
var PREFERRED_ENCODING = hasBrotliSupport ? ['br', 'gzip'] : ['gzip']
 | 
						|
 | 
						|
var encodingSupported = ['gzip', 'deflate', 'identity', 'br']
 | 
						|
 | 
						|
/**
 | 
						|
 * Compress response data with gzip / deflate.
 | 
						|
 *
 | 
						|
 * @param {Object} [options]
 | 
						|
 * @return {Function} middleware
 | 
						|
 * @public
 | 
						|
 */
 | 
						|
 | 
						|
function compression (options) {
 | 
						|
  var opts = options || {}
 | 
						|
  var optsBrotli = {}
 | 
						|
 | 
						|
  if (hasBrotliSupport) {
 | 
						|
    Object.assign(optsBrotli, opts.brotli)
 | 
						|
 | 
						|
    var brotliParams = {}
 | 
						|
    brotliParams[zlib.constants.BROTLI_PARAM_QUALITY] = 4
 | 
						|
 | 
						|
    // set the default level to a reasonable value with balanced speed/ratio
 | 
						|
    optsBrotli.params = Object.assign(brotliParams, optsBrotli.params)
 | 
						|
  }
 | 
						|
 | 
						|
  // options
 | 
						|
  var filter = opts.filter || shouldCompress
 | 
						|
  var threshold = bytes.parse(opts.threshold)
 | 
						|
  var enforceEncoding = opts.enforceEncoding || 'identity'
 | 
						|
 | 
						|
  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 (!headersSent(res)) {
 | 
						|
        this.writeHead(this.statusCode)
 | 
						|
      }
 | 
						|
 | 
						|
      return stream
 | 
						|
        ? stream.write(toBuffer(chunk, encoding))
 | 
						|
        : _write.call(this, chunk, encoding)
 | 
						|
    }
 | 
						|
 | 
						|
    res.end = function end (chunk, encoding) {
 | 
						|
      if (ended) {
 | 
						|
        return false
 | 
						|
      }
 | 
						|
 | 
						|
      if (!headersSent(res)) {
 | 
						|
        // estimate the length
 | 
						|
        if (!this.getHeader('Content-Length')) {
 | 
						|
          length = chunkLength(chunk, encoding)
 | 
						|
        }
 | 
						|
 | 
						|
        this.writeHead(this.statusCode)
 | 
						|
      }
 | 
						|
 | 
						|
      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 negotiator = new Negotiator(req)
 | 
						|
      var method = negotiator.encoding(SUPPORTED_ENCODING, PREFERRED_ENCODING)
 | 
						|
 | 
						|
      // if no method is found, use the default encoding
 | 
						|
      if (!req.headers['accept-encoding'] && encodingSupported.indexOf(enforceEncoding) !== -1) {
 | 
						|
        method = enforceEncoding
 | 
						|
      }
 | 
						|
 | 
						|
      // negotiation failed
 | 
						|
      if (!method || method === 'identity') {
 | 
						|
        nocompress('not acceptable')
 | 
						|
        return
 | 
						|
      }
 | 
						|
 | 
						|
      // compression stream
 | 
						|
      debug('%s compression', method)
 | 
						|
      stream = method === 'gzip'
 | 
						|
        ? zlib.createGzip(opts)
 | 
						|
        : method === 'br'
 | 
						|
          ? zlib.createBrotliCompress(optsBrotli)
 | 
						|
          : 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)
 | 
						|
    ? chunk.length
 | 
						|
    : Buffer.byteLength(chunk, encoding)
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * 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)
 | 
						|
    ? chunk
 | 
						|
    : Buffer.from(chunk, encoding)
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Determine if the response headers have been sent.
 | 
						|
 *
 | 
						|
 * @param {object} res
 | 
						|
 * @returns {boolean}
 | 
						|
 * @private
 | 
						|
 */
 | 
						|
 | 
						|
function headersSent (res) {
 | 
						|
  return typeof res.headersSent !== 'boolean'
 | 
						|
    ? Boolean(res._header)
 | 
						|
    : res.headersSent
 | 
						|
}
 |