-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.js
More file actions
executable file
·126 lines (110 loc) · 3.49 KB
/
Copy pathserver.js
File metadata and controls
executable file
·126 lines (110 loc) · 3.49 KB
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
#!/usr/bin/env node
import http from "node:http";
import fs from "node:fs/promises";
import { buffer } from "node:stream/consumers";
import { encrypt } from "./static/subtle.mjs";
import { getGist, addGist } from "./src/database.js";
import { PORT, BIND, BASE_URL, LIMIT_SIZE } from "./src/constants.js";
// load static files into memory at startup
const staticCache = {
"styles.css": [await fs.readFile("./static/styles.css"), "text/css"],
"home.mjs": [
await fs.readFile("./static/home.mjs"),
"application/javascript",
],
"view.mjs": [
await fs.readFile("./static/view.mjs"),
"application/javascript",
],
"subtle.mjs": [
await fs.readFile("./static/subtle.mjs"),
"application/javascript",
],
};
// simple template engine
const render = async (template) => {
const layout = await fs.readFile("./src/views/_layout.html", "utf8");
const body = await fs.readFile(`./src/views/${template}.html`, "utf8");
return layout.replace("{{body}}", body).replaceAll("{{BASE_URL}}", BASE_URL);
};
// route handlers
const serveStatic = (_req, res, match) => {
const [contents, type] = staticCache[match.pathname.groups.file] || [];
if (!contents) throw { code: 404, message: "Not found" };
res.setHeader("Content-Type", type);
res.setHeader("Cache-Control", "max-age=2592000");
res.end(contents);
};
const home = async (_req, res) => {
res.setHeader("Content-Type", "text/html");
res.end(await render("home"));
};
const view = async (_req, res) => {
res.setHeader("Content-Type", "text/html");
res.end(await render("view"));
};
const gistData = (_req, res, match) => {
const gist = getGist(match.pathname.groups.uuid);
if (!gist) throw { code: 404, message: "Not found" };
res.setHeader("Cache-Control", "max-age=2592000");
res.setHeader("X-IV", gist.iv);
res.end(Buffer.from(gist.cipherText));
};
const createGist = async (req, res) => {
const requestSize = parseInt(req.headers["content-length"]);
if (isNaN(requestSize) || requestSize > LIMIT_SIZE)
throw { code: 413, message: "too large" };
if (requestSize === 0) throw { code: 406, message: "empty file" };
const body = await buffer(req);
const { iv, k, cipherText } = await encrypt(body);
const uuid = addGist(iv, cipherText);
res.statusCode = 201;
res.end(`${BASE_URL}/${uuid}#${k}`);
};
// routes using URLPattern (the order can be relevant for matching)
const routes = [
{ method: "GET", pattern: new URLPattern({ pathname: "/" }), handler: home },
{
method: "GET",
pattern: new URLPattern({ pathname: "/static/:file" }),
handler: serveStatic,
},
{
method: "GET",
pattern: new URLPattern({ pathname: "/data/:uuid" }),
handler: gistData,
},
{
method: "GET",
pattern: new URLPattern({ pathname: "/:uuid" }),
handler: view,
},
{
method: "POST",
pattern: new URLPattern({ pathname: "/" }),
handler: createGist,
},
];
// request handler
const handleRequests = async (req, res) => {
const match = routes.find(
(route) => req.method === route.method && route.pattern.test(req.url),
);
if (match) {
const route = match.pattern.exec(req.url);
return await match.handler(req, res, route);
}
throw { code: 404, message: "Not found" };
};
// server
const server = http.createServer(async (req, res) => {
try {
await handleRequests(req, res);
} catch (err) {
res.statusCode = err.code || 500;
res.end(err.message || "Server error");
}
});
server.listen(PORT, BIND, () => {
console.log(`Listen on http://${BIND}:${PORT}`);
});