import Logging, { LoggingConfiguration, LogTiming, TimeFormat } from "@proxnet/undead-logging"; import { getFullPathFromUrl, getSourceAddress } from "./util/net.ts"; import { createHonoRoute, routeImporter } from "./util/import.ts"; import SocketConsoleHandler from "./server/socket/console/socket.ts"; import Server from "./server/server.ts"; import Command from "./server/commands/command.ts"; import { ServerContentBase } from "./server/ContentBase.ts"; import z from "zod"; import { verify } from "@hono/hono/jwt"; import { SignalRSocketHandler } from "./server/socket/signalr/socket.ts"; import { PushNotificationId } from "./server/socket/signalr/types.ts"; import { genericResponse } from "./util/api.ts"; import { getNetConfig } from "./net.ts"; import { TokenFormat, TokenType } from "./server/platforms/types.ts"; LoggingConfiguration.resetTimeFormat = TimeFormat.Unix; LoggingConfiguration.resetLogTiming = LogTiming.Microtask; const log = new Logging("Main"); log.i(`Galvanic Corrosion rewritten`); if (Deno.args.includes('--flush-persistence')) { await Deno.remove('./persist', { recursive: true }); log.w(`Persistence was wiped!`); } export function detailedLog(items: (string | number | boolean | null)[]) { return items.filter(val => val !== null).join('\r\n '); } function formatHeader(headers: Headers, name: string) { if (!headers.has(name)) return null; return `${name}: ${headers.get(name)}`; } export const logBlacklist = [ '/favicon.ico' ]; export const AppRoot = createHonoRoute('/'); await routeImporter(AppRoot.app, 'src/', [ 'routes/root', 'routes/api', 'routes/auth', 'routes/accounts', 'routes/match', 'routes/img' ]); // deno-lint-ignore require-await AppRoot.app.use('*', async c => { return c.json(genericResponse(false, "Resource Not Found"), 404); }); export const consoleSockets: Set = new Set(); const gameSockets: Set = new Set(); const onListen = async () => { if (!(await Array.fromAsync(Deno.readDir('.'))).find(entry => entry.isDirectory && entry.name == 'persist')) await Deno.mkdir('persist'); await Promise.all(Object.values(Server).map(base => ((base as ServerContentBase).kvInit ? (base as ServerContentBase).kvInit() : undefined))); Server.emit('server.start', undefined); } const netConfig = getNetConfig(); const server = Deno.serve({ hostname: netConfig.host, port: netConfig.port, onListen: addr => { log.n(`Listening info: ${JSON.stringify(addr)}`); onListen(); } }, async (req, info) => { const url = new URL(req.url); const srcAddr = getSourceAddress(req, info.remoteAddr); const unauthRes = new Response("Unauthorized", { status: 401 }); if (url.pathname == '/notify/hub/v1/negotiate') { return new Response(JSON.stringify({ connectionId: "galv4", availableTransports: [{ transport: "WebSockets", transferFormats: ["Text"] }] }), { headers: { 'Content-Type': 'application/json' } }); } if (req.headers.get('Connection')?.includes('Upgrade') && req.headers.get('Upgrade')?.includes('websocket')) { const isSignalR = url.searchParams.has('id'); if (isSignalR) { try { const authHeader = req.headers.get('Authorization'); if (!authHeader) return unauthRes; const splitHeader = authHeader.split(' ')[1]; if (!splitHeader) return unauthRes; const secret = Deno.env.get('secret'); if (!secret) { log.w(`No secret set!`); return unauthRes; } const payload = JSON.parse(JSON.stringify(await verify(splitHeader, secret))) as TokenFormat; if (payload.typ !== TokenType.Access) { log.w(`Only access tokens can be used to connect to the socket`); return unauthRes; } const profile = await Server.Profiles.get(payload.sub); if (!profile) return new Response("Internal Server Error (profile)", { status: 500 }); const { response, socket } = Deno.upgradeWebSocket(req); const handler = new SignalRSocketHandler(socket, profile); gameSockets.add(handler); socket.onclose = () => { gameSockets.delete(handler); } return response; } catch (err) { log.w(`Socket authentication error: ${(err as Error).stack}`); return unauthRes; } } else { const pass = url.searchParams.get('pass'); if (!pass) return unauthRes; else if (pass !== Deno.env.get('CONSOLESECRET')) return unauthRes; const { response, socket } = Deno.upgradeWebSocket(req); const handler = new SocketConsoleHandler(socket, req, info); consoleSockets.add(handler); socket.onclose = () => { consoleSockets.delete(handler); } return response; } } const res = await AppRoot.app.fetch(req, { srcAddr }); const netlog = detailedLog([srcAddr, `${typeof res.status == 'number' ? res.status : "SENT STACK TRACE"}: ${req.method} ${getFullPathFromUrl(new URL(req.url))}`, formatHeader(req.headers, 'Content-Type'), formatHeader(req.headers, 'Connection'), formatHeader(req.headers, 'User-Agent'), ]); if (!logBlacklist.includes(url.pathname)) { if (res.status === 404) log.e(netlog); else log.n(netlog); } return res; }); let shuttingDown = false; Deno.addSignalListener('SIGINT', () => { if (shuttingDown) return; else shuttingDown = true; server.shutdown(); server.unref(); LoggingConfiguration.resetLogTiming = LogTiming.Sync; log.i('Shutting down'); for (const socket of consoleSockets) socket.destroy(); for (const socket of gameSockets) socket.sendNotification(PushNotificationId.ModerationQuitGame); Server.emit('server.destroy', undefined); }); Server.Commands.addRootCommand(new Command({ key: ['ping', 'latency'], exec: (sent: number) => { if (sent) return `${Date.now() - sent}ms`; else return null; }, zod: z.tuple([ z.string().transform(Number) ]), help: 'Get ping (in ms) to the server' }));