Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/project/http-api/server.ts
Views: 687
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45/*6Express HTTP API server.78This is meant to be used from within the project via localhost, both9to get info from the project, and to cause the project to do things.1011Requests must be authenticated using the secret token.12*/1314const MAX_REQUESTS_PER_MINUTE = 150;1516import { callback } from "awaiting";17import { json, urlencoded } from "body-parser";18import type { Request } from "express";19import express from "express";20import RateLimit from "express-rate-limit";21import { writeFile } from "node:fs";22import { getOptions } from "@cocalc/project/init-program";23import { getClient } from "@cocalc/project/client";24import { apiServerPortFile } from "@cocalc/project/data";25import { getSecretToken } from "@cocalc/project/servers/secret-token";26import { once } from "@cocalc/util/async-utils";27import { split } from "@cocalc/util/misc";28import getSyncdocHistory from "./get-syncdoc-history";29import readTextFile from "./read-text-file";30import writeTextFile from "./write-text-file";3132let client: any = undefined;33export { client };3435export default async function init(): Promise<void> {36client = getClient();37if (client == null) throw Error("client must be defined");38const dbg: Function = client.dbg("api_server");39const app: express.Application = express();40app.disable("x-powered-by"); // https://github.com/sagemathinc/cocalc/issues/61014142dbg("configuring server...");43configure(app, dbg);4445const options = getOptions();46const server = app.listen(0, options.hostname);47await once(server, "listening");48const address = server.address();49if (address == null || typeof address == "string") {50throw Error("failed to assign a port");51}52const { port } = address;53dbg(`writing port to file "${apiServerPortFile}"`);54await callback(writeFile, apiServerPortFile, `${port}`);5556dbg(`express server successfully listening at http://${options.hostname}:${port}`);57}5859function configure(server: express.Application, dbg: Function): void {60server.use(json({ limit: "3mb" }));61server.use(urlencoded({ extended: true, limit: "3mb" }));6263rateLimit(server);6465const handler = async (req, res) => {66dbg(`handling ${req.path}`);67try {68handleAuth(req);69res.send(await handleEndpoint(req));70} catch (err) {71dbg(`failed handling ${req.path} -- ${err}`);72res.status(400).send({ error: `${err}` });73}74};7576server.get("/api/v1/*", handler);77server.post("/api/v1/*", handler);78}7980function rateLimit(server: express.Application): void {81// (suggested by LGTM):82// set up rate limiter -- maximum of MAX_REQUESTS_PER_MINUTE requests per minute83const limiter = RateLimit({84windowMs: 1 * 60 * 1000, // 1 minute85max: MAX_REQUESTS_PER_MINUTE,86});87// apply rate limiter to all requests88server.use(limiter);89}9091function handleAuth(req): void {92const h = req.header("Authorization");93if (h == null) {94throw Error("you MUST authenticate all requests");95}9697let providedToken: string;98const [type, user] = split(h);99switch (type) {100case "Bearer":101providedToken = user;102break;103case "Basic":104const x = Buffer.from(user, "base64");105providedToken = x.toString().split(":")[0];106break;107default:108throw Error(`unknown authorization type '${type}'`);109}110111// could throw if not initialized yet -- done in ./init.ts via initSecretToken()112const secretToken = getSecretToken();113114// now check auth115if (secretToken != providedToken) {116throw Error(`incorrect secret token "${secretToken}", "${providedToken}"`);117}118}119120async function handleEndpoint(req): Promise<any> {121const endpoint: string = req.path.slice(req.path.lastIndexOf("/") + 1);122switch (endpoint) {123case "get-syncdoc-history":124return await getSyncdocHistory(getParams(req, ["path", "patches"]));125case "write-text-file":126return await writeTextFile(getParams(req, ["path", "content"]));127case "read-text-file":128return await readTextFile(getParams(req, ["path"]));129default:130throw Error(`unknown endpoint - "${endpoint}"`);131}132}133134function getParams(req: Request, params: string[]) {135const x: any = {};136if (req?.method == "POST") {137for (const param of params) {138x[param] = req.body?.[param];139}140} else {141for (const param of params) {142x[param] = req.query?.[param];143}144}145return x;146}147148149