wmod-proxy
Version:
Website Modification Proxy
402 lines (318 loc) • 16.8 kB
JavaScript
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.ProxyError = exports.MANIFEST_FILE = void 0;
exports["default"] = proxy;
var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
var _inherits2 = _interopRequireDefault(require("@babel/runtime/helpers/inherits"));
var _possibleConstructorReturn2 = _interopRequireDefault(require("@babel/runtime/helpers/possibleConstructorReturn"));
var _getPrototypeOf2 = _interopRequireDefault(require("@babel/runtime/helpers/getPrototypeOf"));
var _wrapNativeSuper2 = _interopRequireDefault(require("@babel/runtime/helpers/wrapNativeSuper"));
var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
var _fs = _interopRequireDefault(require("fs"));
var _path = require("path");
var _mockttp = require("mockttp");
var _service = require("./service");
var _indicator = require("./indicator");
var _logger = require("./logger");
var _model = require("./model");
function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = (0, _getPrototypeOf2["default"])(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = (0, _getPrototypeOf2["default"])(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return (0, _possibleConstructorReturn2["default"])(this, result); }; }
function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }
var MANIFEST_FILE = 'manifest.js';
exports.MANIFEST_FILE = MANIFEST_FILE;
function proxy(_x, _x2) {
return _proxy.apply(this, arguments);
}
function _proxy() {
_proxy = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee2(port, wmod) {
var manifestPath, manifestSource, manifest, logger, keys, server, reqResMap, caFingerprint;
return _regenerator["default"].wrap(function _callee2$(_context2) {
while (1) {
switch (_context2.prev = _context2.next) {
case 0:
manifestPath = (0, _path.join)(wmod, MANIFEST_FILE);
if ((0, _service.fileExists)(manifestPath)) {
_context2.next = 3;
break;
}
throw new ProxyError("Manifest file not found in: \"".concat(manifestPath, "\""));
case 3:
manifestSource = require((0, _path.resolve)(manifestPath));
manifest = (0, _service.readManifest)(manifestSource);
logger = (0, _logger.getLogger)(); // Get keys
_context2.next = 8;
return getSslKeys();
case 8:
keys = _context2.sent;
// Create server
server = (0, _mockttp.getLocal)({
https: keys,
http2: false
}); // Configure server
// An auxiliary map for tracking requests and responses
reqResMap = new Map();
manifest.rules.forEach(function (_ref, ruleId) {
var url = _ref.url,
hostname = _ref.hostname,
path = _ref.path,
action = _ref.action,
method = _ref.method;
// TODO: remove this later when mockttp can handle RegExp hostnames
if (hostname != null && hostname instanceof RegExp) {
throw new ProxyError('Not implemented: hostname is RegExp (1)');
}
logger.dbg("Action: method=".concat(method)); // Build rule
var builder;
builder = server.forAnyRequest();
if (path != null || url != null || hostname != null || method != null) {
builder = builder.matching(function (req) {
logger.dbg("".concat(ruleId, ": r------: [").concat($id(req), "] ").concat($method(req), " ").concat($url(req)));
if (method != null && req.method !== method) {
logger.dbg("".concat(ruleId, ": -M-----: [").concat($id(req), "] ").concat($method(req), " ").concat($url(req)));
return false;
}
var reqUrl = new URL(req.url);
if (hostname != null && reqUrl.hostname !== hostname) {
logger.dbg("".concat(ruleId, ": -H-----: [").concat($id(req), "] ").concat($method(req), " ").concat($url(req)));
return false;
}
if (path != null) {
if (path instanceof RegExp ? path.test(reqUrl.pathname) : path === reqUrl.pathname) {
logger.dbg("".concat(ruleId, ": -p-----: [").concat($id(req), "] ").concat($method(req), " ").concat($path(req)));
return true;
}
}
if (url != null) {
if (url instanceof RegExp ? url.test(req.url) : url === req.url) {
logger.dbg("".concat(ruleId, ": -u-----: [").concat($id(req), "] ").concat($method(req), " ").concat($url(req)));
return true;
}
}
logger.dbg("".concat(ruleId, ": -S-----: [").concat($id(req), "] ").concat($method(req), " ").concat($url(req)));
return false;
});
} // Check what kind of rule we have
if ((0, _model.isResolvedManifestRuleActionScripts)(action)) {
// If there are files to inject...
if (action.files.filter(function (_ref2) {
var inject = _ref2.inject;
return inject;
}).length > 0) {
// Try to inject
builder.thenPassThrough({
beforeRequest: function beforeRequest(req) {
reqResMap.set(req.id, req);
logger.dbg("".concat(ruleId, ": --r----: [").concat($id(req), "] ").concat($method(req), " ").concat($url(req)));
},
beforeResponse: function () {
var _beforeResponse = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee(res) {
var req, bodyText, headersAll, isReplaced;
return _regenerator["default"].wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
logger.dbg("".concat(ruleId, ": ---r---: [").concat($id(res), "]"));
if (reqResMap.has(res.id)) {
_context.next = 4;
break;
}
logger.dbg("".concat(ruleId, ": ----?--: [").concat($id(res), "]"));
return _context.abrupt("return", undefined);
case 4:
req = reqResMap.get(res.id);
if (isContentTypeHtml(res.headers)) {
_context.next = 8;
break;
}
logger.dbg("".concat(ruleId, ": ----T--: [").concat($id(req), "] ").concat($method(req), " ").concat($url(req)));
return _context.abrupt("return", undefined);
case 8:
// Remove used req-res map entry
reqResMap["delete"](res.id);
logger.dbg("".concat(ruleId, ": ----r--: [").concat($id(req), "] ").concat($method(req), " ").concat($url(req)));
_context.next = 12;
return res.body.getText();
case 12:
bodyText = _context.sent;
// Removing Content-Security-Policy
// TODO: ensure only object-src and script-src works
headersAll = res.headers;
if (headersAll['content-security-policy'] != null) {
delete headersAll['content-security-policy'];
}
if (!(bodyText != null)) {
_context.next = 20;
break;
}
// For debugging
// saveFile(`./${req.id}.html`, `${$method(req)} ${req.url}\n${bodyText}`);
isReplaced = false;
bodyText = bodyText.replace(/<body[^>]*>/, function (match) {
isReplaced = true;
var result = match + "<script>".concat((0, _indicator.getIndicatorScript)(manifest), "</script>");
action.files.filter(function (_ref3) {
var inject = _ref3.inject;
return inject;
}).forEach(function (file) {
var scriptName = (0, _path.basename)(file.path);
var scriptSitePath = "/".concat(scriptName);
logger.info("".concat(ruleId, ": -----i-: [").concat($id(req), "] ").concat($method(req), " ").concat($url(req), " <== \"").concat(scriptSitePath, "\""));
result += "<script src=\"".concat(scriptSitePath, "\"></script>");
});
return result;
});
if (isReplaced) {
_context.next = 20;
break;
}
throw new ProxyError('Cannot find the pattern for script injection');
case 20:
logger.dbg("".concat(ruleId, ": ------r: [").concat($id(req), "] ").concat($method(req), " ").concat($url(req)));
return _context.abrupt("return", {
body: bodyText,
headers: headersAll
});
case 22:
case "end":
return _context.stop();
}
}
}, _callee);
}));
function beforeResponse(_x3) {
return _beforeResponse.apply(this, arguments);
}
return beforeResponse;
}()
});
} else {
builder.thenPassThrough();
}
} else if ((0, _model.isManifestRuleActionResponse)(action)) {
builder.thenCallback(function (req) {
logger.info("".concat(ruleId, ": --s----: [").concat($id(req), "] ").concat($method(req), " ").concat($url(req)));
return {
statusCode: action.response
};
});
} else if ((0, _model.isManifestRuleActionClose)(action) && action.close) {
builder.thenCallback(function (req) {
logger.info("".concat(ruleId, ": --c----: [").concat($id(req), "] ").concat($method(req), " ").concat($url(req)));
return 'close';
});
} // Serving GET file requests
if ((0, _model.isResolvedManifestRuleActionScripts)(action)) {
action.files.forEach(function (file) {
var scriptName = (0, _path.basename)(file.path);
var scriptSitePath = "/".concat(scriptName); // Serving our injected scripts
var builder = server.forGet(scriptSitePath);
if (hostname != null) {
builder = builder.forHostname(hostname);
}
builder.thenCallback(function (req) {
logger.info("".concat(ruleId, ": --f----: [").concat($id(req), "] ").concat($method(req), " ").concat($url(req), " <== ").concat((0, _path.join)(wmod, file.path)));
return {
statusCode: 200,
headers: {
'content-type': 'application/javascript; charset=utf-8'
},
body: _fs["default"].readFileSync((0, _path.join)(wmod, file.path))
};
});
});
}
});
server.forUnmatchedRequest().thenPassThrough(); // Run server
_context2.next = 15;
return server.start(port);
case 15:
caFingerprint = (0, _mockttp.generateSPKIFingerprint)(keys.cert); // Print out the server details for manual configuration:
logger.info("Server running on port ".concat(server.port));
logger.info("CA cert fingerprint ".concat(caFingerprint));
return _context2.abrupt("return", server);
case 19:
case "end":
return _context2.stop();
}
}
}, _callee2);
}));
return _proxy.apply(this, arguments);
}
function truncate(str, n) {
return str.length > n ? str.substring(0, n - 1) + '...' : str;
}
function $id(obj) {
return obj.id.substring(0, 6);
}
function $url(obj) {
return truncate(obj.url, 100);
}
function $path(obj) {
return truncate(obj.path, 100);
}
function $method(obj) {
return obj.method.padEnd(7, ' ');
}
function getSslKeys() {
return _getSslKeys.apply(this, arguments);
}
function _getSslKeys() {
_getSslKeys = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee3() {
var keyPemPath, certPemPath, _yield$generateCACert, key, cert;
return _regenerator["default"].wrap(function _callee3$(_context3) {
while (1) {
switch (_context3.prev = _context3.next) {
case 0:
keyPemPath = 'cert/key.pem';
certPemPath = 'cert/cert.pem';
if (!(!(0, _service.fileExists)(keyPemPath) || !(0, _service.fileExists)(certPemPath))) {
_context3.next = 10;
break;
}
_context3.next = 5;
return (0, _mockttp.generateCACertificate)();
case 5:
_yield$generateCACert = _context3.sent;
key = _yield$generateCACert.key;
cert = _yield$generateCACert.cert;
(0, _service.saveFile)(keyPemPath, key);
(0, _service.saveFile)(certPemPath, cert);
case 10:
return _context3.abrupt("return", {
key: (0, _service.readFile)(keyPemPath),
cert: (0, _service.readFile)(certPemPath)
});
case 11:
case "end":
return _context3.stop();
}
}
}, _callee3);
}));
return _getSslKeys.apply(this, arguments);
}
function isContentTypeHtml(headers) {
return headers['content-type'] != null && /^text\/html/.test(headers['content-type']);
}
var ProxyError = /*#__PURE__*/function (_Error) {
(0, _inherits2["default"])(ProxyError, _Error);
var _super = _createSuper(ProxyError);
function ProxyError() {
(0, _classCallCheck2["default"])(this, ProxyError);
return _super.apply(this, arguments);
}
(0, _createClass2["default"])(ProxyError, [{
key: "toString",
value: function toString() {
return 'Proxy error:\n' + this.message;
}
}]);
return ProxyError;
}( /*#__PURE__*/(0, _wrapNativeSuper2["default"])(Error));
exports.ProxyError = ProxyError;
//# sourceMappingURL=proxy.js.map