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
288 lines
5.8 KiB
/*! |
|
* 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 |
|
}
|
|
|