187 lines
5.7 KiB
JavaScript
187 lines
5.7 KiB
JavaScript
|
"use strict";
|
||
|
|
||
|
var utils = require("./lib/utils");
|
||
|
var debug = require("debug")("resp-mod");
|
||
|
|
||
|
function RespModifier (opts) {
|
||
|
|
||
|
// options
|
||
|
opts = opts || {};
|
||
|
opts.blacklist = utils.toArray(opts.blacklist) || [];
|
||
|
opts.whitelist = utils.toArray(opts.whitelist) || [];
|
||
|
opts.hostBlacklist = utils.toArray(opts.hostBlacklist) || [];
|
||
|
opts.rules = opts.rules || [];
|
||
|
opts.ignore = opts.ignore || opts.excludeList || utils.defaultIgnoreTypes;
|
||
|
|
||
|
// helper functions
|
||
|
opts.regex = (function () {
|
||
|
var matches = opts.rules.map(function (item) {
|
||
|
return item.match.source;
|
||
|
}).join("|");
|
||
|
return new RegExp(matches);
|
||
|
})();
|
||
|
|
||
|
var respMod = this;
|
||
|
|
||
|
respMod.opts = opts;
|
||
|
respMod.middleware = respModifierMiddleware;
|
||
|
respMod.update = function (key, value) {
|
||
|
if (respMod.opts[key]) {
|
||
|
respMod.opts[key] = value;
|
||
|
}
|
||
|
return respMod;
|
||
|
};
|
||
|
|
||
|
function respModifierMiddleware(req, res, next) {
|
||
|
|
||
|
if (res._respModifier) {
|
||
|
debug("Reject req", req.url);
|
||
|
return next();
|
||
|
}
|
||
|
debug("Accept req", req.url);
|
||
|
|
||
|
res._respModifier = true;
|
||
|
|
||
|
var writeHead = res.writeHead;
|
||
|
var runPatches = true;
|
||
|
var write = res.write;
|
||
|
var end = res.end;
|
||
|
var singlerules = utils.isWhiteListedForSingle(req.url, respMod.opts.rules);
|
||
|
|
||
|
var withoutSingle = respMod.opts.rules.filter(function (rule) {
|
||
|
if (rule.paths && rule.paths.length) {
|
||
|
return false;
|
||
|
}
|
||
|
return true;
|
||
|
});
|
||
|
|
||
|
/**
|
||
|
* Exit early for blacklisted domains
|
||
|
*/
|
||
|
if (respMod.opts.hostBlacklist.indexOf(req.headers.host) > -1) {
|
||
|
return next();
|
||
|
}
|
||
|
|
||
|
if (singlerules.length) {
|
||
|
modifyResponse(singlerules, true);
|
||
|
} else {
|
||
|
if (utils.isWhitelisted(req.url, respMod.opts.whitelist)) {
|
||
|
modifyResponse(withoutSingle, true);
|
||
|
} else {
|
||
|
if (!utils.hasAcceptHeaders(req) || utils.inBlackList(req.url, respMod.opts)) {
|
||
|
debug("Black listed or no text/html headers", req.url);
|
||
|
return next();
|
||
|
} else {
|
||
|
modifyResponse(withoutSingle);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
next();
|
||
|
|
||
|
/**
|
||
|
* Actually do the overwrite
|
||
|
* @param {Array} rules
|
||
|
* @param {Boolean} [force] - if true, will always attempt to perform
|
||
|
* an overwrite - regardless of whether it appears to be HTML or not
|
||
|
*/
|
||
|
function modifyResponse(rules, force) {
|
||
|
|
||
|
req.headers["accept-encoding"] = "identity";
|
||
|
|
||
|
function restore() {
|
||
|
res.writeHead = writeHead;
|
||
|
res.write = write;
|
||
|
res.end = end;
|
||
|
}
|
||
|
|
||
|
res.push = function (chunk) {
|
||
|
res.data = (res.data || "") + chunk;
|
||
|
};
|
||
|
|
||
|
res.write = function (string, encoding) {
|
||
|
|
||
|
if (!runPatches) {
|
||
|
return write.call(res, string, encoding);
|
||
|
}
|
||
|
|
||
|
if (string !== undefined) {
|
||
|
var body = string instanceof Buffer ? string.toString(encoding) : string;
|
||
|
// If this chunk appears to be valid, push onto the res.data stack
|
||
|
if (force || (utils.isHtml(body) || utils.isHtml(res.data))) {
|
||
|
res.push(body);
|
||
|
} else {
|
||
|
restore();
|
||
|
return write.call(res, string, encoding);
|
||
|
}
|
||
|
}
|
||
|
return true;
|
||
|
};
|
||
|
|
||
|
res.writeHead = function () {
|
||
|
if (!runPatches) {
|
||
|
return writeHead.apply(res, arguments);
|
||
|
}
|
||
|
|
||
|
var headers = arguments[arguments.length - 1];
|
||
|
|
||
|
if (typeof headers === "object") {
|
||
|
for (var name in headers) {
|
||
|
if (/content-length/i.test(name)) {
|
||
|
delete headers[name];
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (res.getHeader("content-length")) {
|
||
|
res.removeHeader("content-length");
|
||
|
}
|
||
|
|
||
|
writeHead.apply(res, arguments);
|
||
|
};
|
||
|
|
||
|
res.end = function (string, encoding) {
|
||
|
|
||
|
res.data = res.data || "";
|
||
|
|
||
|
if (typeof string === "string") {
|
||
|
res.data += string;
|
||
|
}
|
||
|
|
||
|
if (string instanceof Buffer) {
|
||
|
res.data += string.toString();
|
||
|
}
|
||
|
|
||
|
if (!runPatches) {
|
||
|
return end.call(res, string, encoding);
|
||
|
}
|
||
|
|
||
|
// Check if our body is HTML, and if it does not already have the snippet.
|
||
|
if (force || utils.isHtml(res.data) && !utils.snip(res.data)) {
|
||
|
// Include, if necessary, replacing the entire res.data with the included snippet.
|
||
|
res.data = utils.applyRules(rules, res.data, req, res);
|
||
|
runPatches = false;
|
||
|
}
|
||
|
if (res.data !== undefined && !res._header) {
|
||
|
res.setHeader("content-length", Buffer.byteLength(res.data, encoding));
|
||
|
}
|
||
|
end.call(res, res.data, encoding);
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return respMod;
|
||
|
}
|
||
|
|
||
|
module.exports = function (opts) {
|
||
|
var resp = new RespModifier(opts);
|
||
|
return resp.middleware;
|
||
|
};
|
||
|
|
||
|
module.exports.create = function (opts) {
|
||
|
var resp = new RespModifier(opts);
|
||
|
return resp;
|
||
|
};
|
||
|
|
||
|
module.exports.utils = utils;
|