That's a spicy meatball
* APIUtils additions * Socket and web server listen on dedicated ports (see denoland/deno socket issue created by ZombieB1309 on GitHub) * Coach and Server created automatically (untested) * Profile content functions split into 'managers' * Progression temporary implementation * Settings placed into profile content manager * Relationships and messages return temporary empty array * Socket targets defined, message delivery to target, exec returned (goes unused for now)
This commit is contained in:
121
src/main.ts
121
src/main.ts
@@ -6,12 +6,10 @@ import { Discord } from "./discord.ts";
|
||||
import { generateRandomString } from "./apiutils.ts";
|
||||
// @ts-types = "npm:@types/express"
|
||||
import express from "express";
|
||||
import WebSocket, { WebSocketServer } from "ws";
|
||||
import { decode } from "@gz/jwt";
|
||||
import UnifiedProfile, { ProfileTokenFormat } from "./data/profiles.ts";
|
||||
import { SocketHandoff } from "./socket/handoff.ts";
|
||||
import { SignalRSocketHandler } from "./socket/socket.ts";
|
||||
import { IncomingMessage } from "node:http";
|
||||
|
||||
const instanceId = generateRandomString(64);
|
||||
|
||||
@@ -44,8 +42,8 @@ try {
|
||||
Deno.exit(1);
|
||||
}
|
||||
|
||||
const port = config.web.port;
|
||||
const host = config.web.host;
|
||||
const port = config.web.api.port;
|
||||
const host = config.web.api.host;
|
||||
|
||||
log.n(`Starting HTTP server on http://${host}:${port}`);
|
||||
|
||||
@@ -62,7 +60,7 @@ app.use(
|
||||
},
|
||||
);
|
||||
|
||||
app.get("/info", (_rq, rs) => {
|
||||
app.get("/info", APIUtils.setCacheAllowed, (_rq, rs) => {
|
||||
rs.json({
|
||||
name: config.public.serverName,
|
||||
id: config.public.serverId,
|
||||
@@ -102,12 +100,19 @@ try {
|
||||
* Galvanic WebSocket Server
|
||||
*/
|
||||
|
||||
type AuthResult = {
|
||||
token?: ProfileTokenFormat,
|
||||
type AuthResultBase = {
|
||||
valid: boolean
|
||||
}
|
||||
const authenticate = async (req: IncomingMessage) => {
|
||||
const authHeader = req.headers.authorization;
|
||||
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];
|
||||
@@ -118,29 +123,52 @@ try {
|
||||
if (!schemaResult.success) return { valid: false } as AuthResult;
|
||||
else return { token: decodedToken, valid: true } as AuthResult;
|
||||
}
|
||||
|
||||
const abort = new AbortController();
|
||||
|
||||
const wss = new WebSocketServer({ noServer: true, path: "/notify/hub/v1" });
|
||||
wss.on('connection', (socket: WebSocket, req: IncomingMessage) => {
|
||||
if (!req.token) {
|
||||
socket.close();
|
||||
return;
|
||||
// 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 });
|
||||
}
|
||||
|
||||
// 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();
|
||||
new SignalRSocketHandler(socket, UnifiedProfile.get(req.token.sub));
|
||||
});
|
||||
|
||||
const http = app.listen(config.web.port, config.web.host, () => {
|
||||
log.n(`Listening on http://${config.web.host}:${config.web.port}`);
|
||||
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", () => {
|
||||
@@ -148,40 +176,27 @@ try {
|
||||
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.exists(1))) UnifiedProfile.create({ username: "Coach" }); // create Coach if they do not exist
|
||||
if (!(await UnifiedProfile.exists(2))) UnifiedProfile.create({ username: "Server" }); // create Server if they do not exist
|
||||
|
||||
});
|
||||
|
||||
// Currently not working in Deno. Socket problem?
|
||||
/*http.on('upgrade', async (req, socket, head) => {
|
||||
log.d('Handling upgrade');
|
||||
try {
|
||||
const authResult = await authenticate(req);
|
||||
|
||||
if (authResult.valid) {
|
||||
req.token = authResult.token;
|
||||
|
||||
log.d('Auth result was valid.');
|
||||
wss.handleUpgrade(req, socket, head, (ws) => {
|
||||
wss.emit('connection', ws, req);
|
||||
});
|
||||
} else {
|
||||
// Reject the upgrade
|
||||
log.e(`Socket authentication error (401) from ${APIUtils.getSrcIpDefaultRaw(req)}`);
|
||||
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
||||
socket.destroy();
|
||||
}
|
||||
} catch (err) {
|
||||
// Handle authentication error
|
||||
log.e(`Socket authentication error (500): ${err}\n from ${APIUtils.getSrcIpDefaultRaw(req)}`);
|
||||
socket.write('HTTP/1.1 500 Internal Server Error\r\n\r\n');
|
||||
socket.destroy();
|
||||
}
|
||||
});*/
|
||||
http.on('error', err => {
|
||||
log.e(`HTTP error: ${err.stack}`);
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
log.e(`Cannot start: Network could not be initalized. ${err}`);
|
||||
|
||||
Reference in New Issue
Block a user