[6a3a178] | 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 |
|
---|
| 10 | 'use strict'
|
---|
| 11 |
|
---|
| 12 | /**
|
---|
| 13 | * Module dependencies.
|
---|
| 14 | * @private
|
---|
| 15 | */
|
---|
| 16 |
|
---|
| 17 | var accepts = require('accepts')
|
---|
| 18 | var Buffer = require('safe-buffer').Buffer
|
---|
| 19 | var bytes = require('bytes')
|
---|
| 20 | var compressible = require('compressible')
|
---|
| 21 | var debug = require('debug')('compression')
|
---|
| 22 | var onHeaders = require('on-headers')
|
---|
| 23 | var vary = require('vary')
|
---|
| 24 | var zlib = require('zlib')
|
---|
| 25 |
|
---|
| 26 | /**
|
---|
| 27 | * Module exports.
|
---|
| 28 | */
|
---|
| 29 |
|
---|
| 30 | module.exports = compression
|
---|
| 31 | module.exports.filter = shouldCompress
|
---|
| 32 |
|
---|
| 33 | /**
|
---|
| 34 | * Module variables.
|
---|
| 35 | * @private
|
---|
| 36 | */
|
---|
| 37 |
|
---|
| 38 | var cacheControlNoTransformRegExp = /(?:^|,)\s*?no-transform\s*?(?:,|$)/
|
---|
| 39 |
|
---|
| 40 | /**
|
---|
| 41 | * Compress response data with gzip / deflate.
|
---|
| 42 | *
|
---|
| 43 | * @param {Object} [options]
|
---|
| 44 | * @return {Function} middleware
|
---|
| 45 | * @public
|
---|
| 46 | */
|
---|
| 47 |
|
---|
| 48 | function compression (options) {
|
---|
| 49 | var opts = options || {}
|
---|
| 50 |
|
---|
| 51 | // options
|
---|
| 52 | var filter = opts.filter || shouldCompress
|
---|
| 53 | var threshold = bytes.parse(opts.threshold)
|
---|
| 54 |
|
---|
| 55 | if (threshold == null) {
|
---|
| 56 | threshold = 1024
|
---|
| 57 | }
|
---|
| 58 |
|
---|
| 59 | return function compression (req, res, next) {
|
---|
| 60 | var ended = false
|
---|
| 61 | var length
|
---|
| 62 | var listeners = []
|
---|
| 63 | var stream
|
---|
| 64 |
|
---|
| 65 | var _end = res.end
|
---|
| 66 | var _on = res.on
|
---|
| 67 | var _write = res.write
|
---|
| 68 |
|
---|
| 69 | // flush
|
---|
| 70 | res.flush = function flush () {
|
---|
| 71 | if (stream) {
|
---|
| 72 | stream.flush()
|
---|
| 73 | }
|
---|
| 74 | }
|
---|
| 75 |
|
---|
| 76 | // proxy
|
---|
| 77 |
|
---|
| 78 | res.write = function write (chunk, encoding) {
|
---|
| 79 | if (ended) {
|
---|
| 80 | return false
|
---|
| 81 | }
|
---|
| 82 |
|
---|
| 83 | if (!this._header) {
|
---|
| 84 | this._implicitHeader()
|
---|
| 85 | }
|
---|
| 86 |
|
---|
| 87 | return stream
|
---|
| 88 | ? stream.write(toBuffer(chunk, encoding))
|
---|
| 89 | : _write.call(this, chunk, encoding)
|
---|
| 90 | }
|
---|
| 91 |
|
---|
| 92 | res.end = function end (chunk, encoding) {
|
---|
| 93 | if (ended) {
|
---|
| 94 | return false
|
---|
| 95 | }
|
---|
| 96 |
|
---|
| 97 | if (!this._header) {
|
---|
| 98 | // estimate the length
|
---|
| 99 | if (!this.getHeader('Content-Length')) {
|
---|
| 100 | length = chunkLength(chunk, encoding)
|
---|
| 101 | }
|
---|
| 102 |
|
---|
| 103 | this._implicitHeader()
|
---|
| 104 | }
|
---|
| 105 |
|
---|
| 106 | if (!stream) {
|
---|
| 107 | return _end.call(this, chunk, encoding)
|
---|
| 108 | }
|
---|
| 109 |
|
---|
| 110 | // mark ended
|
---|
| 111 | ended = true
|
---|
| 112 |
|
---|
| 113 | // write Buffer for Node.js 0.8
|
---|
| 114 | return chunk
|
---|
| 115 | ? stream.end(toBuffer(chunk, encoding))
|
---|
| 116 | : stream.end()
|
---|
| 117 | }
|
---|
| 118 |
|
---|
| 119 | res.on = function on (type, listener) {
|
---|
| 120 | if (!listeners || type !== 'drain') {
|
---|
| 121 | return _on.call(this, type, listener)
|
---|
| 122 | }
|
---|
| 123 |
|
---|
| 124 | if (stream) {
|
---|
| 125 | return stream.on(type, listener)
|
---|
| 126 | }
|
---|
| 127 |
|
---|
| 128 | // buffer listeners for future stream
|
---|
| 129 | listeners.push([type, listener])
|
---|
| 130 |
|
---|
| 131 | return this
|
---|
| 132 | }
|
---|
| 133 |
|
---|
| 134 | function nocompress (msg) {
|
---|
| 135 | debug('no compression: %s', msg)
|
---|
| 136 | addListeners(res, _on, listeners)
|
---|
| 137 | listeners = null
|
---|
| 138 | }
|
---|
| 139 |
|
---|
| 140 | onHeaders(res, function onResponseHeaders () {
|
---|
| 141 | // determine if request is filtered
|
---|
| 142 | if (!filter(req, res)) {
|
---|
| 143 | nocompress('filtered')
|
---|
| 144 | return
|
---|
| 145 | }
|
---|
| 146 |
|
---|
| 147 | // determine if the entity should be transformed
|
---|
| 148 | if (!shouldTransform(req, res)) {
|
---|
| 149 | nocompress('no transform')
|
---|
| 150 | return
|
---|
| 151 | }
|
---|
| 152 |
|
---|
| 153 | // vary
|
---|
| 154 | vary(res, 'Accept-Encoding')
|
---|
| 155 |
|
---|
| 156 | // content-length below threshold
|
---|
| 157 | if (Number(res.getHeader('Content-Length')) < threshold || length < threshold) {
|
---|
| 158 | nocompress('size below threshold')
|
---|
| 159 | return
|
---|
| 160 | }
|
---|
| 161 |
|
---|
| 162 | var encoding = res.getHeader('Content-Encoding') || 'identity'
|
---|
| 163 |
|
---|
| 164 | // already encoded
|
---|
| 165 | if (encoding !== 'identity') {
|
---|
| 166 | nocompress('already encoded')
|
---|
| 167 | return
|
---|
| 168 | }
|
---|
| 169 |
|
---|
| 170 | // head
|
---|
| 171 | if (req.method === 'HEAD') {
|
---|
| 172 | nocompress('HEAD request')
|
---|
| 173 | return
|
---|
| 174 | }
|
---|
| 175 |
|
---|
| 176 | // compression method
|
---|
| 177 | var accept = accepts(req)
|
---|
| 178 | var method = accept.encoding(['gzip', 'deflate', 'identity'])
|
---|
| 179 |
|
---|
| 180 | // we really don't prefer deflate
|
---|
| 181 | if (method === 'deflate' && accept.encoding(['gzip'])) {
|
---|
| 182 | method = accept.encoding(['gzip', 'identity'])
|
---|
| 183 | }
|
---|
| 184 |
|
---|
| 185 | // negotiation failed
|
---|
| 186 | if (!method || method === 'identity') {
|
---|
| 187 | nocompress('not acceptable')
|
---|
| 188 | return
|
---|
| 189 | }
|
---|
| 190 |
|
---|
| 191 | // compression stream
|
---|
| 192 | debug('%s compression', method)
|
---|
| 193 | stream = method === 'gzip'
|
---|
| 194 | ? zlib.createGzip(opts)
|
---|
| 195 | : zlib.createDeflate(opts)
|
---|
| 196 |
|
---|
| 197 | // add buffered listeners to stream
|
---|
| 198 | addListeners(stream, stream.on, listeners)
|
---|
| 199 |
|
---|
| 200 | // header fields
|
---|
| 201 | res.setHeader('Content-Encoding', method)
|
---|
| 202 | res.removeHeader('Content-Length')
|
---|
| 203 |
|
---|
| 204 | // compression
|
---|
| 205 | stream.on('data', function onStreamData (chunk) {
|
---|
| 206 | if (_write.call(res, chunk) === false) {
|
---|
| 207 | stream.pause()
|
---|
| 208 | }
|
---|
| 209 | })
|
---|
| 210 |
|
---|
| 211 | stream.on('end', function onStreamEnd () {
|
---|
| 212 | _end.call(res)
|
---|
| 213 | })
|
---|
| 214 |
|
---|
| 215 | _on.call(res, 'drain', function onResponseDrain () {
|
---|
| 216 | stream.resume()
|
---|
| 217 | })
|
---|
| 218 | })
|
---|
| 219 |
|
---|
| 220 | next()
|
---|
| 221 | }
|
---|
| 222 | }
|
---|
| 223 |
|
---|
| 224 | /**
|
---|
| 225 | * Add bufferred listeners to stream
|
---|
| 226 | * @private
|
---|
| 227 | */
|
---|
| 228 |
|
---|
| 229 | function addListeners (stream, on, listeners) {
|
---|
| 230 | for (var i = 0; i < listeners.length; i++) {
|
---|
| 231 | on.apply(stream, listeners[i])
|
---|
| 232 | }
|
---|
| 233 | }
|
---|
| 234 |
|
---|
| 235 | /**
|
---|
| 236 | * Get the length of a given chunk
|
---|
| 237 | */
|
---|
| 238 |
|
---|
| 239 | function chunkLength (chunk, encoding) {
|
---|
| 240 | if (!chunk) {
|
---|
| 241 | return 0
|
---|
| 242 | }
|
---|
| 243 |
|
---|
| 244 | return !Buffer.isBuffer(chunk)
|
---|
| 245 | ? Buffer.byteLength(chunk, encoding)
|
---|
| 246 | : chunk.length
|
---|
| 247 | }
|
---|
| 248 |
|
---|
| 249 | /**
|
---|
| 250 | * Default filter function.
|
---|
| 251 | * @private
|
---|
| 252 | */
|
---|
| 253 |
|
---|
| 254 | function shouldCompress (req, res) {
|
---|
| 255 | var type = res.getHeader('Content-Type')
|
---|
| 256 |
|
---|
| 257 | if (type === undefined || !compressible(type)) {
|
---|
| 258 | debug('%s not compressible', type)
|
---|
| 259 | return false
|
---|
| 260 | }
|
---|
| 261 |
|
---|
| 262 | return true
|
---|
| 263 | }
|
---|
| 264 |
|
---|
| 265 | /**
|
---|
| 266 | * Determine if the entity should be transformed.
|
---|
| 267 | * @private
|
---|
| 268 | */
|
---|
| 269 |
|
---|
| 270 | function shouldTransform (req, res) {
|
---|
| 271 | var cacheControl = res.getHeader('Cache-Control')
|
---|
| 272 |
|
---|
| 273 | // Don't compress for Cache-Control: no-transform
|
---|
| 274 | // https://tools.ietf.org/html/rfc7234#section-5.2.2.4
|
---|
| 275 | return !cacheControl ||
|
---|
| 276 | !cacheControlNoTransformRegExp.test(cacheControl)
|
---|
| 277 | }
|
---|
| 278 |
|
---|
| 279 | /**
|
---|
| 280 | * Coerce arguments to Buffer
|
---|
| 281 | * @private
|
---|
| 282 | */
|
---|
| 283 |
|
---|
| 284 | function toBuffer (chunk, encoding) {
|
---|
| 285 | return !Buffer.isBuffer(chunk)
|
---|
| 286 | ? Buffer.from(chunk, encoding)
|
---|
| 287 | : chunk
|
---|
| 288 | }
|
---|