* Unified profiles, rather than instantiating profiles every time we want to access one
* Socket and live instance changes * Possible problem with Deno's handling of sockets, compatibility issue with Node?
This commit is contained in:
@@ -25,7 +25,8 @@
|
|||||||
"files": [],
|
"files": [],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"types": [
|
"types": [
|
||||||
"./src/types/express.ts"
|
"./src/types/express.ts",
|
||||||
|
"./src/types/http.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ import Logging from "@proxnet/undead-logging";
|
|||||||
import { decode } from "@gz/jwt";
|
import { decode } from "@gz/jwt";
|
||||||
import { Config } from "./config.ts";
|
import { Config } from "./config.ts";
|
||||||
import { AuthType, User, UserTokenFormat } from "./data/users.ts";
|
import { AuthType, User, UserTokenFormat } from "./data/users.ts";
|
||||||
import Profile, { ProfileTokenFormat } from "./data/profiles.ts";
|
import UnifiedProfile, { ProfileTokenFormat } from "./data/profiles.ts";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
import { IncomingMessage } from "node:http";
|
||||||
|
|
||||||
const config = Config.getConfig();
|
const config = Config.getConfig();
|
||||||
|
|
||||||
@@ -129,8 +130,16 @@ export function getSrcIpDefault(rq: express.Request): string {
|
|||||||
const xrIp = rq.header("x-real-ip");
|
const xrIp = rq.header("x-real-ip");
|
||||||
if (xrIp !== undefined) return xrIp;
|
if (xrIp !== undefined) return xrIp;
|
||||||
|
|
||||||
const ip = typeof rq.ip === "undefined" ? "(unknown source)" : rq.ip;
|
return typeof rq.ip === "undefined" ? "(unknown source)" : rq.ip;
|
||||||
return ip;
|
}
|
||||||
|
export function getSrcIpDefaultRaw(rq: IncomingMessage) {
|
||||||
|
const cfIp = rq.headers['cf-connecting-ip'];
|
||||||
|
if (cfIp) return cfIp;
|
||||||
|
|
||||||
|
const xrIp = rq.headers['x-real-ip'];
|
||||||
|
if (xrIp) return xrIp;
|
||||||
|
|
||||||
|
return rq.socket.remoteAddress ? rq.socket.remoteAddress : "(unknown source)";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function statusResponse(code: number) {
|
export function statusResponse(code: number) {
|
||||||
@@ -272,7 +281,7 @@ export async function Authentication(
|
|||||||
].includes(false);
|
].includes(false);
|
||||||
if (valid) {
|
if (valid) {
|
||||||
if (decodedToken.typ == AuthType.Web) rs.locals.user = new User(decodedToken.sub);
|
if (decodedToken.typ == AuthType.Web) rs.locals.user = new User(decodedToken.sub);
|
||||||
else if (decodedToken.typ == AuthType.Game) rs.locals.profile = new Profile(decodedToken.sub);
|
else if (decodedToken.typ == AuthType.Game) rs.locals.profile = UnifiedProfile.get(decodedToken.sub);
|
||||||
|
|
||||||
nxt();
|
nxt();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import Profile from "../profiles.ts";
|
import { Profile } from "../profiles.ts";
|
||||||
|
|
||||||
const loginLocks: Map<number, string> = new Map();
|
const loginLocks: Map<number, string> = new Map();
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import Logging from "@proxnet/undead-logging";
|
import Logging from "@proxnet/undead-logging";
|
||||||
import Profile from "../profiles.ts";
|
import { Profile } from "../profiles.ts";
|
||||||
import { RoomInstance, InstanceOptions } from "./types.ts";
|
import { RoomInstance, InstanceOptions } from "./types.ts";
|
||||||
import { Config } from "../../config.ts";
|
import { Config } from "../../config.ts";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { SettingKey } from "../content/settings.ts";
|
import { SettingKey } from "../content/settings.ts";
|
||||||
import Profile from "../profiles.ts";
|
import { Profile } from "../profiles.ts";
|
||||||
import { DeviceClass, PlayerStatusVisibility, RoomInstance, VRMovementMode } from "./types.ts";
|
import { DeviceClass, PlayerStatusVisibility, RoomInstance, VRMovementMode } from "./types.ts";
|
||||||
import Logging from "@proxnet/undead-logging";
|
import Logging from "@proxnet/undead-logging";
|
||||||
|
|
||||||
@@ -151,7 +151,7 @@ const Presence = new PresenceBase();
|
|||||||
const id = setInterval(() => {
|
const id = setInterval(() => {
|
||||||
Presence.deleteDeadPresences();
|
Presence.deleteDeadPresences();
|
||||||
}, 480000); // delete dead presences every 8 minutes
|
}, 480000); // delete dead presences every 8 minutes
|
||||||
Deno.addSignalListener("SIGINT", async () => {
|
Deno.addSignalListener("SIGINT", () => {
|
||||||
clearInterval(id);
|
clearInterval(id);
|
||||||
const presArray = Presence.getAllRawPresences();
|
const presArray = Presence.getAllRawPresences();
|
||||||
for (const pres of presArray.values()) clearInterval(pres.intervalId);
|
for (const pres of presArray.values()) clearInterval(pres.intervalId);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IntegratedRoomScene, RoomDetails } from "../content/roomtypes.ts";
|
import { IntegratedRoomScene, RoomDetails } from "../content/roomtypes.ts";
|
||||||
import Profile from "../profiles.ts";
|
import { Profile } from "../profiles.ts";
|
||||||
|
|
||||||
export enum PhotonRegionCodeString {
|
export enum PhotonRegionCodeString {
|
||||||
Europe = "eu",
|
Europe = "eu",
|
||||||
|
|||||||
@@ -242,4 +242,23 @@ class Profile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Profile;
|
const profiles: Map<number, Profile> = new Map()
|
||||||
|
|
||||||
|
class UnifiedProfileBase {
|
||||||
|
|
||||||
|
get(id: number) {
|
||||||
|
let profile = profiles.get(id);
|
||||||
|
if (!profile) {
|
||||||
|
const inst = new Profile(id);
|
||||||
|
profiles.set(id, inst);
|
||||||
|
profile = inst;
|
||||||
|
}
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const UnifiedProfile = new UnifiedProfileBase();
|
||||||
|
|
||||||
|
export { Profile };
|
||||||
|
export default UnifiedProfile;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Redis } from "../db.ts";
|
import { Redis } from "../db.ts";
|
||||||
import * as JsonWebToken from "@gz/jwt";
|
import * as JsonWebToken from "@gz/jwt";
|
||||||
import { Config } from "../config.ts";
|
import { Config } from "../config.ts";
|
||||||
import Profile from "./profiles.ts";
|
import { Profile } from "./profiles.ts";
|
||||||
import { TokenBaseFormat } from "../apiutils.ts";
|
import { TokenBaseFormat } from "../apiutils.ts";
|
||||||
|
|
||||||
type UserInitOptions = {
|
type UserInitOptions = {
|
||||||
|
|||||||
120
src/main.ts
120
src/main.ts
@@ -9,10 +9,8 @@ import express from "express";
|
|||||||
import WebSocket, { WebSocketServer } from "ws";
|
import WebSocket, { WebSocketServer } from "ws";
|
||||||
import { IncomingMessage } from "../../AppData/Local/deno/npm/registry.npmjs.org/@types/connect/3.4.38/index.d.ts";
|
import { IncomingMessage } from "../../AppData/Local/deno/npm/registry.npmjs.org/@types/connect/3.4.38/index.d.ts";
|
||||||
import { decode } from "@gz/jwt";
|
import { decode } from "@gz/jwt";
|
||||||
import Profile, { ProfileTokenFormat } from "./data/profiles.ts";
|
import UnifiedProfile, { ProfileTokenFormat } from "./data/profiles.ts";
|
||||||
import { SocketHandoff } from "./socket/handoff.ts";
|
import { SocketHandoff } from "./socket/handoff.ts";
|
||||||
import internal from "node:stream";
|
|
||||||
import { Buffer } from "node:buffer";
|
|
||||||
import { SignalRSocketHandler } from "./socket/socket.ts";
|
import { SignalRSocketHandler } from "./socket/socket.ts";
|
||||||
|
|
||||||
const instanceId = generateRandomString(64);
|
const instanceId = generateRandomString(64);
|
||||||
@@ -100,6 +98,47 @@ app.use((rq: express.Request, rs: express.Response) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Galvanic WebSocket Server
|
||||||
|
*/
|
||||||
|
|
||||||
|
type AuthResult = {
|
||||||
|
token?: ProfileTokenFormat,
|
||||||
|
valid: boolean
|
||||||
|
}
|
||||||
|
const authenticate = async (req: IncomingMessage) => {
|
||||||
|
const authHeader = req.headers.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 wss = new WebSocketServer({ noServer: true, path: "/notify/hub/v1" });
|
||||||
|
wss.on('connection', (socket: WebSocket, req: IncomingMessage) => {
|
||||||
|
if (!req.token) {
|
||||||
|
socket.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, () => {
|
const http = app.listen(config.web.port, config.web.host, () => {
|
||||||
log.n(`Listening on http://${config.web.host}:${config.web.port}`);
|
log.n(`Listening on http://${config.web.host}:${config.web.port}`);
|
||||||
|
|
||||||
@@ -110,60 +149,39 @@ try {
|
|||||||
log.i(`Shutting down`);
|
log.i(`Shutting down`);
|
||||||
|
|
||||||
http.close();
|
http.close();
|
||||||
|
http.closeAllConnections();
|
||||||
|
});
|
||||||
|
Deno.addSignalListener("SIGINT", () => {
|
||||||
|
for (const handoff of SocketHandoff.all()) handoff.complete();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const wss = new WebSocketServer({
|
// Currently not working in Deno. Socket problem?
|
||||||
server: http,
|
/*http.on('upgrade', async (req, socket, head) => {
|
||||||
path: "/notify/hub/v1"
|
log.d('Handling upgrade');
|
||||||
});
|
try {
|
||||||
wss.on('connection', (ws: WebSocket, rq: IncomingMessage, profile: Profile, connectionId: string) => {
|
const authResult = await authenticate(req);
|
||||||
const handoff = SocketHandoff.find(connectionId);
|
|
||||||
if (handoff) handoff.complete();
|
|
||||||
log.d(typeof profile);
|
|
||||||
new SignalRSocketHandler(ws, profile);
|
|
||||||
});
|
|
||||||
http.on('upgrade', async (rq: IncomingMessage, socket: internal.Duplex, head: Buffer) => {
|
|
||||||
const errorHandler = (err: Error | undefined) => { log.e(`Socket error: ${err?.stack}`); };
|
|
||||||
socket.on('error', errorHandler);
|
|
||||||
|
|
||||||
function writeUnauthorized() {
|
if (authResult.valid) {
|
||||||
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
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();
|
socket.destroy();
|
||||||
}
|
}
|
||||||
|
});*/
|
||||||
let wrong = false;
|
|
||||||
const unparsedToken = rq.headers['Authorization'];
|
|
||||||
const connectionId = new URL(`http://${rq.headers.host ? rq.headers.host : 'localhost'}${rq.url}`).searchParams.get('connectionId');
|
|
||||||
if (connectionId == null) wrong = true;
|
|
||||||
else {
|
|
||||||
if (typeof unparsedToken == 'string') {
|
|
||||||
const splitToken = unparsedToken.split(' ')[1]
|
|
||||||
if (splitToken) {
|
|
||||||
try {
|
|
||||||
|
|
||||||
const decodedToken = await decode<ProfileTokenFormat>(splitToken, config.auth.secret, {algorithm: 'HS512'});
|
|
||||||
const schemaResult = ProfileTokenSchema.safeParse(decodedToken);
|
|
||||||
if (!schemaResult.success) wrong = true;
|
|
||||||
else {
|
|
||||||
wss.handleUpgrade(rq, socket, head, (ws) => {
|
|
||||||
wss.emit('connection', ws, rq, new Profile(decodedToken.sub), connectionId);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch {
|
|
||||||
wrong = true;
|
|
||||||
}
|
|
||||||
} else wrong = true;
|
|
||||||
} else wrong = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wrong) {
|
|
||||||
writeUnauthorized();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
socket.removeListener('error', errorHandler);
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.e(`Cannot start: Network could not be initalized. ${err}`);
|
log.e(`Cannot start: Network could not be initalized. ${err}`);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { APIUtils } from "../../apiutils.ts";
|
import { APIUtils } from "../../apiutils.ts";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import Profile from "../../data/profiles.ts";
|
import { Profile } from "../../data/profiles.ts";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const route = APIUtils.createRouter("/account");
|
export const route = APIUtils.createRouter("/account");
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { APIUtils, NoBody } from "../../apiutils.ts";
|
import { APIUtils, NoBody } from "../../apiutils.ts";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import Profile from "../../data/profiles.ts";
|
import UnifiedProfile, { Profile } from "../../data/profiles.ts";
|
||||||
import { decode } from "@gz/jwt";
|
import { decode } from "@gz/jwt";
|
||||||
import { Config } from "../../config.ts";
|
import { Config } from "../../config.ts";
|
||||||
import Logging from "@proxnet/undead-logging";
|
import Logging from "@proxnet/undead-logging";
|
||||||
@@ -143,7 +143,7 @@ route.router.post("/token",
|
|||||||
rs.locals.user.addAssociatedDeviceId(rq.body.device_id);
|
rs.locals.user.addAssociatedDeviceId(rq.body.device_id);
|
||||||
rs.locals.user.addAssociatedPlatformId(rq.body.platform_id);
|
rs.locals.user.addAssociatedPlatformId(rq.body.platform_id);
|
||||||
|
|
||||||
const profile = new Profile(targetAccount);
|
const profile = UnifiedProfile.get(targetAccount);
|
||||||
if (!(await Profile.exists(profile.getId()))) {
|
if (!(await Profile.exists(profile.getId()))) {
|
||||||
requestFailed();
|
requestFailed();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -11,10 +11,14 @@ function randomId(length: number) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lots of this is redundant. The WebSocket request already contains an access token for the profile, but I'd
|
/**
|
||||||
// like to make sure that connectionIds are freed automatically.
|
* Reserve `connectionId`s for each connected client, hand off to socket handler when complete or timed out
|
||||||
|
*/
|
||||||
export class SocketHandoff {
|
export class SocketHandoff {
|
||||||
|
|
||||||
|
static all() {
|
||||||
|
return Array.from(handoffs.values());
|
||||||
|
}
|
||||||
static generateId() {
|
static generateId() {
|
||||||
let id = randomId(48);
|
let id = randomId(48);
|
||||||
while (handoffs.values().find(handoff => handoff.id == id)) id = randomId(48);
|
while (handoffs.values().find(handoff => handoff.id == id)) id = randomId(48);
|
||||||
@@ -32,19 +36,15 @@ export class SocketHandoff {
|
|||||||
this.id = SocketHandoff.generateId();
|
this.id = SocketHandoff.generateId();
|
||||||
|
|
||||||
this.#timeout = setTimeout(() => {
|
this.#timeout = setTimeout(() => {
|
||||||
handoffs.delete(this);
|
this.complete();
|
||||||
});
|
});
|
||||||
|
|
||||||
handoffs.add(this);
|
handoffs.add(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
delete() {
|
complete() {
|
||||||
clearTimeout(this.#timeout);
|
clearTimeout(this.#timeout);
|
||||||
handoffs.delete(this);
|
handoffs.delete(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
complete() {
|
|
||||||
this.delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import WebSocket from "ws";
|
import WebSocket from "ws";
|
||||||
import Profile from "../data/profiles.ts";
|
import { Profile } from "../data/profiles.ts";
|
||||||
import { IncomingMessage } from "node:http";
|
|
||||||
import Logging from "@proxnet/undead-logging";
|
import Logging from "@proxnet/undead-logging";
|
||||||
|
|
||||||
export class SignalRSocketHandler {
|
export class SignalRSocketHandler {
|
||||||
@@ -13,13 +12,11 @@ export class SignalRSocketHandler {
|
|||||||
constructor(socket: WebSocket, player: Profile) {
|
constructor(socket: WebSocket, player: Profile) {
|
||||||
|
|
||||||
this.#socket = socket;
|
this.#socket = socket;
|
||||||
|
this.#initLogSource();
|
||||||
|
|
||||||
this.#profile = player;
|
this.#profile = player;
|
||||||
|
|
||||||
throw new Error("This will fail due to undefined access attempt. Debug this. Also, please unify profiles.");
|
|
||||||
player.setSocketHandler(this);
|
player.setSocketHandler(this);
|
||||||
this.log.source += player.getId().toString();
|
|
||||||
|
|
||||||
// log: we connected!!
|
|
||||||
|
|
||||||
Deno.addSignalListener('SIGINT', this.destroy);
|
Deno.addSignalListener('SIGINT', this.destroy);
|
||||||
|
|
||||||
@@ -30,4 +27,17 @@ export class SignalRSocketHandler {
|
|||||||
Deno.removeSignalListener('SIGINT', this.destroy);
|
Deno.removeSignalListener('SIGINT', this.destroy);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async #initLogSource() {
|
||||||
|
this.log.source += this.#profile.getId().toString();
|
||||||
|
|
||||||
|
this.log.i(`Player '${(await this.#profile.export())?.username}' (${this.#profile.getId()}) created hub socket`);
|
||||||
|
|
||||||
|
this.#socket.on('open', () => {
|
||||||
|
this.log.d(`hello world`)
|
||||||
|
});
|
||||||
|
this.#socket.on('message', data => {
|
||||||
|
this.log.d(data.toString());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import Profile from "../data/profiles.ts";
|
import { Profile } from "../data/profiles.ts";
|
||||||
import { User } from "../data/users.ts";
|
import { User } from "../data/users.ts";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
8
src/types/http.ts
Normal file
8
src/types/http.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { ProfileTokenFormat } from "../data/profiles.ts";
|
||||||
|
|
||||||
|
// Extend IncomingMessage interface to include custom properties
|
||||||
|
declare module 'node:http' {
|
||||||
|
interface IncomingMessage {
|
||||||
|
token?: ProfileTokenFormat;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user