index.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. "use strict";
  2. var utils = require("./lib/utils");
  3. var debug = require("debug")("resp-mod");
  4. function RespModifier (opts) {
  5. // options
  6. opts = opts || {};
  7. opts.blacklist = utils.toArray(opts.blacklist) || [];
  8. opts.whitelist = utils.toArray(opts.whitelist) || [];
  9. opts.hostBlacklist = utils.toArray(opts.hostBlacklist) || [];
  10. opts.rules = opts.rules || [];
  11. opts.ignore = opts.ignore || opts.excludeList || utils.defaultIgnoreTypes;
  12. // helper functions
  13. opts.regex = (function () {
  14. var matches = opts.rules.map(function (item) {
  15. return item.match.source;
  16. }).join("|");
  17. return new RegExp(matches);
  18. })();
  19. var respMod = this;
  20. respMod.opts = opts;
  21. respMod.middleware = respModifierMiddleware;
  22. respMod.update = function (key, value) {
  23. if (respMod.opts[key]) {
  24. respMod.opts[key] = value;
  25. }
  26. return respMod;
  27. };
  28. function respModifierMiddleware(req, res, next) {
  29. if (res._respModifier) {
  30. debug("Reject req", req.url);
  31. return next();
  32. }
  33. debug("Accept req", req.url);
  34. res._respModifier = true;
  35. var writeHead = res.writeHead;
  36. var runPatches = true;
  37. var write = res.write;
  38. var end = res.end;
  39. var singlerules = utils.isWhiteListedForSingle(req.url, respMod.opts.rules);
  40. var withoutSingle = respMod.opts.rules.filter(function (rule) {
  41. if (rule.paths && rule.paths.length) {
  42. return false;
  43. }
  44. return true;
  45. });
  46. /**
  47. * Exit early for blacklisted domains
  48. */
  49. if (respMod.opts.hostBlacklist.indexOf(req.headers.host) > -1) {
  50. return next();
  51. }
  52. if (singlerules.length) {
  53. modifyResponse(singlerules, true);
  54. } else {
  55. if (utils.isWhitelisted(req.url, respMod.opts.whitelist)) {
  56. modifyResponse(withoutSingle, true);
  57. } else {
  58. if (!utils.hasAcceptHeaders(req) || utils.inBlackList(req.url, respMod.opts)) {
  59. debug("Black listed or no text/html headers", req.url);
  60. return next();
  61. } else {
  62. modifyResponse(withoutSingle);
  63. }
  64. }
  65. }
  66. next();
  67. /**
  68. * Actually do the overwrite
  69. * @param {Array} rules
  70. * @param {Boolean} [force] - if true, will always attempt to perform
  71. * an overwrite - regardless of whether it appears to be HTML or not
  72. */
  73. function modifyResponse(rules, force) {
  74. req.headers["accept-encoding"] = "identity";
  75. function restore() {
  76. res.writeHead = writeHead;
  77. res.write = write;
  78. res.end = end;
  79. }
  80. res.push = function (chunk) {
  81. res.data = (res.data || "") + chunk;
  82. };
  83. res.write = function (string, encoding) {
  84. if (!runPatches) {
  85. return write.call(res, string, encoding);
  86. }
  87. if (string !== undefined) {
  88. var body = string instanceof Buffer ? string.toString(encoding) : string;
  89. // If this chunk appears to be valid, push onto the res.data stack
  90. if (force || (utils.isHtml(body) || utils.isHtml(res.data))) {
  91. res.push(body);
  92. } else {
  93. restore();
  94. return write.call(res, string, encoding);
  95. }
  96. }
  97. return true;
  98. };
  99. res.writeHead = function () {
  100. if (!runPatches) {
  101. return writeHead.apply(res, arguments);
  102. }
  103. var headers = arguments[arguments.length - 1];
  104. if (typeof headers === "object") {
  105. for (var name in headers) {
  106. if (/content-length/i.test(name)) {
  107. delete headers[name];
  108. }
  109. }
  110. }
  111. if (res.getHeader("content-length")) {
  112. res.removeHeader("content-length");
  113. }
  114. writeHead.apply(res, arguments);
  115. };
  116. res.end = function (string, encoding) {
  117. res.data = res.data || "";
  118. if (typeof string === "string") {
  119. res.data += string;
  120. }
  121. if (string instanceof Buffer) {
  122. res.data += string.toString();
  123. }
  124. if (!runPatches) {
  125. return end.call(res, string, encoding);
  126. }
  127. // Check if our body is HTML, and if it does not already have the snippet.
  128. if (force || utils.isHtml(res.data) && !utils.snip(res.data)) {
  129. // Include, if necessary, replacing the entire res.data with the included snippet.
  130. res.data = utils.applyRules(rules, res.data, req, res);
  131. runPatches = false;
  132. }
  133. if (res.data !== undefined && !res._header) {
  134. res.setHeader("content-length", Buffer.byteLength(res.data, encoding));
  135. }
  136. end.call(res, res.data, encoding);
  137. };
  138. }
  139. }
  140. return respMod;
  141. }
  142. module.exports = function (opts) {
  143. var resp = new RespModifier(opts);
  144. return resp.middleware;
  145. };
  146. module.exports.create = function (opts) {
  147. var resp = new RespModifier(opts);
  148. return resp;
  149. };
  150. module.exports.utils = utils;