mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-03 21:43:21 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			124 lines
		
	
	
		
			3.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			124 lines
		
	
	
		
			3.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import crypto from "node:crypto";
 | 
						|
 | 
						|
import {bodyParser} from "@koa/bodyparser";
 | 
						|
import katex from "katex";
 | 
						|
import Koa from "koa";
 | 
						|
import Prometheus from "prom-client";
 | 
						|
 | 
						|
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());
 | 
						|
 | 
						|
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());
 | 
						|
});
 | 
						|
 | 
						|
app.use((ctx, _next) => {
 | 
						|
    if (ctx.request.method !== "POST" || ctx.request.path !== "/") {
 | 
						|
        ctx.status = 404;
 | 
						|
        return;
 | 
						|
    }
 | 
						|
    const body: unknown = ctx.request.body;
 | 
						|
    if (typeof body !== "object" || body === null) {
 | 
						|
        ctx.status = 400;
 | 
						|
        ctx.type = "text/plain";
 | 
						|
        ctx.body = "Missing POST body";
 | 
						|
        return;
 | 
						|
    }
 | 
						|
    if (
 | 
						|
        !("shared_secret" in body) ||
 | 
						|
        typeof body.shared_secret !== "string" ||
 | 
						|
        !compare_secret(body.shared_secret)
 | 
						|
    ) {
 | 
						|
        ctx.status = 403;
 | 
						|
        ctx.type = "text/plain";
 | 
						|
        ctx.body = "Invalid 'shared_secret' argument";
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    const is_display = "is_display" in body && body.is_display === "true";
 | 
						|
 | 
						|
    if (!("content" in body) || typeof body.content !== "string") {
 | 
						|
        ctx.status = 400;
 | 
						|
        ctx.type = "text/plain";
 | 
						|
        ctx.body = "Invalid 'content' argument";
 | 
						|
        return;
 | 
						|
    }
 | 
						|
    const content = body.content;
 | 
						|
 | 
						|
    httpRequestSizeBytes.labels(String(is_display)).observe(Buffer.byteLength(content, "utf8"));
 | 
						|
    try {
 | 
						|
        const output = katex.renderToString(content, {displayMode: is_display});
 | 
						|
        ctx.body = output;
 | 
						|
        httpResponseSizeBytes.labels(String(is_display)).observe(Buffer.byteLength(output, "utf8"));
 | 
						|
    } 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}`);
 | 
						|
});
 |