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