Path: blob/master/src/packages/project/http-api/server.ts
1447 views
/*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, secretToken } from "@cocalc/project/data";25import { once } from "@cocalc/util/async-utils";26import { split } from "@cocalc/util/misc";27import readTextFile from "./read-text-file";28import writeTextFile from "./write-text-file";2930let client: any = undefined;31export { client };3233export default async function init(): Promise<void> {34client = getClient();35if (client == null) throw Error("client must be defined");36const dbg: Function = client.dbg("api_server");37const app: express.Application = express();38app.disable("x-powered-by"); // https://github.com/sagemathinc/cocalc/issues/61013940dbg("configuring server...");41configure(app, dbg);4243const options = getOptions();44const server = app.listen(0, options.hostname);45await once(server, "listening");46const address = server.address();47if (address == null || typeof address == "string") {48throw Error("failed to assign a port");49}50const { port } = address;51dbg(`writing port to file "${apiServerPortFile}"`);52await callback(writeFile, apiServerPortFile, `${port}`);5354dbg(55`express server successfully listening at http://${options.hostname}:${port}`,56);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// now check auth112if (secretToken != providedToken) {113throw Error(`incorrect secret token "${secretToken}", "${providedToken}"`);114}115}116117async function handleEndpoint(req): Promise<any> {118const endpoint: string = req.path.slice(req.path.lastIndexOf("/") + 1);119switch (endpoint) {120case "write-text-file":121return await writeTextFile(getParams(req, ["path", "content"]));122case "read-text-file":123return await readTextFile(getParams(req, ["path"]));124default:125throw Error(`unknown endpoint - "${endpoint}"`);126}127}128129function getParams(req: Request, params: string[]) {130const x: any = {};131if (req?.method == "POST") {132for (const param of params) {133x[param] = req.body?.[param];134}135} else {136for (const param of params) {137x[param] = req.query?.[param];138}139}140return x;141}142143144