"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Gateway = exports.Pattern = void 0; const querystring_1 = require("querystring"); const dotObject = __importStar(require("dot-object")); const request_1 = require("./request"); const errors_1 = require("./errors"); const http_client_1 = require("./http.client"); const server_1 = require("./server"); var Pattern; (function (Pattern) { Pattern["POST"] = "post"; Pattern["GET"] = "get"; Pattern["PATCH"] = "patch"; Pattern["PUT"] = "put"; Pattern["DELETE"] = "delete"; })(Pattern = exports.Pattern || (exports.Pattern = {})); /** * The Gateway proxies http requests to Twirp Compliant * handlers */ class Gateway { constructor(routes) { this.routes = routes; } /** * Middleware that rewrite the current request * to a Twirp compliant request */ twirpRewrite(prefix = "/twirp") { return (req, resp, next) => { this.rewrite(req, resp, prefix) .then(() => next()) .catch((e) => { if (e instanceof errors_1.TwirpError) { if (e.code !== errors_1.TwirpErrorCode.NotFound) { server_1.writeError(resp, e); } else { next(); } } }); }; } /** * Rewrite an incoming request to a Twirp compliant request * @param req * @param resp * @param prefix */ rewrite(req, resp, prefix = "/twirp") { return __awaiter(this, void 0, void 0, function* () { const [match, route] = this.matchRoute(req); const body = yield this.prepareTwirpBody(req, match, route); const twirpUrl = `${prefix}/${route.packageName}.${route.serviceName}/${route.methodName}`; req.url = twirpUrl; req.originalUrl = twirpUrl; req.method = "POST"; req.headers["content-type"] = "application/json"; req.rawBody = Buffer.from(JSON.stringify(body)); if (route.responseBodyKey) { const endFn = resp.end.bind(resp); resp.end = function (chunk) { if (resp.statusCode === 200) { endFn(`{ "${route.responseBodyKey}": ${chunk} }`); } else { endFn(chunk); } }; } }); } /** * Create a reverse proxy handler to * proxy http requests to Twirp Compliant handlers * @param httpClientOption */ reverseProxy(httpClientOption) { const client = http_client_1.NodeHttpRPC(httpClientOption); return (req, res) => __awaiter(this, void 0, void 0, function* () { try { const [match, route] = this.matchRoute(req); const body = yield this.prepareTwirpBody(req, match, route); const response = yield client.request(`${route.packageName}.${route.serviceName}`, route.methodName, "application/json", body); res.statusCode = 200; res.setHeader("content-type", "application/json"); let jsonResponse; if (route.responseBodyKey) { jsonResponse = JSON.stringify({ [route.responseBodyKey]: response }); } else { jsonResponse = JSON.stringify(response); } res.end(jsonResponse); } catch (e) { server_1.writeError(res, e); } }); } /** * Prepares twirp body requests using http.google.annotions * compliant spec * * @param req * @param match * @param route * @protected */ prepareTwirpBody(req, match, route) { return __awaiter(this, void 0, void 0, function* () { const _a = match.params, { query_string } = _a, params = __rest(_a, ["query_string"]); let requestBody = Object.assign({}, params); if (query_string && route.bodyKey !== "*") { const queryParams = this.parseQueryString(query_string); requestBody = Object.assign(Object.assign({}, queryParams), requestBody); } let body = {}; if (route.bodyKey) { const data = yield request_1.getRequestData(req); try { const jsonBody = JSON.parse(data.toString() || "{}"); if (route.bodyKey === "*") { body = jsonBody; } else { body[route.bodyKey] = jsonBody; } } catch (e) { const msg = "the json request could not be decoded"; throw new errors_1.TwirpError(errors_1.TwirpErrorCode.Malformed, msg).withCause(e, true); } } return Object.assign(Object.assign({}, body), requestBody); }); } /** * Matches a route * @param req */ matchRoute(req) { var _a; const httpMethod = (_a = req.method) === null || _a === void 0 ? void 0 : _a.toLowerCase(); if (!httpMethod) { throw new errors_1.BadRouteError(`method not allowed`, req.method || "", req.url || ""); } const routes = this.routes[httpMethod]; for (const route of routes) { const match = route.matcher(req.url || "/"); if (match) { return [match, route]; } } throw new errors_1.NotFoundError(`url ${req.url} not found`); } /** * Parse query string * @param queryString */ parseQueryString(queryString) { const queryParams = querystring_1.parse(queryString.replace("?", "")); return dotObject.object(queryParams); } } exports.Gateway = Gateway;