This repository has been archived on 2026-03-19. You can view files and clone it, but cannot push or open issues or pull requests.
Files
galvanic-corrosion/src/main.ts
zombieb 638c0fbf1f Changes (/shrug)
* Added middleware timer for performance debugging
* Relationships and avatar database keys
* CDN
* Profiles are SelfAccounts in most cases, rather than Accounts
* Simplified profile content management
* Progression fixes
* Relationships (favorites not yet implemented)
* Relationship backend
* Relationship and avatar routes
2025-03-31 01:48:46 -04:00

208 lines
7.3 KiB
TypeScript

import * as Log from "@proxnet/undead-logging";
import * as Config from "./config.ts";
import { Database } from "./db.ts";
import { APIUtils, ProfileTokenSchema } from "./apiutils.ts";
import { Discord } from "./discord.ts";
import { generateRandomString } from "./apiutils.ts";
// @ts-types = "npm:@types/express"
import express from "express";
import { decode } from "@gz/jwt";
import UnifiedProfile, { ProfileTokenFormat } from "./data/profiles.ts";
import { SocketHandoff } from "./socket/handoff.ts";
import { SignalRSocketHandler } from "./socket/socket.ts";
const instanceId = generateRandomString(64);
const log = new Log.default("Main");
log.i(`Starting Galvanic Corrosion..`);
const config = Config.getConfig();
if (typeof config == "undefined") {
log.e("Cannot start: Configuration is undefined");
Deno.exit(1);
}
if (config.auth.secret == Config.defaultConfig.auth.secret) {
log.e(`Cannot start: Auth secret is default. Please change 'secrets.authSecret' in 'config.json'`);
Deno.exit(1);
}
if (config.public.serverId == Config.defaultConfig.public.serverId) {
log.e(`Cannot start: Server ID is default. Please change 'public.serverId' in 'config.json'`);
Deno.exit(1);
}
Log.MessageTypeVisibility.Network = config.logging.network;
Log.MessageTypeVisibility.Debug = config.logging.debug;
try {
Database.connect();
} catch (err) {
log.e(`Cannot start: Redis could not be initialized. ${err}`);
Deno.exit(1);
}
const port = config.web.api.port;
const host = config.web.api.host;
log.n(`Starting HTTP server on http://${host}:${port}`);
const app = express();
app.disable("etag");
app.disable("x-powered-by");
app.use(
(rq: express.Request, rs: express.Response, nxt: express.NextFunction) => {
rs.setHeader("Instance", instanceId);
rs.locals.reqId = generateRandomString(12);
log.n(`${rs.locals.reqId} ${APIUtils.getSrcIpDefault(rq)} ${rq.method} ${rq.originalUrl}`);
nxt();
},
);
app.get("/info", APIUtils.setCacheAllowed, (_rq, rs) => {
rs.json({
name: config.public.serverName,
id: config.public.serverId,
motd: config.public.motd,
patches: config.public.patches,
});
});
// content routes
const nameserverRouter = await import("./routes/nameserver.ts");
const apiRouter = await import("./routes/api.ts");
const userRouter = await import("./routes/user.ts");
const authRouter = await import("./routes/auth.ts");
const accountRouter = await import("./routes/account.ts");
const imgRouter = await import("./routes/img.ts");
const matchRouter = await import("./routes/match.ts");
const notifyRouter = await import("./socket/route.ts");
const cdnRouter = await import("./routes/cdn.ts");
app.use(nameserverRouter.route.path, nameserverRouter.route.router);
app.use(apiRouter.route.path, apiRouter.route.router);
app.use(userRouter.route.path, userRouter.route.router);
app.use(authRouter.route.path, authRouter.route.router);
app.use(accountRouter.route.path, accountRouter.route.router);
app.use(imgRouter.route.path, imgRouter.route.router);
app.use(matchRouter.route.path, matchRouter.route.router);
app.use(notifyRouter.route.path, notifyRouter.route.router);
app.use(cdnRouter.route.path, cdnRouter.route.router);
app.use((rq: express.Request, rs: express.Response) => {
log.e(`${APIUtils.getSrcIpDefault(rq)} 404 ${rq.method} ${rq.url.toString()}`);
rs.statusCode = 404;
rs.json(APIUtils.genericResponseFormat(true, "Endpoint not found. Check your syntax and/or method."));
});
try {
/**
* Galvanic WebSocket Server
*/
type AuthResultBase = {
valid: boolean
}
interface SuccessfulAuth extends AuthResultBase {
token: ProfileTokenFormat,
valid: true
}
interface FailedAuth extends AuthResultBase {
valid: false
}
type AuthResult = FailedAuth | SuccessfulAuth;
const authenticate = async (req: Request) => {
const authHeader = req.headers.get('authorization');
if (!authHeader) return { valid: false } as AuthResult;
const token = authHeader.split(" ")[1];
if (!token) return { valid: false } as AuthResult;
const decodedToken = await decode<ProfileTokenFormat>(token, config.auth.secret, {algorithm: 'HS512'});
const schemaResult = ProfileTokenSchema.safeParse(decodedToken);
if (!schemaResult.success) return { valid: false } as AuthResult;
else return { token: decodedToken, valid: true } as AuthResult;
}
const abort = new AbortController();
// Galvanic WebSocket
Deno.serve({port: config.web.socket.port, hostname: config.web.socket.host, signal: abort.signal, onListen: addr => {
log.n(`Socket listening on http://${addr.hostname}:${addr.port}`);
}}, async (req: Request, info: Deno.ServeHandlerInfo<Deno.NetAddr>) => {
const path = new URL(req.url).pathname;
const upgrade = req.headers.get('Upgrade') === 'websocket';
log.n(`U:${upgrade}; ${info.remoteAddr.hostname}:${info.remoteAddr.port} ${req.method} ${path}`);
if (path === '/negotiate' && req.method == 'POST')
return new Response(JSON.stringify({}));
if (!upgrade) return new Response(null, { status: 401 });
const authResult = await authenticate(req);
if (authResult.valid) {
// ID is given as "/notify/hub/v1?&id=pprhdSzJn" by the client.
let handoff: SocketHandoff | undefined;
if (req.url) {
const pathParts = req.url.replace('v1', '').split('/');
const query = new URLSearchParams(pathParts[pathParts.length - 1]);
const connectionId = query.get('id');
if (connectionId) handoff = SocketHandoff.find(connectionId);
}
if (handoff) handoff.complete();
const { socket, response } = Deno.upgradeWebSocket(req);
new SignalRSocketHandler(socket, UnifiedProfile.get(authResult.token.sub));
return response;
} else {
log.e(`401 ${info.remoteAddr} ${req.method} ${req.url}`);
return new Response(null, { status: 401 });
}
});
const http = app.listen(config.web.api.port, config.web.api.host, async () => {
log.n(`Web listening on http://${config.web.api.host}:${config.web.api.port}`);
let shuttingDown = false;
Deno.addSignalListener("SIGINT", () => {
if (shuttingDown) return;
shuttingDown = true;
log.i(`Shutting down`);
abort.abort(); // websockets
http.close();
http.closeAllConnections();
});
Deno.addSignalListener("SIGINT", () => {
for (const handoff of SocketHandoff.all()) handoff.complete();
});
/*
PLACE TEST HERE
*/
if (!(await UnifiedProfile.existsByName("Coach"))) UnifiedProfile.create({ username: "Coach", id: 1 }); // create Coach id 1 if they do not exist
if (!(await UnifiedProfile.existsByName("Server"))) UnifiedProfile.create({ username: "Server", id: 2 }); // create Server id 2 if they do not exist
});
http.on('error', err => {
log.e(`HTTP error: ${err.stack}`);
});
} catch (err) {
log.e(`Cannot start: Network could not be initalized. ${err}`);
Deno.exit(1);
}
Discord.login();