-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathapi.ts
147 lines (128 loc) · 4.2 KB
/
api.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
import { Semaphore } from "https://deno.land/x/semaphore@v1.1.2/semaphore.ts";
import { serve } from "https://deno.land/std@0.176.0/http/mod.ts";
import { createScript, safeURL, safeURLs } from "./utils.ts";
// Sandboxed scripts are only allowed to reach the internet via
// this proxy (enforced via --allow-net).
const PROXY_LOCATION = "localhost:3001";
const SCRIPT_TIME_LIMIT = 1000; // ms
const SCRIPT_MEMORY_LIMIT = 64; // mb
const SCRIPT_REQUEST_LIMIT = 5; // number of requests per script
const CONCURRENCY_LIMIT = 3;
const conurrencyMutex = new Semaphore(CONCURRENCY_LIMIT);
// Track scripts that are running so that we can:
// - auth requests to the proxy
// - limit the number of requests per script
const inFlightScriptIds: Record<string, number> = {};
const SCRIPT_ROUTE = new URLPattern({ pathname: "/script" });
const PROXY_ROUTE = new URLPattern({ pathname: "/proxy" });
async function handler(req: Request) {
if (SCRIPT_ROUTE.exec(req.url)) {
// TODO: stop users sending gigabytes of source code
const code = await req.text();
const { scriptId, scriptPath } = await createScript(code);
inFlightScriptIds[scriptId] = SCRIPT_REQUEST_LIMIT;
const cmd = [
"deno",
"run",
`--v8-flags=--max-old-space-size=${SCRIPT_MEMORY_LIMIT}`,
`--allow-read=${scriptPath}`,
`--allow-net=${PROXY_LOCATION}`,
"./sandbox.ts",
`scriptId=${scriptId}`,
`scriptPath=${scriptPath}`,
];
const release = await conurrencyMutex.acquire();
const scriptProcess = Deno.run({ cmd, stderr: "piped", stdout: "piped" });
release();
setTimeout(async () => {
try {
scriptProcess.kill();
scriptProcess.close();
} catch (e) {
if (!e.message.includes("ESRCH")) { // Might have already closed
console.log(`couldn't kill or close ${scriptId}`, { error: e, code });
}
}
try {
await Deno.remove(scriptPath);
} catch (e) {
console.log(`couldn't remove ${scriptPath}`, { error: e, code });
}
}, SCRIPT_TIME_LIMIT);
const [status, stdout, stderr] = await Promise.all([
scriptProcess.status(),
scriptProcess.output(),
scriptProcess.stderrOutput(),
]);
try {
scriptProcess.kill();
scriptProcess.close();
} catch (e) {
if (!e.message.includes("ESRCH")) { // Might have already closed
console.log(`couldn't kill or close ${scriptId}`, { error: e, code });
}
}
delete inFlightScriptIds[scriptId];
return new Response(
JSON.stringify({
status,
stdout: new TextDecoder().decode(stdout),
stderr: new TextDecoder().decode(stderr),
}),
{
status: 200,
},
);
}
// Proxy
if (PROXY_ROUTE.exec(req.url)) {
const resource = req.headers.get("x-script-fetch") ||
"missing-resource";
const scriptId = req.headers.get("x-script-id");
// Check the request came from a real script
if (scriptId === null || !inFlightScriptIds[scriptId]) {
return new Response(
`bad auth: ${safeURLs.join("\n")}`,
{ status: 400 },
);
}
// Apply some limits
if (--inFlightScriptIds[scriptId] < 1) {
return new Response(
`too many requests, max requests per script: ${SCRIPT_REQUEST_LIMIT}`,
{ status: 400 },
);
}
if (!safeURL(resource)) {
return new Response(
`only these URLs are allowed: [${safeURLs.join(", ")}]`,
{ status: 400 },
);
}
try {
const controller = new AbortController();
const { signal } = controller;
setTimeout(() => {
try {
controller.abort();
} catch { /* */ } // The fetch might have already ended
}, SCRIPT_TIME_LIMIT);
const proxiedRes = await fetch(resource, {
method: req.method,
headers: {
...req.headers,
},
body: req.body,
signal,
});
return new Response(proxiedRes.body, {
...proxiedRes,
});
} catch (e) {
console.log(`error while proxying ${resource}`, { error: e });
return new Response("", { status: 500 });
}
}
return new Response("hm, unknown route", { status: 404 });
}
serve(handler, { port: 3001 });