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",
|
"@std/http": "jsr:@std/http@^1.0.13",
|
||||||
"@types/cookie-parser": "npm:@types/cookie-parser@^1.4.8",
|
"@types/cookie-parser": "npm:@types/cookie-parser@^1.4.8",
|
||||||
"@types/express": "npm:@types/express@^5.0.0",
|
"@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/validator": "npm:@types/validator@^13.12.2",
|
||||||
"@types/ws": "npm:@types/ws@^8.18.0",
|
"@types/ws": "npm:@types/ws@^8.18.0",
|
||||||
"cookie-parser": "npm:cookie-parser@^1.4.7",
|
"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.0.0",
|
||||||
"npm:@types/express@5": "5.0.0",
|
"npm:@types/express@5": "5.0.0",
|
||||||
"npm:@types/node@*": "22.5.4",
|
"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/validator@^13.12.2": "13.12.2",
|
||||||
"npm:@types/ws@^8.18.0": "8.18.0",
|
"npm:@types/ws@^8.18.0": "8.18.0",
|
||||||
"npm:chalk@^5.3.0": "5.3.0",
|
"npm:chalk@^5.3.0": "5.3.0",
|
||||||
@@ -228,6 +229,12 @@
|
|||||||
"undici-types@6.20.0"
|
"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": {
|
"@types/node@22.5.4": {
|
||||||
"integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==",
|
"integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
@@ -931,6 +938,7 @@
|
|||||||
"jsr:@std/http@^1.0.13",
|
"jsr:@std/http@^1.0.13",
|
||||||
"npm:@types/cookie-parser@^1.4.8",
|
"npm:@types/cookie-parser@^1.4.8",
|
||||||
"npm:@types/express@5",
|
"npm:@types/express@5",
|
||||||
|
"npm:@types/node@^22.13.14",
|
||||||
"npm:@types/validator@^13.12.2",
|
"npm:@types/validator@^13.12.2",
|
||||||
"npm:@types/ws@^8.18.0",
|
"npm:@types/ws@^8.18.0",
|
||||||
"npm:cookie-parser@^1.4.7",
|
"npm:cookie-parser@^1.4.7",
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { Config } from "./config.ts";
|
|||||||
import { AuthType, User, UserTokenFormat } from "./data/users.ts";
|
import { AuthType, User, UserTokenFormat } from "./data/users.ts";
|
||||||
import UnifiedProfile, { 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();
|
||||||
|
|
||||||
@@ -25,6 +24,11 @@ export function createRouter(path: string) {
|
|||||||
return router;
|
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) {
|
export function generateRandomString(length: number) {
|
||||||
const characters =
|
const characters =
|
||||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
@@ -132,14 +136,14 @@ export function getSrcIpDefault(rq: express.Request): string {
|
|||||||
|
|
||||||
return typeof rq.ip === "undefined" ? "(unknown source)" : rq.ip;
|
return typeof rq.ip === "undefined" ? "(unknown source)" : rq.ip;
|
||||||
}
|
}
|
||||||
export function getSrcIpDefaultRaw(rq: IncomingMessage) {
|
export function getSrcIpDefaultDeno(req: Request, info: Deno.ServeHandlerInfo<Deno.NetAddr>) {
|
||||||
const cfIp = rq.headers['cf-connecting-ip'];
|
const cfIp = req.headers.get('cf-connecting-ip');
|
||||||
if (cfIp) return cfIp;
|
if (cfIp) return cfIp;
|
||||||
|
|
||||||
const xrIp = rq.headers['x-real-ip'];
|
const xrIp = req.headers.get('x-real-ip');
|
||||||
if (xrIp) return xrIp;
|
if (xrIp) return xrIp;
|
||||||
|
|
||||||
return rq.socket.remoteAddress ? rq.socket.remoteAddress : "(unknown source)";
|
return info.remoteAddr.hostname;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function statusResponse(code: number) {
|
export function statusResponse(code: number) {
|
||||||
@@ -277,11 +281,12 @@ export async function Authentication(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const valid = ![ // used to contain more conditions, now is only 1
|
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);
|
].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 = UnifiedProfile.get(decodedToken.sub);
|
else if (decodedToken.typ == AuthType.Game) rs.locals.profile = UnifiedProfile.get(decodedToken.sub);
|
||||||
|
rs.locals.token = token;
|
||||||
|
|
||||||
nxt();
|
nxt();
|
||||||
} else {
|
} 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 type NoBody = Record<string | number | symbol, never>;
|
||||||
|
|
||||||
export * as APIUtils from "./apiutils.ts";
|
export * as APIUtils from "./apiutils.ts";
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ type WebConfiguration = {
|
|||||||
host: string;
|
host: string;
|
||||||
publichost: string;
|
publichost: string;
|
||||||
securepublichost: boolean;
|
securepublichost: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebRootConfiguration = {
|
||||||
|
api: WebConfiguration,
|
||||||
|
socket: WebConfiguration
|
||||||
};
|
};
|
||||||
|
|
||||||
type PublicConfiguration = {
|
type PublicConfiguration = {
|
||||||
@@ -52,7 +57,7 @@ type AuthConfiguration = {
|
|||||||
|
|
||||||
export type GalvanicConfiguration = {
|
export type GalvanicConfiguration = {
|
||||||
redis: RedisConfiguration;
|
redis: RedisConfiguration;
|
||||||
web: WebConfiguration;
|
web: WebRootConfiguration;
|
||||||
public: PublicConfiguration;
|
public: PublicConfiguration;
|
||||||
logging: LoggingConfiguration;
|
logging: LoggingConfiguration;
|
||||||
discord: DiscordConfiguration | null;
|
discord: DiscordConfiguration | null;
|
||||||
@@ -68,11 +73,19 @@ export const defaultConfig: GalvanicConfiguration = {
|
|||||||
db: 0,
|
db: 0,
|
||||||
},
|
},
|
||||||
web: {
|
web: {
|
||||||
port: 3000,
|
api: {
|
||||||
|
port: 13370,
|
||||||
host: "127.0.0.1",
|
host: "127.0.0.1",
|
||||||
publichost: "127.0.0.1:3000",
|
publichost: "127.0.0.1:13370",
|
||||||
securepublichost: false,
|
securepublichost: false,
|
||||||
},
|
},
|
||||||
|
socket: {
|
||||||
|
port: 13371,
|
||||||
|
host: "127.0.0.1",
|
||||||
|
publichost: "127.0.0.1:13371",
|
||||||
|
securepublichost: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
public: {
|
public: {
|
||||||
serverName: "Galvanic Corrosion",
|
serverName: "Galvanic Corrosion",
|
||||||
serverId: "galvanic-corrosion-default",
|
serverId: "galvanic-corrosion-default",
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ export type PublicConfig = {
|
|||||||
ConfigTable: Config[];
|
ConfigTable: Config[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plain public config, NOT GameConfigs
|
||||||
|
*/
|
||||||
export function getConfig() {
|
export function getConfig() {
|
||||||
const c = Config.getConfig();
|
const c = Config.getConfig();
|
||||||
if (typeof c == "undefined") return null;
|
if (typeof c == "undefined") return null;
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class PlayerPresence {
|
|||||||
const PlayerStatusVisibilityEnum = z.nativeEnum(PlayerStatusVisibility);
|
const PlayerStatusVisibilityEnum = z.nativeEnum(PlayerStatusVisibility);
|
||||||
type PlayerStatusVisibilityEnum = z.infer<typeof PlayerStatusVisibilityEnum>;
|
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;
|
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 * as JsonWebToken from "@gz/jwt";
|
||||||
import { TokenBaseFormat } from "../apiutils.ts";
|
import { TokenBaseFormat } from "../apiutils.ts";
|
||||||
import { DeviceClass, RoomInstance, VRMovementMode } from "./live/types.ts";
|
import { DeviceClass, RoomInstance, VRMovementMode } from "./live/types.ts";
|
||||||
import { Setting } from "./profiletypes.ts";
|
|
||||||
import { SettingKey } from "./content/settings.ts";
|
import { SettingKey } from "./content/settings.ts";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { SignalRSocketHandler } from "../socket/socket.ts";
|
import { SignalRSocketHandler } from "../socket/socket.ts";
|
||||||
|
import { ProfileSettingsManager } from "./profile/settings.ts";
|
||||||
|
import { ProfileProgressionManager } from "./profile/progression.ts";
|
||||||
|
|
||||||
const config = Config.getConfig();
|
const config = Config.getConfig();
|
||||||
|
|
||||||
@@ -140,8 +141,15 @@ class Profile {
|
|||||||
|
|
||||||
#socket: SignalRSocketHandler | null = null;
|
#socket: SignalRSocketHandler | null = null;
|
||||||
|
|
||||||
|
Settings = new ProfileSettingsManager();
|
||||||
|
Progression = new ProfileProgressionManager();
|
||||||
|
|
||||||
constructor(id: number) {
|
constructor(id: number) {
|
||||||
this.#id = id;
|
this.#id = id;
|
||||||
|
|
||||||
|
// Set IDs for all content managers
|
||||||
|
this.Settings.setProfile(this.#id);
|
||||||
|
this.Progression.setProfile(this.#id);
|
||||||
}
|
}
|
||||||
|
|
||||||
setInstance(instance: RoomInstance | null) {
|
setInstance(instance: RoomInstance | null) {
|
||||||
@@ -164,29 +172,6 @@ class Profile {
|
|||||||
return await Profile.getExportAccount(this.#id);
|
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) {
|
async setKnownDeviceClass(deviceClass: string | number) {
|
||||||
await Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.#id.toString(), Redis.KeyGroups.Profiles.DeviceClass), deviceClass);
|
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) {
|
async setVRMovementMode(movementMode: string | number) {
|
||||||
return await this.setSetting(SettingKey.VRMovementMode, movementMode.toString());
|
return await this.Settings.setSetting(SettingKey.VRMovementMode, movementMode.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
async getVRMovementMode() {
|
async getVRMovementMode() {
|
||||||
const data = await this.getSetting(SettingKey.VRMovementMode);
|
const data = await this.Settings.getSetting(SettingKey.VRMovementMode);
|
||||||
|
|
||||||
const VRMovementModeEnum = z.nativeEnum(VRMovementMode);
|
const VRMovementModeEnum = z.nativeEnum(VRMovementMode);
|
||||||
type VRMovementModeEnum = z.infer<typeof VRMovementModeEnum>
|
type VRMovementModeEnum = z.infer<typeof VRMovementModeEnum>
|
||||||
@@ -232,7 +217,7 @@ class Profile {
|
|||||||
|
|
||||||
async getToken() {
|
async getToken() {
|
||||||
const payload: ProfileTokenFormat = {
|
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,
|
sub: this.#id,
|
||||||
role: (await this.getIsOperator()) ? 'developer' : 'user',
|
role: (await this.getIsOperator()) ? 'developer' : 'user',
|
||||||
exp: Math.round(Date.now() / 1000) + Math.round(config.auth.timeout * 60 * 60),
|
exp: Math.round(Date.now() / 1000) + Math.round(config.auth.timeout * 60 * 60),
|
||||||
@@ -256,6 +241,14 @@ class UnifiedProfileBase {
|
|||||||
return profile;
|
return profile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async create(options: ProfileInitOptions) {
|
||||||
|
return await Profile.init(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async exists(id: number) {
|
||||||
|
return await Profile.exists(id);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const UnifiedProfile = new UnifiedProfileBase();
|
const UnifiedProfile = new UnifiedProfileBase();
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
export interface Setting {
|
|
||||||
Key: string;
|
|
||||||
Value: string;
|
|
||||||
}
|
|
||||||
@@ -9,10 +9,6 @@ type UserInitOptions = {
|
|||||||
pubkey: string;
|
pubkey: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type UserCreatedObj = {
|
|
||||||
user: User;
|
|
||||||
};
|
|
||||||
|
|
||||||
export enum AuthType {
|
export enum AuthType {
|
||||||
Game,
|
Game,
|
||||||
Web,
|
Web,
|
||||||
@@ -78,7 +74,7 @@ export class User {
|
|||||||
|
|
||||||
async getToken() {
|
async getToken() {
|
||||||
const payload: UserTokenFormat = {
|
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,
|
sub: this.#client_id,
|
||||||
exp: Math.round(Date.now() / 1000) + (config.auth.timeout * 60 * 60),
|
exp: Math.round(Date.now() / 1000) + (config.auth.timeout * 60 * 60),
|
||||||
typ: AuthType.Web
|
typ: AuthType.Web
|
||||||
|
|||||||
101
src/main.ts
101
src/main.ts
@@ -6,12 +6,10 @@ import { Discord } from "./discord.ts";
|
|||||||
import { generateRandomString } from "./apiutils.ts";
|
import { generateRandomString } from "./apiutils.ts";
|
||||||
// @ts-types = "npm:@types/express"
|
// @ts-types = "npm:@types/express"
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import WebSocket, { WebSocketServer } from "ws";
|
|
||||||
import { decode } from "@gz/jwt";
|
import { decode } from "@gz/jwt";
|
||||||
import UnifiedProfile, { 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 { SignalRSocketHandler } from "./socket/socket.ts";
|
import { SignalRSocketHandler } from "./socket/socket.ts";
|
||||||
import { IncomingMessage } from "node:http";
|
|
||||||
|
|
||||||
const instanceId = generateRandomString(64);
|
const instanceId = generateRandomString(64);
|
||||||
|
|
||||||
@@ -44,8 +42,8 @@ try {
|
|||||||
Deno.exit(1);
|
Deno.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const port = config.web.port;
|
const port = config.web.api.port;
|
||||||
const host = config.web.host;
|
const host = config.web.api.host;
|
||||||
|
|
||||||
log.n(`Starting HTTP server on http://${host}:${port}`);
|
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({
|
rs.json({
|
||||||
name: config.public.serverName,
|
name: config.public.serverName,
|
||||||
id: config.public.serverId,
|
id: config.public.serverId,
|
||||||
@@ -102,12 +100,19 @@ try {
|
|||||||
* Galvanic WebSocket Server
|
* Galvanic WebSocket Server
|
||||||
*/
|
*/
|
||||||
|
|
||||||
type AuthResult = {
|
type AuthResultBase = {
|
||||||
token?: ProfileTokenFormat,
|
|
||||||
valid: boolean
|
valid: boolean
|
||||||
}
|
}
|
||||||
const authenticate = async (req: IncomingMessage) => {
|
interface SuccessfulAuth extends AuthResultBase {
|
||||||
const authHeader = req.headers.authorization;
|
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;
|
if (!authHeader) return { valid: false } as AuthResult;
|
||||||
|
|
||||||
const token = authHeader.split(" ")[1];
|
const token = authHeader.split(" ")[1];
|
||||||
@@ -119,12 +124,25 @@ try {
|
|||||||
else return { token: decodedToken, valid: true } as AuthResult;
|
else return { token: decodedToken, valid: true } as AuthResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
const wss = new WebSocketServer({ noServer: true, path: "/notify/hub/v1" });
|
const abort = new AbortController();
|
||||||
wss.on('connection', (socket: WebSocket, req: IncomingMessage) => {
|
|
||||||
if (!req.token) {
|
// Galvanic WebSocket
|
||||||
socket.close();
|
Deno.serve({port: config.web.socket.port, hostname: config.web.socket.host, signal: abort.signal, onListen: addr => {
|
||||||
return;
|
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.
|
// ID is given as "/notify/hub/v1?&id=pprhdSzJn" by the client.
|
||||||
let handoff: SocketHandoff | undefined;
|
let handoff: SocketHandoff | undefined;
|
||||||
@@ -136,11 +154,21 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (handoff) handoff.complete();
|
if (handoff) handoff.complete();
|
||||||
new SignalRSocketHandler(socket, UnifiedProfile.get(req.token.sub));
|
|
||||||
|
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.port, config.web.host, () => {
|
const http = app.listen(config.web.api.port, config.web.api.host, async () => {
|
||||||
log.n(`Listening on http://${config.web.host}:${config.web.port}`);
|
log.n(`Web listening on http://${config.web.api.host}:${config.web.api.port}`);
|
||||||
|
|
||||||
let shuttingDown = false;
|
let shuttingDown = false;
|
||||||
Deno.addSignalListener("SIGINT", () => {
|
Deno.addSignalListener("SIGINT", () => {
|
||||||
@@ -148,40 +176,27 @@ try {
|
|||||||
shuttingDown = true;
|
shuttingDown = true;
|
||||||
log.i(`Shutting down`);
|
log.i(`Shutting down`);
|
||||||
|
|
||||||
|
abort.abort(); // websockets
|
||||||
http.close();
|
http.close();
|
||||||
http.closeAllConnections();
|
http.closeAllConnections();
|
||||||
});
|
});
|
||||||
Deno.addSignalListener("SIGINT", () => {
|
Deno.addSignalListener("SIGINT", () => {
|
||||||
for (const handoff of SocketHandoff.all()) handoff.complete();
|
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('error', err => {
|
||||||
/*http.on('upgrade', async (req, socket, head) => {
|
log.e(`HTTP error: ${err.stack}`);
|
||||||
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();
|
|
||||||
}
|
|
||||||
});*/
|
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.e(`Cannot start: Network could not be initalized. ${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 ConfigRoute } from "./api/config.ts";
|
||||||
import { route as GameConfig } from "./api/gameconfigs.ts";
|
import { route as GameConfig } from "./api/gameconfigs.ts";
|
||||||
import { route as PlayerReportingRoute } from "./api/PlayerReporting.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";
|
import { APIUtils } from "../apiutils.ts";
|
||||||
|
|
||||||
export const route = APIUtils.createRouter("/api");
|
export const route = APIUtils.createRouter("/api");
|
||||||
@@ -10,3 +12,5 @@ route.router.use(VersionCheckRoute.path, VersionCheckRoute.router);
|
|||||||
route.router.use(ConfigRoute.path, ConfigRoute.router);
|
route.router.use(ConfigRoute.path, ConfigRoute.router);
|
||||||
route.router.use(GameConfig.path, GameConfig.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);
|
if (config == null) rs.sendStatus(500);
|
||||||
else rs.json(config);
|
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 { APIUtils } from "../../apiutils.ts";
|
||||||
|
import { AuthType } from "../../data/users.ts";
|
||||||
|
|
||||||
export const route = APIUtils.createRouter("/cachedlogin");
|
export const route = APIUtils.createRouter("/cachedlogin");
|
||||||
|
|
||||||
route.router.get("/forplatformid/:platformtype/:platformid",
|
route.router.get("/forplatformid/:platformtype/:platformid",
|
||||||
|
|
||||||
APIUtils.Authentication,
|
APIUtils.Authentication,
|
||||||
|
APIUtils.AuthenticationType(AuthType.Web),
|
||||||
|
|
||||||
async (_rq, rs) => {
|
async (_rq, rs) => {
|
||||||
const profiles = await rs.locals.user.exportAssociatedProfiles();
|
const profiles = await rs.locals.user.exportAssociatedProfiles();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ 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";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { AuthType } from "../../data/users.ts";
|
||||||
|
|
||||||
const config = Config.getConfig();
|
const config = Config.getConfig();
|
||||||
|
|
||||||
@@ -75,6 +76,7 @@ interface TokenResponseBody {
|
|||||||
route.router.post("/token",
|
route.router.post("/token",
|
||||||
|
|
||||||
APIUtils.Authentication,
|
APIUtils.Authentication,
|
||||||
|
APIUtils.AuthenticationType(AuthType.Web),
|
||||||
express.urlencoded({ extended: true }),
|
express.urlencoded({ extended: true }),
|
||||||
APIUtils.validateRequestBody<AuthBodyBase>(TokenRequestBodySchema),
|
APIUtils.validateRequestBody<AuthBodyBase>(TokenRequestBodySchema),
|
||||||
|
|
||||||
@@ -149,6 +151,9 @@ route.router.post("/token",
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const details = await profile.export();
|
||||||
|
log.i(`Player ${details?.username} "${details?.displayName}" (${profile.getId()}) logged in`);
|
||||||
|
|
||||||
const token = await profile.getToken();
|
const token = await profile.getToken();
|
||||||
rs.json({
|
rs.json({
|
||||||
access_token: token,
|
access_token: token,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { APIUtils, NoBody } from "../../apiutils.ts";
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import Matchmaking from "../../data/live/base.ts";
|
import Matchmaking from "../../data/live/base.ts";
|
||||||
import Presence from "../../data/live/presence.ts";
|
import Presence from "../../data/live/presence.ts";
|
||||||
|
import { AuthType } from "../../data/users.ts";
|
||||||
|
|
||||||
export const route = APIUtils.createRouter('/player');
|
export const route = APIUtils.createRouter('/player');
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ const LoginSchema = z.object({
|
|||||||
route.router.post('/login',
|
route.router.post('/login',
|
||||||
|
|
||||||
APIUtils.Authentication,
|
APIUtils.Authentication,
|
||||||
|
APIUtils.AuthenticationType(AuthType.Game),
|
||||||
express.urlencoded({extended: true}),
|
express.urlencoded({extended: true}),
|
||||||
APIUtils.validateRequestBody(LoginSchema),
|
APIUtils.validateRequestBody(LoginSchema),
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { APIUtils } from "../apiutils.ts";
|
|||||||
import { Config } from "../config.ts";
|
import { Config } from "../config.ts";
|
||||||
|
|
||||||
const config = Config.getConfig() as Config.GalvanicConfiguration;
|
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");
|
export const route = APIUtils.createRouter("/ns");
|
||||||
|
|
||||||
@@ -21,17 +21,17 @@ type NameserverHosts = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const nameserver: NameserverHosts = {
|
const nameserver: NameserverHosts = {
|
||||||
Auth: `${protocol}://${config.web.publichost}/auth`,
|
Auth: `${protocol}://${config.web.api.publichost}/auth`,
|
||||||
API: `${protocol}://${config.web.publichost}`,
|
API: `${protocol}://${config.web.api.publichost}`,
|
||||||
WWW: `${protocol}://${config.web.publichost}`,
|
WWW: `${protocol}://${config.web.api.publichost}`,
|
||||||
Notifications: `${protocol}://${config.web.publichost}/notify`,
|
Notifications: `${protocol}://${config.web.api.publichost}/notify`,
|
||||||
Images: `${protocol}://${config.web.publichost}/img`,
|
Images: `${protocol}://${config.web.api.publichost}/img`,
|
||||||
CDN: `${protocol}://${config.web.publichost}/cdn`,
|
CDN: `${protocol}://${config.web.api.publichost}/cdn`,
|
||||||
Commerce: `${protocol}://${config.web.publichost}/commerce`,
|
Commerce: `${protocol}://${config.web.api.publichost}/commerce`,
|
||||||
Matchmaking: `${protocol}://${config.web.publichost}/match`,
|
Matchmaking: `${protocol}://${config.web.api.publichost}/match`,
|
||||||
Storage: `${protocol}://${config.web.publichost}/storage`,
|
Storage: `${protocol}://${config.web.api.publichost}/storage`,
|
||||||
Chat: `${protocol}://${config.web.publichost}/chat`,
|
Chat: `${protocol}://${config.web.api.publichost}/chat`,
|
||||||
Leaderboard: `${protocol}://${config.web.publichost}/leaderboard`,
|
Leaderboard: `${protocol}://${config.web.api.publichost}/leaderboard`,
|
||||||
};
|
};
|
||||||
|
|
||||||
route.router.get("*", (_rq, rs) => {
|
route.router.get("*", (_rq, rs) => {
|
||||||
|
|||||||
@@ -1,17 +1,27 @@
|
|||||||
import { APIUtils } from "../apiutils.ts";
|
import { APIUtils } from "../apiutils.ts";
|
||||||
|
import { Config } from "../config.ts";
|
||||||
|
import { AuthType } from "../data/users.ts";
|
||||||
import { SocketHandoff } from "./handoff.ts";
|
import { SocketHandoff } from "./handoff.ts";
|
||||||
|
import express from "express";
|
||||||
|
|
||||||
|
const config = Config.getConfig();
|
||||||
|
|
||||||
export const route = APIUtils.createRouter('/notify');
|
export const route = APIUtils.createRouter('/notify');
|
||||||
|
|
||||||
route.router.post('/hub/v1/negotiate',
|
route.router.post('/hub/v1/negotiate',
|
||||||
|
|
||||||
APIUtils.Authentication,
|
APIUtils.Authentication,
|
||||||
|
APIUtils.AuthenticationType(AuthType.Game),
|
||||||
|
express.urlencoded({ extended: true }),
|
||||||
|
APIUtils.logBody,
|
||||||
|
|
||||||
(_rq, rs) => {
|
(_rq, rs) => {
|
||||||
const handoff = new SocketHandoff();
|
const handoff = new SocketHandoff();
|
||||||
rs.json({
|
rs.json({
|
||||||
connectionId: handoff.id,
|
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 { Profile } from "../data/profiles.ts";
|
||||||
import Logging from "@proxnet/undead-logging";
|
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 {
|
export class SignalRSocketHandler {
|
||||||
|
|
||||||
log: Logging = new Logging("SignalMock-");
|
#log: Logging = new Logging("SignalMock-");
|
||||||
|
|
||||||
#socket: WebSocket;
|
#socket: WebSocket;
|
||||||
#profile: Profile;
|
#profile: Profile;
|
||||||
|
|
||||||
|
#Targets: Map<string, SocketTarget> = new Map();
|
||||||
|
|
||||||
constructor(socket: WebSocket, player: Profile) {
|
constructor(socket: WebSocket, player: Profile) {
|
||||||
|
|
||||||
this.#socket = socket;
|
this.#socket = socket;
|
||||||
this.#initLogSource();
|
|
||||||
|
|
||||||
this.#profile = player;
|
this.#profile = player;
|
||||||
|
|
||||||
|
this.#init();
|
||||||
|
|
||||||
player.setSocketHandler(this);
|
player.setSocketHandler(this);
|
||||||
|
|
||||||
Deno.addSignalListener('SIGINT', this.destroy);
|
this.#Targets.set('SubscribeToPlayers', new PlayerSocketSubscriptionTarget());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
async #dispatchTarget<T = unknown>(target: string, args: object[]): Promise<TargetResult> {
|
||||||
this.#socket.close();
|
const targetExec = this.#Targets.get(target);
|
||||||
Deno.removeSignalListener('SIGINT', this.destroy);
|
if (!targetExec) return { type: TargetResultType.Failure } as TargetResultFailure;
|
||||||
|
else return { type: TargetResultType.Success, data: await targetExec.exec(args) } as TargetResultSuccess<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
async #initLogSource() {
|
#onMessage(message: Message) {
|
||||||
this.log.source += this.#profile.getId().toString();
|
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.i(`Player '${(await this.#profile.export())?.username}' (${this.#profile.getId()}) created hub socket`);
|
||||||
this.log.d(`hello world`)
|
|
||||||
|
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
|
||||||
});
|
});
|
||||||
this.#socket.on('message', data => {
|
else {
|
||||||
this.log.d(data.toString());
|
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 {
|
import { z } from "zod";
|
||||||
CancelInvocation,
|
|
||||||
Close,
|
export enum MessageKind {
|
||||||
Completion,
|
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,
|
Handshake,
|
||||||
Invocation,
|
Invocation,
|
||||||
Ping,
|
|
||||||
StreamInvocation,
|
|
||||||
StreamItem,
|
StreamItem,
|
||||||
Ack
|
Completion,
|
||||||
|
StreamInvocation,
|
||||||
|
CancelInvocation,
|
||||||
|
Ping,
|
||||||
|
Close
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SignalRMessage {
|
interface BaseMessage {
|
||||||
arguments: object[],
|
type: SignalMessageType;
|
||||||
error?: string,
|
|
||||||
invocationId?: string,
|
|
||||||
item?: object,
|
|
||||||
nonblocking: boolean,
|
|
||||||
result?: object,
|
|
||||||
target: string,
|
|
||||||
type: MessageTypes
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
interface Locals {
|
||||||
profile: Profile;
|
profile: Profile;
|
||||||
user: User;
|
user: User;
|
||||||
|
token: string | undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user