2024-03-04 21:00:20 +01:00
|
|
|
import crypto from "node:crypto";
|
|
|
|
|
|
|
|
import bodyParser from "@koa/bodyparser";
|
|
|
|
import katex from "katex";
|
|
|
|
import Koa from "koa";
|
2024-03-13 17:29:10 +01:00
|
|
|
import Prometheus from "prom-client";
|
2024-03-04 21:00:20 +01:00
|
|
|
|
|
|
|
const host = "localhost";
|
|
|
|
const port = Number(process.argv[2] ?? "9700");
|
|
|
|
if (!Number.isInteger(port)) {
|
|
|
|
throw new TypeError("Invalid port");
|
|
|
|
}
|
|
|
|
|
|
|
|
const shared_secret = process.env.SHARED_SECRET;
|
|
|
|
if (typeof shared_secret !== "string") {
|
|
|
|
console.error("No SHARED_SECRET set!");
|
|
|
|
process.exit(1);
|
|
|
|
}
|
|
|
|
const compare_secret = (given_secret: string): boolean => {
|
|
|
|
try {
|
|
|
|
// Throws an exception if the strings are unequal length
|
|
|
|
return crypto.timingSafeEqual(
|
|
|
|
Buffer.from(shared_secret, "utf8"),
|
|
|
|
Buffer.from(given_secret, "utf8"),
|
|
|
|
);
|
|
|
|
} catch {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const app = new Koa();
|
|
|
|
app.use(bodyParser());
|
|
|
|
|
2024-03-13 17:29:10 +01:00
|
|
|
Prometheus.collectDefaultMetrics();
|
|
|
|
const httpRequestDurationSeconds = new Prometheus.Histogram({
|
|
|
|
name: "katex_http_request_duration_seconds",
|
|
|
|
help: "Duration of HTTP requests in seconds",
|
|
|
|
labelNames: ["method", "path", "status"] as const,
|
|
|
|
buckets: [
|
|
|
|
0.00001, 0.00002, 0.00005, 0.0001, 0.0002, 0.0005, 0.001, 0.002, 0.005, 0.01, 0.02, 0.05,
|
|
|
|
0.1,
|
|
|
|
],
|
|
|
|
});
|
|
|
|
|
|
|
|
const httpRequestSizeBytes = new Prometheus.Histogram({
|
|
|
|
name: "katex_request_size_bytes",
|
|
|
|
help: "Size of successful KaTeX input in bytes",
|
|
|
|
labelNames: ["display_mode"] as const,
|
|
|
|
buckets: [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000],
|
|
|
|
});
|
|
|
|
|
|
|
|
const httpResponseSizeBytes = new Prometheus.Histogram({
|
|
|
|
name: "katex_response_size_bytes",
|
|
|
|
help: "Size of successful KaTeX output in bytes",
|
|
|
|
labelNames: ["display_mode"] as const,
|
|
|
|
buckets: [100, 200, 500, 1000, 2000, 5000, 10000, 20000, 50000, 100000, 200000, 500000],
|
|
|
|
});
|
|
|
|
|
|
|
|
app.use(async (ctx, next) => {
|
|
|
|
if (ctx.request.method === "GET" && ctx.request.path === "/metrics") {
|
|
|
|
ctx.body = await Prometheus.register.metrics();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const endTimer = httpRequestDurationSeconds.startTimer();
|
|
|
|
await next();
|
|
|
|
const {method, path} = ctx.request;
|
|
|
|
const {status} = ctx.response;
|
|
|
|
httpRequestDurationSeconds.labels({method, path, status: String(status)}).observe(endTimer());
|
|
|
|
});
|
|
|
|
|
2024-03-04 21:00:20 +01:00
|
|
|
app.use((ctx, _next) => {
|
|
|
|
if (ctx.request.method !== "POST" || ctx.request.path !== "/") {
|
|
|
|
ctx.status = 404;
|
|
|
|
return;
|
|
|
|
}
|
2024-05-04 02:24:37 +02:00
|
|
|
const body: unknown = ctx.request.body;
|
|
|
|
if (typeof body !== "object" || body === null) {
|
2024-03-04 21:00:20 +01:00
|
|
|
ctx.status = 400;
|
|
|
|
ctx.type = "text/plain";
|
|
|
|
ctx.body = "Missing POST body";
|
|
|
|
return;
|
|
|
|
}
|
2024-05-04 02:24:37 +02:00
|
|
|
if (
|
|
|
|
!("shared_secret" in body) ||
|
|
|
|
typeof body.shared_secret !== "string" ||
|
|
|
|
!compare_secret(body.shared_secret)
|
|
|
|
) {
|
2024-03-04 21:00:20 +01:00
|
|
|
ctx.status = 403;
|
|
|
|
ctx.type = "text/plain";
|
|
|
|
ctx.body = "Invalid 'shared_secret' argument";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-05-04 02:24:37 +02:00
|
|
|
const is_display = "is_display" in body && body.is_display === "true";
|
2024-03-04 21:00:20 +01:00
|
|
|
|
2024-05-04 02:24:37 +02:00
|
|
|
if (!("content" in body) || typeof body.content !== "string") {
|
2024-03-04 21:00:20 +01:00
|
|
|
ctx.status = 400;
|
|
|
|
ctx.type = "text/plain";
|
|
|
|
ctx.body = "Invalid 'content' argument";
|
|
|
|
return;
|
|
|
|
}
|
2024-05-04 02:24:37 +02:00
|
|
|
const content = body.content;
|
2024-03-04 21:00:20 +01:00
|
|
|
|
2024-03-13 17:29:10 +01:00
|
|
|
httpRequestSizeBytes.labels(String(is_display)).observe(Buffer.byteLength(content, "utf8"));
|
2024-03-04 21:00:20 +01:00
|
|
|
try {
|
2024-05-04 02:24:37 +02:00
|
|
|
const output = katex.renderToString(content, {displayMode: is_display});
|
|
|
|
ctx.body = output;
|
|
|
|
httpResponseSizeBytes.labels(String(is_display)).observe(Buffer.byteLength(output, "utf8"));
|
2024-03-04 21:00:20 +01:00
|
|
|
} catch (error) {
|
|
|
|
if (error instanceof katex.ParseError) {
|
|
|
|
ctx.status = 400;
|
|
|
|
ctx.type = "text/plain";
|
|
|
|
ctx.body = error.message;
|
|
|
|
} else {
|
|
|
|
ctx.status = 500;
|
|
|
|
console.error(error);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
app.listen(port, host, () => {
|
|
|
|
console.log(`Server started on http://${host}:${port}`);
|
|
|
|
});
|