CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/project/http-api/server.ts
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
/*
7
Express HTTP API server.
8
9
This is meant to be used from within the project via localhost, both
10
to get info from the project, and to cause the project to do things.
11
12
Requests must be authenticated using the secret token.
13
*/
14
15
const MAX_REQUESTS_PER_MINUTE = 150;
16
17
import { callback } from "awaiting";
18
import { json, urlencoded } from "body-parser";
19
import type { Request } from "express";
20
import express from "express";
21
import RateLimit from "express-rate-limit";
22
import { writeFile } from "node:fs";
23
import { getOptions } from "@cocalc/project/init-program";
24
import { getClient } from "@cocalc/project/client";
25
import { apiServerPortFile } from "@cocalc/project/data";
26
import { getSecretToken } from "@cocalc/project/servers/secret-token";
27
import { once } from "@cocalc/util/async-utils";
28
import { split } from "@cocalc/util/misc";
29
import getSyncdocHistory from "./get-syncdoc-history";
30
import readTextFile from "./read-text-file";
31
import writeTextFile from "./write-text-file";
32
33
let client: any = undefined;
34
export { client };
35
36
export default async function init(): Promise<void> {
37
client = getClient();
38
if (client == null) throw Error("client must be defined");
39
const dbg: Function = client.dbg("api_server");
40
const app: express.Application = express();
41
app.disable("x-powered-by"); // https://github.com/sagemathinc/cocalc/issues/6101
42
43
dbg("configuring server...");
44
configure(app, dbg);
45
46
const options = getOptions();
47
const server = app.listen(0, options.hostname);
48
await once(server, "listening");
49
const address = server.address();
50
if (address == null || typeof address == "string") {
51
throw Error("failed to assign a port");
52
}
53
const { port } = address;
54
dbg(`writing port to file "${apiServerPortFile}"`);
55
await callback(writeFile, apiServerPortFile, `${port}`);
56
57
dbg(`express server successfully listening at http://${options.hostname}:${port}`);
58
}
59
60
function configure(server: express.Application, dbg: Function): void {
61
server.use(json({ limit: "3mb" }));
62
server.use(urlencoded({ extended: true, limit: "3mb" }));
63
64
rateLimit(server);
65
66
const handler = async (req, res) => {
67
dbg(`handling ${req.path}`);
68
try {
69
handleAuth(req);
70
res.send(await handleEndpoint(req));
71
} catch (err) {
72
dbg(`failed handling ${req.path} -- ${err}`);
73
res.status(400).send({ error: `${err}` });
74
}
75
};
76
77
server.get("/api/v1/*", handler);
78
server.post("/api/v1/*", handler);
79
}
80
81
function rateLimit(server: express.Application): void {
82
// (suggested by LGTM):
83
// set up rate limiter -- maximum of MAX_REQUESTS_PER_MINUTE requests per minute
84
const limiter = RateLimit({
85
windowMs: 1 * 60 * 1000, // 1 minute
86
max: MAX_REQUESTS_PER_MINUTE,
87
});
88
// apply rate limiter to all requests
89
server.use(limiter);
90
}
91
92
function handleAuth(req): void {
93
const h = req.header("Authorization");
94
if (h == null) {
95
throw Error("you MUST authenticate all requests");
96
}
97
98
let providedToken: string;
99
const [type, user] = split(h);
100
switch (type) {
101
case "Bearer":
102
providedToken = user;
103
break;
104
case "Basic":
105
const x = Buffer.from(user, "base64");
106
providedToken = x.toString().split(":")[0];
107
break;
108
default:
109
throw Error(`unknown authorization type '${type}'`);
110
}
111
112
// could throw if not initialized yet -- done in ./init.ts via initSecretToken()
113
const secretToken = getSecretToken();
114
115
// now check auth
116
if (secretToken != providedToken) {
117
throw Error(`incorrect secret token "${secretToken}", "${providedToken}"`);
118
}
119
}
120
121
async function handleEndpoint(req): Promise<any> {
122
const endpoint: string = req.path.slice(req.path.lastIndexOf("/") + 1);
123
switch (endpoint) {
124
case "get-syncdoc-history":
125
return await getSyncdocHistory(getParams(req, ["path", "patches"]));
126
case "write-text-file":
127
return await writeTextFile(getParams(req, ["path", "content"]));
128
case "read-text-file":
129
return await readTextFile(getParams(req, ["path"]));
130
default:
131
throw Error(`unknown endpoint - "${endpoint}"`);
132
}
133
}
134
135
function getParams(req: Request, params: string[]) {
136
const x: any = {};
137
if (req?.method == "POST") {
138
for (const param of params) {
139
x[param] = req.body?.[param];
140
}
141
} else {
142
for (const param of params) {
143
x[param] = req.query?.[param];
144
}
145
}
146
return x;
147
}
148
149