forked from zombieb/galvanic-corrosion-rewrite
179 lines
6.3 KiB
TypeScript
179 lines
6.3 KiB
TypeScript
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<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)));
|
|
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'
|
|
})); |