forest-express
Version:
Official package for all Forest Express Lianas
138 lines (133 loc) • 5.94 kB
JavaScript
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
var superagent = require('superagent');
var path = require('../services/path');
var auth = require('../services/auth');
// Never forwarded (request or response): hop-by-hop, Host, and body-framing headers.
// res.json() re-serializes the body, so upstream length/encoding no longer match — and
// forwarding accept-encoding would relay an encoding the re-emitted body doesn't use.
var SKIPPED_HEADERS = new Set(['connection', 'keep-alive', 'transfer-encoding', 'upgrade', 'te', 'trailer', 'proxy-authenticate', 'proxy-authorization', 'host', 'content-length', 'content-encoding', 'accept-encoding']);
var UNSAFE_PATH_FRAGMENTS = ['..', '%2e', '%2E', '\\', '\0'];
// NOTICE: Bound the proxied request so a hanging executor cannot tie up the
// connection indefinitely. A timeout raises an error without `.response`,
// so it falls through to the 503 branch.
var REQUEST_TIMEOUT = {
response: 120000,
deadline: 120000
};
function forwardedRequestHeaders(requestHeaders) {
var headers = {};
Object.entries(requestHeaders).forEach(function (_ref) {
var _ref2 = (0, _slicedToArray2["default"])(_ref, 2),
name = _ref2[0],
value = _ref2[1];
if (value !== undefined && !SKIPPED_HEADERS.has(name.toLowerCase())) {
headers[name] = value;
}
});
return headers;
}
function copyResponseHeaders(upstream, response) {
Object.entries(upstream.headers || {}).forEach(function (_ref3) {
var _ref4 = (0, _slicedToArray2["default"])(_ref3, 2),
name = _ref4[0],
value = _ref4[1];
if (value !== undefined && !SKIPPED_HEADERS.has(name.toLowerCase())) {
response.set(name, value);
}
});
}
// First-pass rejection of escape attempts; returns null for anything that could leave the
// executor origin (the authoritative origin check is in buildHandler).
function executorPath(wildcard) {
if (!wildcard || wildcard.startsWith('/') || UNSAFE_PATH_FRAGMENTS.some(function (fragment) {
return wildcard.includes(fragment);
})) {
return null;
}
return "/".concat(wildcard);
}
function buildHandler(_ref5) {
var baseUrl = _ref5.baseUrl,
logger = _ref5.logger;
var normalizedBase = baseUrl.replace(/\/+$/, '');
var baseOrigin = new URL(normalizedBase).origin;
return /*#__PURE__*/function () {
var _ref6 = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee(request, response) {
var suffix, url, method, outgoing, upstream;
return _regenerator["default"].wrap(function _callee$(_context) {
while (1) switch (_context.prev = _context.next) {
case 0:
suffix = executorPath(request.params[0]);
if (!(suffix === null)) {
_context.next = 3;
break;
}
return _context.abrupt("return", response.status(404).json({
error: 'not_found'
}));
case 3:
url = "".concat(normalizedBase).concat(suffix); // Authoritative SSRF check: the forwarded request must never leave the executor origin.
if (!(new URL(url).origin !== baseOrigin)) {
_context.next = 6;
break;
}
return _context.abrupt("return", response.status(404).json({
error: 'not_found'
}));
case 6:
method = request.method.toLowerCase();
_context.prev = 7;
outgoing = superagent[method](url)
// Don't follow redirects: a 3xx to an off-origin Location would bypass the origin guard
// (and matches the Node agent, whose raw http client never follows redirects).
.redirects(0).timeout(REQUEST_TIMEOUT).set(forwardedRequestHeaders(request.headers));
if (request.query) outgoing = outgoing.query(request.query);
if (method !== 'get' && request.body !== undefined) {
outgoing = outgoing.send(request.body);
}
_context.next = 13;
return outgoing;
case 13:
upstream = _context.sent;
copyResponseHeaders(upstream, response);
return _context.abrupt("return", response.status(upstream.status).json(upstream.body));
case 18:
_context.prev = 18;
_context.t0 = _context["catch"](7);
if (!_context.t0.response) {
_context.next = 23;
break;
}
copyResponseHeaders(_context.t0.response, response);
return _context.abrupt("return", response.status(_context.t0.response.status).json(_context.t0.response.body));
case 23:
logger.error('[forest-express] workflow executor proxy error: ', _context.t0.message);
return _context.abrupt("return", response.status(503).json({
error: 'workflow_executor_unreachable'
}));
case 25:
case "end":
return _context.stop();
}
}, _callee, null, [[7, 18]]);
}));
return function (_x, _x2) {
return _ref6.apply(this, arguments);
};
}();
}
// Catch-all: forward any verb/sub-path verbatim to the executor, so a new executor route needs
// no change here.
function initWorkflowExecutorRoutes(app, opts, _ref7) {
var logger = _ref7.logger;
if (!opts.workflowExecutorUrl) return;
app.all(path.generate('_internal/executor/*', opts), auth.ensureAuthenticated, buildHandler({
baseUrl: opts.workflowExecutorUrl,
logger: logger
}));
}
module.exports = initWorkflowExecutorRoutes;