"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;