http-proxy-interceptor
Version:
middleware for node-http-proxy to modify responses using streams
167 lines (143 loc) • 4.77 kB
JavaScript
'use strict'
const zlib = require('zlib')
const stream = require('stream')
module.exports = function httpProxyInterceptor(interceptorFactory, filter) {
var _interceptorFactory = interceptorFactory
var _filter = filter || { url: /.*/ }
//Make all headers lowercase all the time for easier comparisons
if (_filter.headers) {
var headersLC = {}
for (var header in _filter.headers) {
headersLC[header.toLowerCase()] = _filter.headers[header]
}
_filter.headers = headersLC
}
function attachInterceptor(req, res, filter, factory) {
//Prepare interception framework
if (typeof res.interceptor === 'undefined') {
res.interceptor = new interceptor(res)
res.writeHead = function() {
var headers = (arguments.length > 2) ? arguments[2] : arguments[1] //writeHead() supports (statusCode, headers) as well as (statusCode, statusMessage, headers)
res.interceptor.checkHeaders(req, res, headers)
res.interceptor.writeHeadOrig.apply(res, arguments)
}
res.write = function(data, encoding) {
//In case writeHead() hasn't been called yet
res.interceptor.checkHeaders(req, res)
res.interceptor.write(data, encoding)
}
res.end = function(data, encoding) {
//In case writeHead() or write() haven't been called yet
res.interceptor.checkHeaders(req, res)
res.interceptor.end(data, encoding)
}
}
res.interceptor.add(filter, factory)
}
return function httpProxyInterceptor(req, res, next) {
if (!_filter.url || _filter.url.test(decodeURI(req.url))) {
attachInterceptor(req, res, _filter.headers, _interceptorFactory)
}
next()
}
}
class interceptor {
constructor(res) {
this.filters = new Array()
this.streamFactories = new Array()
this.streams = new Array()
this.decompressor = new stream.PassThrough()
this.compressor = new stream.PassThrough()
this.writeOrig = res.write
this.endOrig = res.end
this.writeHeadOrig = res.writeHead
this.headersChecked = false
}
add(filter, factory) {
this.filters.push(filter)
this.streamFactories.push(factory)
}
write(data, encoding) {
this.decompressor.write(data, encoding)
}
end(data, encoding) {
this.decompressor.end(data, encoding)
}
addStreams(idx, req, res) {
var streams = this.streamFactories[idx].call(null, req, res)
if (typeof streams[Symbol.iterator] === 'function') {
for (var stream of streams) {
this.streams.push(stream)
}
}
else {
this.streams.push(streams)
}
}
checkHeaders(req, res, headers) {
if (!this.headersChecked) {
for (var i = 0; i < this.filters.length; i++) {
//Always add streams where there was no header filter
if (typeof this.filters[i] === 'undefined') {
this.addStreams(i, req, res)
}
else {
var filterMatched = true
for (var headerName in this.filters[i]) {
var reqHeader = res.getHeader(headerName) || (headers ? headers[headerName] : undefined)
if (typeof reqHeader === 'undefined' || !this.filters[i][headerName].test(reqHeader)) {
filterMatched = false
break
}
}
if (filterMatched) {
this.addStreams(i, req, res)
}
}
}
if (this.streams.length > 0) {
//Remove content length headers since it must be sent chunked
res.removeHeader('content-length')
if (typeof headers === 'object') {
for (var header in headers) {
if (header.toLowerCase() == 'content-length')
delete headers[header]
}
}
var contentEncoding = res.getHeader('content-encoding')
if (typeof contentEncoding === 'undefined' && typeof headers === 'object') {
for (var header in headers) {
if (header.toLowerCase() == 'content-encoding')
contentEncoding = headers[header]
}
}
if (typeof contentEncoding !== 'undefined') {
if (/\bgzip\b/.test(contentEncoding)) { //not RFC compliant testing
this.compressor = zlib.createGzip()
this.decompressor = zlib.createGunzip()
}
else if (/\bdeflate\b/.test(contentEncoding)) {
this.compressor = zlib.createDeflate()
this.decompressor = zlib.createInflate()
}
}
this.decompressor.pipe(this.streams[0])
for (var i = 1; i < this.streams.length; i++) {
this.streams[i-1].pipe(this.streams[i])
}
this.streams[this.streams.length-1].pipe(this.compressor)
}
else {
//Write directly to the response if there is no interception
this.decompressor.pipe(this.compressor)
}
this.compressor.on('data', function(chunk) {
this.writeOrig.call(res, chunk)
}.bind(this))
this.compressor.on('end', function(chunk) {
this.endOrig.call(res, chunk)
}.bind(this))
this.headersChecked = true
}
} //checkHeaders()
} //class interceptor