| /*! |
| * 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(Buffer.from(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(Buffer.from(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) |
| } |