Initial commit

This commit is contained in:
2025-07-25 19:00:06 -04:00
commit e604c7a437
52 changed files with 96098 additions and 0 deletions

158
src/main.ts Normal file
View File

@@ -0,0 +1,158 @@
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 { type ProfileToken } from "./server/profiles/types/profile.ts";
import { SignalRSocketHandler } from "./server/socket/signalr/socket.ts";
import { PushNotificationId } from "./server/socket/signalr/types.ts";
LoggingConfiguration.resetTimeFormat = TimeFormat.Unix;
LoggingConfiguration.resetLogTiming = LogTiming.Microtask;
const log = new Logging("Main");
log.i(`wsi by zombieb`);
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'
]);
// deno-lint-ignore require-await
AppRoot.app.use('*', async c => {
if (!logBlacklist.includes(c.req.url)) log.e(detailedLog([c.get('srcAddr'),
`404 ${c.req.method} ${getFullPathFromUrl(new URL(c.req.url))}`
]));
c.res = new Response("Not Found", { status: 404 });
});
export const consoleSockets: Set<SocketConsoleHandler> = new Set();
const gameSockets: Set<SignalRSocketHandler> = 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)));
Object.values(Server).forEach(base => ((base as ServerContentBase).start ? (base as ServerContentBase).start() : undefined));
}
const server = Deno.serve({ hostname: "10.0.1.39", port: 13370, 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: "who_said_it",
availableTransports: [{transport:"WebSockets",transferFormats:["Text"]}]
}), { headers: { 'Content-Type': 'application/json' }});
}
if (req.headers.get('Connection')?.includes('Upgrade') && req.headers.get('Upgrade') === 'websocket') {
const isSignalR = req.headers.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 = (await verify(splitHeader, secret)) as ProfileToken;
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;
}
}
if (!logBlacklist.includes(url.pathname)) log.n(detailedLog([srcAddr,
`${req.method} ${getFullPathFromUrl(new URL(req.url))}`,
formatHeader(req.headers, 'Content-Type'),
formatHeader(req.headers, 'Connection'),
formatHeader(req.headers, 'User-Agent'),
]));
return await AppRoot.app.fetch(req, { srcAddr });
});
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.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'
}));