87 lines
2.5 KiB
TypeScript
87 lines
2.5 KiB
TypeScript
import { createHash, timingSafeEqual } from "node:crypto"
|
|
|
|
import { eq } from "drizzle-orm"
|
|
import { Hono, type Context } from "hono"
|
|
import { z } from "zod"
|
|
|
|
import { config } from "../config"
|
|
import { db, schema } from "../db"
|
|
import { failure } from "../http"
|
|
|
|
const heartbeatSchema = z.object({
|
|
hostname: z.string().min(1).max(128),
|
|
judger_version: z.string().min(1).max(32),
|
|
cpu_core: z.number().int().positive(),
|
|
memory: z.number().min(0).max(100),
|
|
cpu: z.number().min(0).max(100),
|
|
action: z.literal("heartbeat"),
|
|
service_url: z.string().min(1).max(256),
|
|
})
|
|
|
|
export const judgeServerRoutes = new Hono()
|
|
|
|
function tokenMatches(value: string | undefined) {
|
|
if (!value) return false
|
|
const expected = createHash("sha256")
|
|
.update(config.judgeServerToken)
|
|
.digest("hex")
|
|
const actualBuffer = Buffer.from(value)
|
|
const expectedBuffer = Buffer.from(expected)
|
|
return (
|
|
actualBuffer.length === expectedBuffer.length &&
|
|
timingSafeEqual(actualBuffer, expectedBuffer)
|
|
)
|
|
}
|
|
|
|
async function heartbeat(c: Context) {
|
|
if (!tokenMatches(c.req.header("X-Judge-Server-Token"))) {
|
|
return failure(c, 403, "invalid-judge-token", "Invalid token")
|
|
}
|
|
|
|
const parsed = heartbeatSchema.safeParse(await c.req.json().catch(() => null))
|
|
if (!parsed.success) {
|
|
return failure(c, 400, "invalid-heartbeat", "Invalid heartbeat payload")
|
|
}
|
|
|
|
const now = new Date().toISOString()
|
|
const [existing] = await db
|
|
.select({ id: schema.judgeServer.id })
|
|
.from(schema.judgeServer)
|
|
.where(eq(schema.judgeServer.hostname, parsed.data.hostname))
|
|
.limit(1)
|
|
|
|
const common = {
|
|
ip:
|
|
c.req.header("x-forwarded-for")?.split(",")[0]?.trim() ||
|
|
c.req.header("x-real-ip") ||
|
|
null,
|
|
judgerVersion: parsed.data.judger_version,
|
|
cpuCore: parsed.data.cpu_core,
|
|
memoryUsage: parsed.data.memory,
|
|
cpuUsage: parsed.data.cpu,
|
|
lastHeartbeat: now,
|
|
serviceUrl: parsed.data.service_url,
|
|
}
|
|
|
|
if (existing) {
|
|
await db
|
|
.update(schema.judgeServer)
|
|
.set(common)
|
|
.where(eq(schema.judgeServer.id, existing.id))
|
|
} else {
|
|
await db.insert(schema.judgeServer).values({
|
|
...common,
|
|
hostname: parsed.data.hostname,
|
|
createTime: now,
|
|
taskNumber: 0,
|
|
isDisabled: false,
|
|
})
|
|
}
|
|
|
|
// JudgeServer 1.6.1 healthcheck still reads the legacy { error, data } envelope.
|
|
return c.json({ error: null, data: null })
|
|
}
|
|
|
|
judgeServerRoutes.post("/judge-server/heartbeat", heartbeat)
|
|
judgeServerRoutes.post("/judge-server/heartbeat/", heartbeat)
|