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:
@@ -12,6 +12,7 @@
|
||||
"@std/http": "jsr:@std/http@^1.0.13",
|
||||
"@types/cookie-parser": "npm:@types/cookie-parser@^1.4.8",
|
||||
"@types/express": "npm:@types/express@^5.0.0",
|
||||
"@types/node": "npm:@types/node@^22.13.14",
|
||||
"@types/validator": "npm:@types/validator@^13.12.2",
|
||||
"@types/ws": "npm:@types/ws@^8.18.0",
|
||||
"cookie-parser": "npm:cookie-parser@^1.4.7",
|
||||
|
||||
8
deno.lock
generated
8
deno.lock
generated
@@ -23,6 +23,7 @@
|
||||
"npm:@types/express@*": "5.0.0",
|
||||
"npm:@types/express@5": "5.0.0",
|
||||
"npm:@types/node@*": "22.5.4",
|
||||
"npm:@types/node@^22.13.14": "22.13.14",
|
||||
"npm:@types/validator@^13.12.2": "13.12.2",
|
||||
"npm:@types/ws@^8.18.0": "8.18.0",
|
||||
"npm:chalk@^5.3.0": "5.3.0",
|
||||
@@ -228,6 +229,12 @@
|
||||
"undici-types@6.20.0"
|
||||
]
|
||||
},
|
||||
"@types/node@22.13.14": {
|
||||
"integrity": "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==",
|
||||
"dependencies": [
|
||||
"undici-types@6.20.0"
|
||||
]
|
||||
},
|
||||
"@types/node@22.5.4": {
|
||||
"integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==",
|
||||
"dependencies": [
|
||||
@@ -931,6 +938,7 @@
|
||||
"jsr:@std/http@^1.0.13",
|
||||
"npm:@types/cookie-parser@^1.4.8",
|
||||
"npm:@types/express@5",
|
||||
"npm:@types/node@^22.13.14",
|
||||
"npm:@types/validator@^13.12.2",
|
||||
"npm:@types/ws@^8.18.0",
|
||||
"npm:cookie-parser@^1.4.7",
|
||||
|
||||
@@ -6,7 +6,6 @@ import { Config } from "./config.ts";
|
||||
import { AuthType, User, UserTokenFormat } from "./data/users.ts";
|
||||
import UnifiedProfile, { ProfileTokenFormat } from "./data/profiles.ts";
|
||||
import z from "zod";
|
||||
import { IncomingMessage } from "node:http";
|
||||
|
||||
const config = Config.getConfig();
|
||||
|
||||
@@ -25,6 +24,11 @@ export function createRouter(path: string) {
|
||||
return router;
|
||||
}
|
||||
|
||||
export function setCacheAllowed(_rq: express.Request, rs: express.Response, nxt: express.NextFunction) {
|
||||
rs.setHeader("Cache-Control", 'public');
|
||||
nxt();
|
||||
}
|
||||
|
||||
export function generateRandomString(length: number) {
|
||||
const characters =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
@@ -132,14 +136,14 @@ export function getSrcIpDefault(rq: express.Request): string {
|
||||
|
||||
return typeof rq.ip === "undefined" ? "(unknown source)" : rq.ip;
|
||||
}
|
||||
export function getSrcIpDefaultRaw(rq: IncomingMessage) {
|
||||
const cfIp = rq.headers['cf-connecting-ip'];
|
||||
export function getSrcIpDefaultDeno(req: Request, info: Deno.ServeHandlerInfo<Deno.NetAddr>) {
|
||||
const cfIp = req.headers.get('cf-connecting-ip');
|
||||
if (cfIp) return cfIp;
|
||||
|
||||
const xrIp = rq.headers['x-real-ip'];
|
||||
const xrIp = req.headers.get('x-real-ip');
|
||||
if (xrIp) return xrIp;
|
||||
|
||||
return rq.socket.remoteAddress ? rq.socket.remoteAddress : "(unknown source)";
|
||||
return info.remoteAddr.hostname;
|
||||
}
|
||||
|
||||
export function statusResponse(code: number) {
|
||||
@@ -277,11 +281,12 @@ export async function Authentication(
|
||||
}
|
||||
|
||||
const valid = ![ // used to contain more conditions, now is only 1
|
||||
decodedToken.iss == `${config.web.securepublichost ? 'https' : 'http'}://${config.web.publichost}`,
|
||||
decodedToken.iss == `${config.web.api.securepublichost ? 'https' : 'http'}://${config.web.api.publichost}`,
|
||||
].includes(false);
|
||||
if (valid) {
|
||||
if (decodedToken.typ == AuthType.Web) rs.locals.user = new User(decodedToken.sub);
|
||||
else if (decodedToken.typ == AuthType.Game) rs.locals.profile = UnifiedProfile.get(decodedToken.sub);
|
||||
rs.locals.token = token;
|
||||
|
||||
nxt();
|
||||
} else {
|
||||
@@ -294,6 +299,18 @@ export async function Authentication(
|
||||
}
|
||||
}
|
||||
|
||||
export function AuthenticationType(type: AuthType) {
|
||||
return (_rq: express.Request, rs: express.Response, nxt: express.NextFunction) => {
|
||||
const profile = rs.locals.profile;
|
||||
const user = rs.locals.user;
|
||||
|
||||
if ((type == AuthType.Game && !profile) || (type == AuthType.Web && !user)) {
|
||||
rs.json(genericResponseFormat(true, 'Wrong authentication type provided.'));
|
||||
return;
|
||||
} else nxt();
|
||||
}
|
||||
}
|
||||
|
||||
export type NoBody = Record<string | number | symbol, never>;
|
||||
|
||||
export * as APIUtils from "./apiutils.ts";
|
||||
|
||||
@@ -17,6 +17,11 @@ type WebConfiguration = {
|
||||
host: string;
|
||||
publichost: string;
|
||||
securepublichost: boolean;
|
||||
}
|
||||
|
||||
type WebRootConfiguration = {
|
||||
api: WebConfiguration,
|
||||
socket: WebConfiguration
|
||||
};
|
||||
|
||||
type PublicConfiguration = {
|
||||
@@ -52,7 +57,7 @@ type AuthConfiguration = {
|
||||
|
||||
export type GalvanicConfiguration = {
|
||||
redis: RedisConfiguration;
|
||||
web: WebConfiguration;
|
||||
web: WebRootConfiguration;
|
||||
public: PublicConfiguration;
|
||||
logging: LoggingConfiguration;
|
||||
discord: DiscordConfiguration | null;
|
||||
@@ -68,10 +73,18 @@ export const defaultConfig: GalvanicConfiguration = {
|
||||
db: 0,
|
||||
},
|
||||
web: {
|
||||
port: 3000,
|
||||
host: "127.0.0.1",
|
||||
publichost: "127.0.0.1:3000",
|
||||
securepublichost: false,
|
||||
api: {
|
||||
port: 13370,
|
||||
host: "127.0.0.1",
|
||||
publichost: "127.0.0.1:13370",
|
||||
securepublichost: false,
|
||||
},
|
||||
socket: {
|
||||
port: 13371,
|
||||
host: "127.0.0.1",
|
||||
publichost: "127.0.0.1:13371",
|
||||
securepublichost: false,
|
||||
}
|
||||
},
|
||||
public: {
|
||||
serverName: "Galvanic Corrosion",
|
||||
|
||||
@@ -19,6 +19,9 @@ export type PublicConfig = {
|
||||
ConfigTable: Config[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Plain public config, NOT GameConfigs
|
||||
*/
|
||||
export function getConfig() {
|
||||
const c = Config.getConfig();
|
||||
if (typeof c == "undefined") return null;
|
||||
|
||||
@@ -55,7 +55,7 @@ class PlayerPresence {
|
||||
const PlayerStatusVisibilityEnum = z.nativeEnum(PlayerStatusVisibility);
|
||||
type PlayerStatusVisibilityEnum = z.infer<typeof PlayerStatusVisibilityEnum>;
|
||||
|
||||
const visibilityResult = PlayerStatusVisibilityEnum.safeParse(await this.#profile.getSetting(SettingKey.PlayerStatusVisibility));
|
||||
const visibilityResult = PlayerStatusVisibilityEnum.safeParse(await this.#profile.Settings.getSetting(SettingKey.PlayerStatusVisibility));
|
||||
if (visibilityResult.success) this.statusVisibility = visibilityResult.data;
|
||||
}
|
||||
|
||||
|
||||
15
src/data/profile/profilemanagerbase.ts
Normal file
15
src/data/profile/profilemanagerbase.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export class ProfileContentManager {
|
||||
|
||||
profileNotSetError = new Error("The profile on this manager is not set.");
|
||||
|
||||
profileId: number | null = null;
|
||||
|
||||
setProfile(id: number) {
|
||||
this.profileId = id;
|
||||
}
|
||||
|
||||
profileIsSet() {
|
||||
return this.profileId !== null;
|
||||
}
|
||||
|
||||
}
|
||||
35
src/data/profile/progression.ts
Normal file
35
src/data/profile/progression.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Config } from "../../config.ts";
|
||||
import { GameConfigs } from "../config.ts";
|
||||
import { ProfileContentManager } from "./profilemanagerbase.ts";
|
||||
|
||||
const serverConfig = Config.getConfig();
|
||||
const config = GameConfigs.getConfig();
|
||||
/**
|
||||
* Level -> Required XP
|
||||
*/
|
||||
const requiredXpMap: Map<number, number> = new Map();
|
||||
|
||||
export class ProfileProgressionManager extends ProfileContentManager {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
// fill `requiredXpMap` using `config.public` values
|
||||
}
|
||||
|
||||
#getRequiredXp(level: number) {
|
||||
if (level > serverConfig.public.maxLevels) return null;
|
||||
else {
|
||||
const req = requiredXpMap.get(level);
|
||||
return req ? req : null;
|
||||
}
|
||||
}
|
||||
|
||||
getLevel() {
|
||||
return 30; // temporary
|
||||
}
|
||||
|
||||
getXp() {
|
||||
return 0; // temporary
|
||||
}
|
||||
|
||||
}
|
||||
40
src/data/profile/settings.ts
Normal file
40
src/data/profile/settings.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Redis } from "../../db.ts";
|
||||
import { SettingKey } from "../content/settings.ts";
|
||||
import { ProfileContentManager } from "./profilemanagerbase.ts";
|
||||
|
||||
export interface Setting {
|
||||
Key: string;
|
||||
Value: string;
|
||||
}
|
||||
|
||||
export class ProfileSettingsManager extends ProfileContentManager {
|
||||
|
||||
async getSettings() {
|
||||
if (!this.profileIsSet()) throw this.profileNotSetError;
|
||||
const settings = await Redis.Database.hgetall(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profileId!.toString(), Redis.KeyGroups.Profiles.Settings));
|
||||
const returnSettings: Setting[] = [];
|
||||
for (const key of Object.keys(settings)) returnSettings.push({ Key: key, Value: settings[key] });
|
||||
return returnSettings;
|
||||
}
|
||||
|
||||
async getSetting(key: SettingKey) {
|
||||
if (!this.profileIsSet()) throw this.profileNotSetError;
|
||||
return await Redis.Database.hget(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profileId!.toString(), Redis.KeyGroups.Profiles.Settings), key);
|
||||
}
|
||||
|
||||
async setSetting(key: SettingKey, value: string) {
|
||||
if (!this.profileIsSet()) throw this.profileNotSetError;
|
||||
await Redis.Database.hset(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profileId!.toString(), Redis.KeyGroups.Profiles.Settings), key, value);
|
||||
}
|
||||
|
||||
async delSetting(key: SettingKey) {
|
||||
if (!this.profileIsSet()) throw this.profileNotSetError;
|
||||
await Redis.Database.hdel(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profileId!.toString(), Redis.KeyGroups.Profiles.Settings), key);
|
||||
}
|
||||
|
||||
async delAllSettings() {
|
||||
if (!this.profileIsSet()) throw this.profileNotSetError;
|
||||
await Redis.Database.del(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profileId!.toString(), Redis.KeyGroups.Profiles.Settings));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,10 +5,11 @@ import { AuthType } from "./users.ts";
|
||||
import * as JsonWebToken from "@gz/jwt";
|
||||
import { TokenBaseFormat } from "../apiutils.ts";
|
||||
import { DeviceClass, RoomInstance, VRMovementMode } from "./live/types.ts";
|
||||
import { Setting } from "./profiletypes.ts";
|
||||
import { SettingKey } from "./content/settings.ts";
|
||||
import { z } from "zod";
|
||||
import { SignalRSocketHandler } from "../socket/socket.ts";
|
||||
import { ProfileSettingsManager } from "./profile/settings.ts";
|
||||
import { ProfileProgressionManager } from "./profile/progression.ts";
|
||||
|
||||
const config = Config.getConfig();
|
||||
|
||||
@@ -140,8 +141,15 @@ class Profile {
|
||||
|
||||
#socket: SignalRSocketHandler | null = null;
|
||||
|
||||
Settings = new ProfileSettingsManager();
|
||||
Progression = new ProfileProgressionManager();
|
||||
|
||||
constructor(id: number) {
|
||||
this.#id = id;
|
||||
|
||||
// Set IDs for all content managers
|
||||
this.Settings.setProfile(this.#id);
|
||||
this.Progression.setProfile(this.#id);
|
||||
}
|
||||
|
||||
setInstance(instance: RoomInstance | null) {
|
||||
@@ -164,29 +172,6 @@ class Profile {
|
||||
return await Profile.getExportAccount(this.#id);
|
||||
}
|
||||
|
||||
async getSettings() {
|
||||
const settings = await Redis.Database.hgetall(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.#id.toString(), Redis.KeyGroups.Profiles.Settings));
|
||||
const returnSettings: Setting[] = [];
|
||||
for (const key of Object.keys(settings)) returnSettings.push({Key: key, Value: settings[key]});
|
||||
return returnSettings;
|
||||
}
|
||||
|
||||
async getSetting(key: SettingKey) {
|
||||
return await Redis.Database.hget(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.#id.toString(), Redis.KeyGroups.Profiles.Settings), key);
|
||||
}
|
||||
|
||||
async setSetting(key: SettingKey, value: string) {
|
||||
await Redis.Database.hset(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.#id.toString(), Redis.KeyGroups.Profiles.Settings), key, value);
|
||||
}
|
||||
|
||||
async delSetting(key: SettingKey) {
|
||||
await Redis.Database.hdel(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.#id.toString(), Redis.KeyGroups.Profiles.Settings), key);
|
||||
}
|
||||
|
||||
async delAllSettings() {
|
||||
await Redis.Database.del(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.#id.toString(), Redis.KeyGroups.Profiles.Settings));
|
||||
}
|
||||
|
||||
async setKnownDeviceClass(deviceClass: string | number) {
|
||||
await Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.#id.toString(), Redis.KeyGroups.Profiles.DeviceClass), deviceClass);
|
||||
}
|
||||
@@ -203,11 +188,11 @@ class Profile {
|
||||
}
|
||||
|
||||
async setVRMovementMode(movementMode: string | number) {
|
||||
return await this.setSetting(SettingKey.VRMovementMode, movementMode.toString());
|
||||
return await this.Settings.setSetting(SettingKey.VRMovementMode, movementMode.toString());
|
||||
}
|
||||
|
||||
async getVRMovementMode() {
|
||||
const data = await this.getSetting(SettingKey.VRMovementMode);
|
||||
const data = await this.Settings.getSetting(SettingKey.VRMovementMode);
|
||||
|
||||
const VRMovementModeEnum = z.nativeEnum(VRMovementMode);
|
||||
type VRMovementModeEnum = z.infer<typeof VRMovementModeEnum>
|
||||
@@ -232,7 +217,7 @@ class Profile {
|
||||
|
||||
async getToken() {
|
||||
const payload: ProfileTokenFormat = {
|
||||
iss: `${config.web.securepublichost ? 'https' : 'http'}://${config.web.publichost}`,
|
||||
iss: `${config.web.api.securepublichost ? 'https' : 'http'}://${config.web.api.publichost}`,
|
||||
sub: this.#id,
|
||||
role: (await this.getIsOperator()) ? 'developer' : 'user',
|
||||
exp: Math.round(Date.now() / 1000) + Math.round(config.auth.timeout * 60 * 60),
|
||||
@@ -256,6 +241,14 @@ class UnifiedProfileBase {
|
||||
return profile;
|
||||
}
|
||||
|
||||
async create(options: ProfileInitOptions) {
|
||||
return await Profile.init(options);
|
||||
}
|
||||
|
||||
async exists(id: number) {
|
||||
return await Profile.exists(id);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const UnifiedProfile = new UnifiedProfileBase();
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export interface Setting {
|
||||
Key: string;
|
||||
Value: string;
|
||||
}
|
||||
@@ -9,10 +9,6 @@ type UserInitOptions = {
|
||||
pubkey: string;
|
||||
};
|
||||
|
||||
type UserCreatedObj = {
|
||||
user: User;
|
||||
};
|
||||
|
||||
export enum AuthType {
|
||||
Game,
|
||||
Web,
|
||||
@@ -78,7 +74,7 @@ export class User {
|
||||
|
||||
async getToken() {
|
||||
const payload: UserTokenFormat = {
|
||||
iss: `${config.web.securepublichost ? 'https' : 'http'}://${config.web.publichost}`,
|
||||
iss: `${config.web.api.securepublichost ? 'https' : 'http'}://${config.web.api.publichost}`,
|
||||
sub: this.#client_id,
|
||||
exp: Math.round(Date.now() / 1000) + (config.auth.timeout * 60 * 60),
|
||||
typ: AuthType.Web
|
||||
|
||||
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}`);
|
||||
|
||||
@@ -2,6 +2,8 @@ import { route as VersionCheckRoute } from "./api/versioncheck.ts";
|
||||
import { route as ConfigRoute } from "./api/config.ts";
|
||||
import { route as GameConfig } from "./api/gameconfigs.ts";
|
||||
import { route as PlayerReportingRoute } from "./api/PlayerReporting.ts";
|
||||
import { route as MessagesRoute } from "./api/messages.ts";
|
||||
import { route as RelationshipsRoute } from "./api/relationships.ts";
|
||||
import { APIUtils } from "../apiutils.ts";
|
||||
|
||||
export const route = APIUtils.createRouter("/api");
|
||||
@@ -9,4 +11,6 @@ export const route = APIUtils.createRouter("/api");
|
||||
route.router.use(VersionCheckRoute.path, VersionCheckRoute.router);
|
||||
route.router.use(ConfigRoute.path, ConfigRoute.router);
|
||||
route.router.use(GameConfig.path, GameConfig.router);
|
||||
route.router.use(PlayerReportingRoute.path, PlayerReportingRoute.router);
|
||||
route.router.use(PlayerReportingRoute.path, PlayerReportingRoute.router);
|
||||
route.router.use(MessagesRoute.path, MessagesRoute.router);
|
||||
route.router.use(RelationshipsRoute.path, RelationshipsRoute.router);
|
||||
@@ -8,3 +8,10 @@ route.router.get("/v2", (_rq, rs) => {
|
||||
if (config == null) rs.sendStatus(500);
|
||||
else rs.json(config);
|
||||
});
|
||||
|
||||
route.router.get('/v1/amplitude',
|
||||
APIUtils.setCacheAllowed,
|
||||
(_rq, rs) => {
|
||||
rs.json({AmplitudeKey: ""});
|
||||
}
|
||||
);
|
||||
13
src/routes/api/messages.ts
Normal file
13
src/routes/api/messages.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { APIUtils } from "../../apiutils.ts";
|
||||
|
||||
export const route = APIUtils.createRouter("/messages");
|
||||
|
||||
route.router.get('/v2/get',
|
||||
|
||||
APIUtils.Authentication,
|
||||
|
||||
(_rq, rs) => {
|
||||
rs.json([]); // temporary
|
||||
}
|
||||
|
||||
)
|
||||
17
src/routes/api/players.ts
Normal file
17
src/routes/api/players.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { APIUtils } from "../../apiutils.ts";
|
||||
|
||||
export const route = APIUtils.createRouter("/players");
|
||||
|
||||
route.router.get('/v1/progression/:id',
|
||||
|
||||
APIUtils.Authentication,
|
||||
|
||||
async (_rq, rs) => {
|
||||
rs.json({
|
||||
PlayerId: rs.locals.profile.getId(),
|
||||
Level: await rs.locals.profile.Progression.getLevel(), // await is temporary
|
||||
Xp: await rs.locals.profile.Progression.getXp()
|
||||
});
|
||||
}
|
||||
|
||||
);
|
||||
15
src/routes/api/relationships.ts
Normal file
15
src/routes/api/relationships.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { APIUtils } from "../../apiutils.ts";
|
||||
import { AuthType } from "../../data/users.ts";
|
||||
|
||||
export const route = APIUtils.createRouter("/relationships");
|
||||
|
||||
route.router.get('/v2/get',
|
||||
|
||||
APIUtils.Authentication,
|
||||
APIUtils.AuthenticationType(AuthType.Game),
|
||||
|
||||
(_rq, rs) => {
|
||||
rs.json([]); // temporary
|
||||
}
|
||||
|
||||
);
|
||||
@@ -1,10 +1,12 @@
|
||||
import { APIUtils } from "../../apiutils.ts";
|
||||
import { AuthType } from "../../data/users.ts";
|
||||
|
||||
export const route = APIUtils.createRouter("/cachedlogin");
|
||||
|
||||
route.router.get("/forplatformid/:platformtype/:platformid",
|
||||
|
||||
APIUtils.Authentication,
|
||||
APIUtils.AuthenticationType(AuthType.Web),
|
||||
|
||||
async (_rq, rs) => {
|
||||
const profiles = await rs.locals.user.exportAssociatedProfiles();
|
||||
|
||||
@@ -5,6 +5,7 @@ import { decode } from "@gz/jwt";
|
||||
import { Config } from "../../config.ts";
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
import { z } from "zod";
|
||||
import { AuthType } from "../../data/users.ts";
|
||||
|
||||
const config = Config.getConfig();
|
||||
|
||||
@@ -75,6 +76,7 @@ interface TokenResponseBody {
|
||||
route.router.post("/token",
|
||||
|
||||
APIUtils.Authentication,
|
||||
APIUtils.AuthenticationType(AuthType.Web),
|
||||
express.urlencoded({ extended: true }),
|
||||
APIUtils.validateRequestBody<AuthBodyBase>(TokenRequestBodySchema),
|
||||
|
||||
@@ -149,6 +151,9 @@ route.router.post("/token",
|
||||
return;
|
||||
}
|
||||
|
||||
const details = await profile.export();
|
||||
log.i(`Player ${details?.username} "${details?.displayName}" (${profile.getId()}) logged in`);
|
||||
|
||||
const token = await profile.getToken();
|
||||
rs.json({
|
||||
access_token: token,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { APIUtils, NoBody } from "../../apiutils.ts";
|
||||
import express from "express";
|
||||
import Matchmaking from "../../data/live/base.ts";
|
||||
import Presence from "../../data/live/presence.ts";
|
||||
import { AuthType } from "../../data/users.ts";
|
||||
|
||||
export const route = APIUtils.createRouter('/player');
|
||||
|
||||
@@ -17,6 +18,7 @@ const LoginSchema = z.object({
|
||||
route.router.post('/login',
|
||||
|
||||
APIUtils.Authentication,
|
||||
APIUtils.AuthenticationType(AuthType.Game),
|
||||
express.urlencoded({extended: true}),
|
||||
APIUtils.validateRequestBody(LoginSchema),
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { APIUtils } from "../apiutils.ts";
|
||||
import { Config } from "../config.ts";
|
||||
|
||||
const config = Config.getConfig() as Config.GalvanicConfiguration;
|
||||
const protocol = config.web.securepublichost ? "https" : "http";
|
||||
const protocol = config.web.api.securepublichost ? "https" : "http";
|
||||
|
||||
export const route = APIUtils.createRouter("/ns");
|
||||
|
||||
@@ -21,17 +21,17 @@ type NameserverHosts = {
|
||||
};
|
||||
|
||||
const nameserver: NameserverHosts = {
|
||||
Auth: `${protocol}://${config.web.publichost}/auth`,
|
||||
API: `${protocol}://${config.web.publichost}`,
|
||||
WWW: `${protocol}://${config.web.publichost}`,
|
||||
Notifications: `${protocol}://${config.web.publichost}/notify`,
|
||||
Images: `${protocol}://${config.web.publichost}/img`,
|
||||
CDN: `${protocol}://${config.web.publichost}/cdn`,
|
||||
Commerce: `${protocol}://${config.web.publichost}/commerce`,
|
||||
Matchmaking: `${protocol}://${config.web.publichost}/match`,
|
||||
Storage: `${protocol}://${config.web.publichost}/storage`,
|
||||
Chat: `${protocol}://${config.web.publichost}/chat`,
|
||||
Leaderboard: `${protocol}://${config.web.publichost}/leaderboard`,
|
||||
Auth: `${protocol}://${config.web.api.publichost}/auth`,
|
||||
API: `${protocol}://${config.web.api.publichost}`,
|
||||
WWW: `${protocol}://${config.web.api.publichost}`,
|
||||
Notifications: `${protocol}://${config.web.api.publichost}/notify`,
|
||||
Images: `${protocol}://${config.web.api.publichost}/img`,
|
||||
CDN: `${protocol}://${config.web.api.publichost}/cdn`,
|
||||
Commerce: `${protocol}://${config.web.api.publichost}/commerce`,
|
||||
Matchmaking: `${protocol}://${config.web.api.publichost}/match`,
|
||||
Storage: `${protocol}://${config.web.api.publichost}/storage`,
|
||||
Chat: `${protocol}://${config.web.api.publichost}/chat`,
|
||||
Leaderboard: `${protocol}://${config.web.api.publichost}/leaderboard`,
|
||||
};
|
||||
|
||||
route.router.get("*", (_rq, rs) => {
|
||||
|
||||
@@ -1,17 +1,27 @@
|
||||
import { APIUtils } from "../apiutils.ts";
|
||||
import { Config } from "../config.ts";
|
||||
import { AuthType } from "../data/users.ts";
|
||||
import { SocketHandoff } from "./handoff.ts";
|
||||
import express from "express";
|
||||
|
||||
const config = Config.getConfig();
|
||||
|
||||
export const route = APIUtils.createRouter('/notify');
|
||||
|
||||
route.router.post('/hub/v1/negotiate',
|
||||
|
||||
APIUtils.Authentication,
|
||||
APIUtils.AuthenticationType(AuthType.Game),
|
||||
express.urlencoded({ extended: true }),
|
||||
APIUtils.logBody,
|
||||
|
||||
(_rq, rs) => {
|
||||
const handoff = new SocketHandoff();
|
||||
rs.json({
|
||||
connectionId: handoff.id,
|
||||
availableTransports: [{transport:"WebSockets",transferFormats:["Text"]}]
|
||||
availableTransports: [{transport:"WebSockets",transferFormats:["Text"]}],
|
||||
url: `${config.web.socket.securepublichost ? 'https' : 'http'}://${config.web.socket.publichost}/`,
|
||||
accessToken: rs.locals.token
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -1,43 +1,77 @@
|
||||
import WebSocket from "ws";
|
||||
import { Profile } from "../data/profiles.ts";
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
import { Message, MessageKind, SignalMessageType, SignalRMessage, SignalRMessageSchema, TargetResult, TargetResultFailure, TargetResultSuccess, TargetResultType } from "./types.ts";
|
||||
import { SocketTarget } from "./targets/targetbase.ts";
|
||||
import { PlayerSocketSubscriptionTarget } from "./targets/SubscribeToPlayers.ts";
|
||||
|
||||
export class SignalRSocketHandler {
|
||||
|
||||
log: Logging = new Logging("SignalMock-");
|
||||
#log: Logging = new Logging("SignalMock-");
|
||||
|
||||
#socket: WebSocket;
|
||||
#profile: Profile;
|
||||
|
||||
#Targets: Map<string, SocketTarget> = new Map();
|
||||
|
||||
constructor(socket: WebSocket, player: Profile) {
|
||||
|
||||
this.#socket = socket;
|
||||
this.#initLogSource();
|
||||
|
||||
this.#profile = player;
|
||||
|
||||
this.#init();
|
||||
|
||||
player.setSocketHandler(this);
|
||||
|
||||
Deno.addSignalListener('SIGINT', this.destroy);
|
||||
this.#Targets.set('SubscribeToPlayers', new PlayerSocketSubscriptionTarget());
|
||||
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.#socket.close();
|
||||
Deno.removeSignalListener('SIGINT', this.destroy);
|
||||
async #dispatchTarget<T = unknown>(target: string, args: object[]): Promise<TargetResult> {
|
||||
const targetExec = this.#Targets.get(target);
|
||||
if (!targetExec) return { type: TargetResultType.Failure } as TargetResultFailure;
|
||||
else return { type: TargetResultType.Success, data: await targetExec.exec(args) } as TargetResultSuccess<T>;
|
||||
}
|
||||
|
||||
async #initLogSource() {
|
||||
this.log.source += this.#profile.getId().toString();
|
||||
#onMessage(message: Message) {
|
||||
if (message.kind == MessageKind.Protocol) {
|
||||
this.#send({});
|
||||
return;
|
||||
} else {
|
||||
this.#log.d(`CLIENT MESSAGE\n Type: ${message.data.type} (${SignalMessageType[message.data.type - 1]})\n ${JSON.stringify(message.data)}`);
|
||||
}
|
||||
}
|
||||
|
||||
this.log.i(`Player '${(await this.#profile.export())?.username}' (${this.#profile.getId()}) created hub socket`);
|
||||
async #init() {
|
||||
this.#log.source += this.#profile.getId().toString();
|
||||
|
||||
this.#socket.on('open', () => {
|
||||
this.log.d(`hello world`)
|
||||
});
|
||||
this.#socket.on('message', data => {
|
||||
this.log.d(data.toString());
|
||||
this.#log.i(`Player '${(await this.#profile.export())?.username}' (${this.#profile.getId()}) created hub socket`);
|
||||
|
||||
this.#socket.addEventListener('message', message => {
|
||||
try {
|
||||
|
||||
const dec = new TextDecoder();
|
||||
const str = dec.decode(message.data);
|
||||
const data = JSON.parse(str.substring(0, str.length - 1));
|
||||
|
||||
const parseResult = SignalRMessageSchema.safeParse(data);
|
||||
if (parseResult.success) this.#onMessage({
|
||||
kind: MessageKind.Data,
|
||||
data: parseResult.data as SignalRMessage
|
||||
});
|
||||
else {
|
||||
this.#onMessage({
|
||||
kind: MessageKind.Protocol
|
||||
});
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
this.#log.e(`Socket error: ${err}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#send(data: object) {
|
||||
this.#socket.send(`${JSON.stringify(data)}\u001e`);
|
||||
}
|
||||
|
||||
}
|
||||
16
src/socket/targets/SubscribeToPlayers.ts
Normal file
16
src/socket/targets/SubscribeToPlayers.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { SocketTarget } from "./targetbase.ts";
|
||||
|
||||
export class PlayerSocketSubscriptionTarget extends SocketTarget {
|
||||
|
||||
subscriptions: number[] = [];
|
||||
|
||||
setSubscriptions(subs: number[]) {
|
||||
this.subscriptions = subs;
|
||||
}
|
||||
|
||||
// deno-lint-ignore require-await
|
||||
override async exec(_args: (object | string | number | boolean)[]) {
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
20
src/socket/targets/targetbase.ts
Normal file
20
src/socket/targets/targetbase.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export class SocketTarget {
|
||||
|
||||
profileNotSetError = new Error("The profile on this target is not set.");
|
||||
|
||||
profileId: number | null = null;
|
||||
|
||||
setProfile(id: number) {
|
||||
this.profileId = id;
|
||||
}
|
||||
|
||||
profileIsSet() {
|
||||
return this.profileId !== null;
|
||||
}
|
||||
|
||||
// deno-lint-ignore require-await
|
||||
async exec(_args: (object | string | number | boolean)[]) {
|
||||
throw new Error("Execution for this target is not set.");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,22 +1,125 @@
|
||||
export enum MessageTypes {
|
||||
CancelInvocation,
|
||||
Close,
|
||||
Completion,
|
||||
import { z } from "zod";
|
||||
|
||||
export enum MessageKind {
|
||||
Protocol,
|
||||
Data
|
||||
}
|
||||
interface MessageBase {
|
||||
kind: MessageKind
|
||||
}
|
||||
interface DataMessage extends MessageBase {
|
||||
kind: MessageKind.Data,
|
||||
data: SignalRMessage
|
||||
}
|
||||
interface ProtocolMessage extends MessageBase {
|
||||
kind: MessageKind.Protocol
|
||||
}
|
||||
export type Message = ProtocolMessage | DataMessage;
|
||||
|
||||
export type SignalRMessage =
|
||||
| InvocationMessage
|
||||
| StreamItemMessage
|
||||
| CompletionMessage
|
||||
| PingMessage
|
||||
| CloseMessage;
|
||||
|
||||
export enum SignalMessageType {
|
||||
Handshake,
|
||||
Invocation,
|
||||
Ping,
|
||||
StreamInvocation,
|
||||
StreamItem,
|
||||
Ack
|
||||
Completion,
|
||||
StreamInvocation,
|
||||
CancelInvocation,
|
||||
Ping,
|
||||
Close
|
||||
}
|
||||
|
||||
export interface SignalRMessage {
|
||||
arguments: object[],
|
||||
error?: string,
|
||||
invocationId?: string,
|
||||
item?: object,
|
||||
nonblocking: boolean,
|
||||
result?: object,
|
||||
target: string,
|
||||
type: MessageTypes
|
||||
}
|
||||
interface BaseMessage {
|
||||
type: SignalMessageType;
|
||||
}
|
||||
|
||||
interface InvocationMessage extends BaseMessage {
|
||||
type: SignalMessageType.Invocation;
|
||||
target: string;
|
||||
arguments: unknown[];
|
||||
invocationId?: string;
|
||||
}
|
||||
|
||||
interface StreamItemMessage extends BaseMessage {
|
||||
type: SignalMessageType.StreamItem;
|
||||
invocationId: string;
|
||||
item: unknown;
|
||||
}
|
||||
|
||||
interface CompletionMessage extends BaseMessage {
|
||||
type: SignalMessageType.Completion;
|
||||
invocationId: string;
|
||||
result?: unknown;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface PingMessage extends BaseMessage {
|
||||
type: SignalMessageType.Ping;
|
||||
}
|
||||
|
||||
interface CloseMessage extends BaseMessage {
|
||||
type: SignalMessageType.Close;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const BaseMessageSchema = z.object({
|
||||
type: z.nativeEnum(SignalMessageType),
|
||||
});
|
||||
|
||||
const InvocationMessageSchema = BaseMessageSchema.extend({
|
||||
type: z.literal(SignalMessageType.Invocation),
|
||||
target: z.string(),
|
||||
arguments: z.array(z.unknown()),
|
||||
invocationId: z.string().optional(),
|
||||
});
|
||||
|
||||
const StreamItemMessageSchema = BaseMessageSchema.extend({
|
||||
type: z.literal(SignalMessageType.StreamItem),
|
||||
invocationId: z.string(),
|
||||
item: z.unknown(),
|
||||
});
|
||||
|
||||
const CompletionMessageSchema = BaseMessageSchema.extend({
|
||||
type: z.literal(SignalMessageType.Completion),
|
||||
invocationId: z.string(),
|
||||
result: z.unknown().optional(),
|
||||
error: z.string().optional(),
|
||||
});
|
||||
|
||||
const PingMessageSchema = BaseMessageSchema.extend({
|
||||
type: z.literal(SignalMessageType.Ping),
|
||||
});
|
||||
|
||||
const CloseMessageSchema = BaseMessageSchema.extend({
|
||||
type: z.literal(SignalMessageType.Close),
|
||||
error: z.string().optional(),
|
||||
});
|
||||
|
||||
export const SignalRMessageSchema = z.discriminatedUnion("type", [
|
||||
InvocationMessageSchema,
|
||||
StreamItemMessageSchema,
|
||||
CompletionMessageSchema,
|
||||
PingMessageSchema,
|
||||
CloseMessageSchema,
|
||||
]);
|
||||
|
||||
export enum TargetResultType {
|
||||
Success,
|
||||
Failure
|
||||
}
|
||||
interface TargetResultBase {
|
||||
type: TargetResultType
|
||||
}
|
||||
export interface TargetResultSuccess<T = unknown> extends TargetResultBase {
|
||||
type: TargetResultType.Success,
|
||||
data: T
|
||||
}
|
||||
export interface TargetResultFailure extends TargetResultBase {
|
||||
type: TargetResultType.Failure
|
||||
}
|
||||
export type TargetResult = TargetResultSuccess | TargetResultFailure;
|
||||
@@ -6,6 +6,7 @@ declare global {
|
||||
interface Locals {
|
||||
profile: Profile;
|
||||
user: User;
|
||||
token: string | undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user