import { Context, Next } from "@hono/hono"; import { type HonoEnv } from "./types.ts"; import Logging from "@proxnet/undead-logging"; import z from "zod"; import { verify } from "@hono/hono/jwt"; import Server from "../server/server.ts"; import { TokenFormat } from "../server/platforms/types.ts"; import { HTTPStatus, httpStatusText } from "@oneday/http-status"; import { ContentfulStatusCode } from "@hono/hono/utils/http-status"; import { env } from "../env.ts"; const log = new Logging("APIUtils"); export function statusResponse(c: Context, code: HTTPStatus, msg?: string) { return c.json(genericResponse(code < 400, msg ?? httpStatusText(code)), code as ContentfulStatusCode); } export function genericResponse(success: boolean, msg?: string, data?: null) { return { success, msg, data } }; export function successResponse(success: boolean, error: string) { return (c: Context) => { return c.json({ success, error }); } } const authHeaderSchema = z.string().transform((arg, ctx) => { const split = arg.split(' '); for (const item of split) if (item.length < 6) { ctx.addIssue("Authorization header is invalid"); return; } return split[1]; }); export async function authenticate(c: Context, nxt: Next) { const secret = env["SECRET"]; if (!secret) return c.json(genericResponse(false, "Internal Server Error"), 500); const authHeader = authHeaderSchema.safeParse(c.req.header('Authorization')); if (authHeader.success) { try { const payload = JSON.parse(JSON.stringify(await verify(authHeader.data ? authHeader.data : 'not a valid token', secret))); const profile = await Server.Profiles.get((payload as TokenFormat).sub); if (!profile) return c.json(genericResponse(false, "Internal Server Error"), 500); c.set('profile', profile); return await nxt(); } catch (err) { log.w(`Authentication failed: ${(err as Error).stack}`); return c.json(genericResponse(false, "Internal Server Error"), 500); } } else return c.json(genericResponse(false, "Authorization required"), 401); } export function recNetResult(success: boolean, error?: string) { return {success, error}; } export function recNetResultResponse(c: Context, status: HTTPStatus, success: boolean, error?: string) { return c.json(recNetResult(success, error), status as ContentfulStatusCode); } export function generateRandomString(length: number) { const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; let randomString = ""; for (let i = 0; i < length; i++) { const randomIndex = Math.floor(Math.random() * characters.length); randomString += characters.charAt(randomIndex); } return randomString; } export class RateLimiter { #intervalId: number; #hitLimit: number; #addressHits: Map = new Map(); /** * @param interval In seconds: rate at which hit counts will be cleared * @param limit Number of hits (inclusive) before requests are blocked */ constructor(interval: number = 60, limit: number = 10) { this.#hitLimit = limit; this.#intervalId = setInterval(() => { this.#addressHits.clear(); }, interval * 1000); Deno.addSignalListener("SIGINT", () => { this.#close(); }); } #addressIncrement(address: string) { const hits = this.#addressHits.get(address); if (hits) this.#addressHits.set(address, hits + 1); else this.#addressHits.set(address, 1); } #getAddressHits(address: string) { const hits = this.#addressHits.get(address); if (hits) return hits; else { this.#addressHits.set(address, 1); return 1; } } middle() { return ( c: Context, next: Next ) => { const address = c.get('srcAddr'); if (address == '127.0.0.1' || address == '::1') return next(); this.#addressIncrement(address); const hits = this.#getAddressHits(address); if (hits && hits > this.#hitLimit) return c.json(recNetResult(false, "Rate Limited. Please try again later."), 429); else return next(); }; } #close() { clearInterval(this.#intervalId); } } const loginLockBodySchema = z.object({ LoginLock: z.uuidv4() }); export const loginLockMiddleware = async (c: Context, nxt: Next) => { function unauthorized() { return statusResponse(c, HTTPStatus.Unauthorized); } if (c.req.header("Content-Type") !== "application/x-www-form-urlencoded") return unauthorized(); try { const form = await c.req.formData(); const body = await loginLockBodySchema.safeParseAsync(Object.fromEntries(form.entries())); if (body.success) { if (typeof c.get('profile') == 'undefined') { log.w(`Profile was not set, cannot validate LoginLock. Was the request authorized?`); return statusResponse(c, HTTPStatus.InternalServerError); } const profile = c.get('profile'); const loginLock = await profile.Matchmaking.getLoginLock(); if (!loginLock) await profile.Matchmaking.setLoginLock(body.data.LoginLock); else if (body.data.LoginLock !== loginLock) { log.w(`LoginLock did not match. The token for this profile could be compromised or the client is an unknown state.`); return unauthorized(); } return await nxt(); } else { log.w(`LoginLock parse failed: ${JSON.stringify(body.error)}`); return unauthorized(); } } catch { return unauthorized(); } }