UNPKG

@cocalc/project

Version:
134 lines (132 loc) 4.98 kB
"use strict"; /* * This file is part of CoCalc: Copyright © 2020 Sagemath, Inc. * License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.client = void 0; /* Express HTTP API server. This is meant to be used from within the project via localhost, both to get info from the project, and to cause the project to do things. Requests must be authenticated using the secret token. */ const MAX_REQUESTS_PER_MINUTE = 150; const express_1 = __importDefault(require("express")); const fs_1 = require("fs"); const awaiting_1 = require("awaiting"); const async_utils_1 = require("@cocalc/util/async-utils"); const misc_1 = require("@cocalc/util/misc"); const body_parser_1 = require("body-parser"); const express_rate_limit_1 = __importDefault(require("express-rate-limit")); const data_1 = require("@cocalc/project/data"); const theClient = require("@cocalc/project/client"); const secret_token_1 = require("@cocalc/project/servers/secret-token"); let client = undefined; exports.client = client; const get_syncdoc_history_1 = __importDefault(require("./get-syncdoc-history")); const write_text_file_1 = __importDefault(require("./write-text-file")); const read_text_file_1 = __importDefault(require("./read-text-file")); async function init() { exports.client = client = theClient.client; if (client == null) throw Error("client must be defined"); const dbg = client.dbg("api_server"); const app = (0, express_1.default)(); app.disable("x-powered-by"); // https://github.com/sagemathinc/cocalc/issues/6101 dbg("configuring server..."); configure(app, dbg); const server = app.listen(0, "localhost"); await (0, async_utils_1.once)(server, "listening"); const address = server.address(); if (address == null || typeof address == "string") { throw Error("failed to assign a port"); } const { port } = address; dbg(`writing port to file "${data_1.apiServerPortFile}"`); await (0, awaiting_1.callback)(fs_1.writeFile, data_1.apiServerPortFile, `${port}`); dbg(`express server successfully listening at http://localhost:${port}`); } exports.default = init; function configure(server, dbg) { server.use((0, body_parser_1.json)({ limit: "3mb" })); server.use((0, body_parser_1.urlencoded)({ extended: true, limit: "3mb" })); rateLimit(server); const handler = async (req, res) => { dbg(`handling ${req.path}`); try { handleAuth(req); res.send(await handleEndpoint(req)); } catch (err) { dbg(`failed handling ${req.path} -- ${err}`); res.status(400).send({ error: `${err}` }); } }; server.get("/api/v1/*", handler); server.post("/api/v1/*", handler); } function rateLimit(server) { // (suggested by LGTM): // set up rate limiter -- maximum of MAX_REQUESTS_PER_MINUTE requests per minute const limiter = (0, express_rate_limit_1.default)({ windowMs: 1 * 60 * 1000, max: MAX_REQUESTS_PER_MINUTE, }); // apply rate limiter to all requests server.use(limiter); } function handleAuth(req) { const h = req.header("Authorization"); if (h == null) { throw Error("you MUST authenticate all requests"); } let providedToken; const [type, user] = (0, misc_1.split)(h); switch (type) { case "Bearer": providedToken = user; break; case "Basic": const x = Buffer.from(user, "base64"); providedToken = x.toString().split(":")[0]; break; default: throw Error(`unknown authorization type '${type}'`); } // now check auth if (secret_token_1.secretToken != providedToken) { throw Error(`incorrect secret token "${secret_token_1.secretToken}", "${providedToken}"`); } } async function handleEndpoint(req) { const endpoint = req.path.slice(req.path.lastIndexOf("/") + 1); switch (endpoint) { case "get-syncdoc-history": return await (0, get_syncdoc_history_1.default)(getParams(req, ["path", "patches"])); case "write-text-file": return await (0, write_text_file_1.default)(getParams(req, ["path", "content"])); case "read-text-file": return await (0, read_text_file_1.default)(getParams(req, ["path"])); default: throw Error(`unknown endpoint - "${endpoint}"`); } } function getParams(req, params) { const x = {}; if (req?.method == "POST") { for (const param of params) { x[param] = req.body?.[param]; } } else { for (const param of params) { x[param] = req.query?.[param]; } } return x; } //# sourceMappingURL=server.js.map