UNPKG

forest-express

Version:

Official package for all Forest Express Lianas

138 lines (133 loc) 5.94 kB
"use strict"; 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;