Embed base images into binary

Include resource directory
Ran `deno fmt` with 4 space indent, that changed every file (!!!!!)
various changes
This commit is contained in:
2025-03-24 19:11:36 -04:00
parent 2207e389c9
commit 49c481aa0e
61 changed files with 2369 additions and 2031 deletions

2
.gitignore vendored
View File

@@ -132,3 +132,5 @@ dist
# galvanic corrosion # galvanic corrosion
build/ build/
config.json config.json
.vscode

View File

@@ -1,17 +1,23 @@
# Galvanic Corrosion # Galvanic Corrosion
delectable yum yum delectable yum yum
Rec Room custom server for communities. Fast runtime and easy setup.<br>Built for Rec Room build 526 (Timestamp: 637098805133024772, Version: 20191120) Rec Room custom server for communities. Fast runtime and easy setup.<br>Built
for Rec Room build 526 (Timestamp: 637098805133024772, Version: 20191120)
<img src="galv4.jpg" alt="drawing" width="200"/> <img src="galv4.jpg" alt="drawing" width="200"/>
## Compiling Galvanic Corrosion ## Compiling Galvanic Corrosion
* Install [Deno 2.x](https://docs.deno.com/runtime/getting_started/installation/)
* Configure project - Install
- Clone this repo [Deno 2.x](https://docs.deno.com/runtime/getting_started/installation/)
- Install dependencies with `deno i` - Configure project
- Compile server with `deno run cross-compile` - Clone this repo
- Install dependencies with `deno i`
- Compile server with `deno run cross-compile`
## Client Patches ## Client Patches
You can configure some client patches from the server. See the IL2CPP universal patch for a list of patch IDs.
You can configure some client patches from the server. See the IL2CPP universal
patch for a list of patch IDs.
<br>Place desired patch ID strings into the config `public.patches`. <br>Place desired patch ID strings into the config `public.patches`.

View File

@@ -1,27 +1,28 @@
{ {
"tasks": { "tasks": {
"compile-win": "deno compile --target x86_64-pc-windows-msvc -o build/GalvanicCorrosion.exe -A src/main.ts", "compile-win": "deno compile --include res --include src --target x86_64-pc-windows-msvc -o build/GalvanicCorrosion.exe -A src/main.ts",
"compile-linux": "deno compile --target x86_64-unknown-linux-gnu -o build/GalvanicCorrosion -A src/main.ts", "compile-linux": "deno compile --include res --include src --target x86_64-unknown-linux-gnu -o build/GalvanicCorrosion -A src/main.ts",
"cross-compile": "deno run compile-win && deno run compile-linux", "cross-compile": "deno run compile-win && deno run compile-linux",
"dev": "deno run -A src/main.ts --dev" "dev": "deno run -A src/main.ts --dev"
}, },
"imports": { "imports": {
"@gz/jwt": "jsr:@gz/jwt@^0.1.0", "@gz/jwt": "jsr:@gz/jwt@^0.1.0",
"@proxnet/undead-logging": "jsr:@proxnet/undead-logging@^1.2.0", "@proxnet/undead-logging": "jsr:@proxnet/undead-logging@^1.2.0",
"@std/assert": "jsr:@std/assert@1", "@std/assert": "jsr:@std/assert@1",
"@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/validator": "npm:@types/validator@^13.12.2", "@types/validator": "npm:@types/validator@^13.12.2",
"cookie-parser": "npm:cookie-parser@^1.4.7", "cookie-parser": "npm:cookie-parser@^1.4.7",
"discord.js": "npm:discord.js@^14.16.3", "discord.js": "npm:discord.js@^14.16.3",
"express": "npm:express@^4.21.2", "express": "npm:express@^4.21.2",
"ioredis": "npm:ioredis@^5.5.0", "ioredis": "npm:ioredis@^5.5.0",
"validator": "npm:validator@^13.12.0", "validator": "npm:validator@^13.12.0",
"bcrypt": "https://deno.land/x/bcrypt@v0.3.0/mod.ts" "bcrypt": "https://deno.land/x/bcrypt@v0.3.0/mod.ts"
}, },
"compilerOptions": { "files": [],
"types": [ "compilerOptions": {
"./src/types/express.ts" "types": [
] "./src/types/express.ts"
} ]
}
} }

36
deno.lock generated
View File

@@ -8,6 +8,7 @@
"jsr:@std/crypto@^1.0.3": "1.0.3", "jsr:@std/crypto@^1.0.3": "1.0.3",
"jsr:@std/internal@^1.0.5": "1.0.5", "jsr:@std/internal@^1.0.5": "1.0.5",
"jsr:@std/uuid@*": "1.0.4", "jsr:@std/uuid@*": "1.0.4",
"npm:@imagemagick/magick-wasm@0.0.31": "0.0.31",
"npm:@types/cookie-parser@*": "1.4.8_@types+express@5.0.0", "npm:@types/cookie-parser@*": "1.4.8_@types+express@5.0.0",
"npm:@types/cookie-parser@^1.4.8": "1.4.8_@types+express@5.0.0", "npm:@types/cookie-parser@^1.4.8": "1.4.8_@types+express@5.0.0",
"npm:@types/express@*": "5.0.0", "npm:@types/express@*": "5.0.0",
@@ -110,6 +111,9 @@
"ws" "ws"
] ]
}, },
"@imagemagick/magick-wasm@0.0.31": {
"integrity": "sha512-QNivAUxSaItuiY8ziI/vRy6TtoecD7TOsD1LGZCG3wv8lfbdGbIj2QiJk0FlGkGwAVR966NlD3mkxPNvQrvq0w=="
},
"@ioredis/commands@1.2.0": { "@ioredis/commands@1.2.0": {
"integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg=="
}, },
@@ -708,6 +712,9 @@
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==" "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="
} }
}, },
"redirects": {
"https://deno.land/x/imagemagick_deno/mod.ts": "https://deno.land/x/imagemagick_deno@0.0.31/mod.ts"
},
"remote": { "remote": {
"https://deno.land/std@0.140.0/_util/assert.ts": "e94f2eb37cebd7f199952e242c77654e43333c1ac4c5c700e929ea3aa5489f74", "https://deno.land/std@0.140.0/_util/assert.ts": "e94f2eb37cebd7f199952e242c77654e43333c1ac4c5c700e929ea3aa5489f74",
"https://deno.land/std@0.140.0/_util/os.ts": "3b4c6e27febd119d36a416d7a97bd3b0251b77c88942c8f16ee5953ea13e2e49", "https://deno.land/std@0.140.0/_util/os.ts": "3b4c6e27febd119d36a416d7a97bd3b0251b77c88942c8f16ee5953ea13e2e49",
@@ -808,6 +815,35 @@
"https://deno.land/x/emit@0.25.0/_utils.ts": "98412edc7aa29e77d592b54fbad00bdec1b05d0c25eb772a5f8edc9813e08d88", "https://deno.land/x/emit@0.25.0/_utils.ts": "98412edc7aa29e77d592b54fbad00bdec1b05d0c25eb772a5f8edc9813e08d88",
"https://deno.land/x/emit@0.25.0/emit.generated.js": "0728e0cd293b930db2532f8cb5087fdb77aee1f30a059207533780f40250fd6a", "https://deno.land/x/emit@0.25.0/emit.generated.js": "0728e0cd293b930db2532f8cb5087fdb77aee1f30a059207533780f40250fd6a",
"https://deno.land/x/emit@0.25.0/mod.ts": "66ef8ddaedcfca033eeee851379af59ed3f0e0aa6e025e7cdd24e4e158d874f3", "https://deno.land/x/emit@0.25.0/mod.ts": "66ef8ddaedcfca033eeee851379af59ed3f0e0aa6e025e7cdd24e4e158d874f3",
"https://deno.land/x/imagemagick_deno@0.0.31/mod.ts": "124d7f045429f6e6c486b86e72d025410d09576bc0d8075e69f97118a1a33413",
"https://deno.land/x/imagescript@1.3.0/ImageScript.js": "cf90773c966031edd781ed176c598f7ed495e7694cd9b86c986d2d97f783cca0",
"https://deno.land/x/imagescript@1.3.0/mod.ts": "18a6cb83c55e690c873505f6fe867364c678afb64934fe7aef593a6b92f79995",
"https://deno.land/x/imagescript@1.3.0/png/src/crc.mjs": "5cf50de181d61dd00e66a240d811018ba5070afa8bba302f393604404604de84",
"https://deno.land/x/imagescript@1.3.0/png/src/mem.mjs": "4968d400dae069b4bf0ef4767c1802fd2cc7d15d90eda4cfadf5b4cd19b96c6d",
"https://deno.land/x/imagescript@1.3.0/png/src/png.mjs": "96ef0ceff1b5a6cd9304749e5f187b4ab238509fb5f9a8be8ee934240271ed8d",
"https://deno.land/x/imagescript@1.3.0/png/src/zlib.mjs": "9867dc3fab1d31b664f9344b0d7e977f493d9c912a76c760d012ed2b89f7061c",
"https://deno.land/x/imagescript@1.3.0/utils/buffer.js": "952cb1beb8827e50a493a5d1f29a4845e8c648789406d389dd51f51205ba02d8",
"https://deno.land/x/imagescript@1.3.0/utils/crc32.js": "573d6222b3605890714ebc374e687ec2aa3e9a949223ea199483e47ca4864f7d",
"https://deno.land/x/imagescript@1.3.0/utils/png.js": "fbed9117e0a70602645d70df9c103ff6e79c03e987bd5c1685dcb4200729b6de",
"https://deno.land/x/imagescript@1.3.0/utils/wasm/font.js": "9e75d842608c057045698d6a7cdf5ffd27241b5cdea0391c89a1917b31294524",
"https://deno.land/x/imagescript@1.3.0/utils/wasm/gif.js": "8b86f7b96486bb8ff50fbc7c7487f86cb5cef85e6acd71e1def78a1aa2f12e4f",
"https://deno.land/x/imagescript@1.3.0/utils/wasm/jpeg.js": "75295e2fcf96b4f7bb894b3844fdaa8140d63169d28b466b5d5be89d59a7b6e6",
"https://deno.land/x/imagescript@1.3.0/utils/wasm/png.js": "0659536a8dd8f892c8346e268b2754b4414fad0ec1e9794dfcde1ba1c804ee02",
"https://deno.land/x/imagescript@1.3.0/utils/wasm/svg.js": "f5c8a9d1977b51a7c07549ceb6bbbaca9497321a193f28b3dc229a42d91bcf14",
"https://deno.land/x/imagescript@1.3.0/utils/wasm/tiff.js": "c2d7bdaef094df25aae1752e75167f485e89275d76a1379e39d8949580b7af4f",
"https://deno.land/x/imagescript@1.3.0/utils/wasm/zlib.js": "749875f83abffe24d3b977475a0cbd5f9b52bee1fbdbef61ec183cbfc17805f6",
"https://deno.land/x/imagescript@1.3.0/v2/framebuffer.mjs": "add44ff184636659714b3c6d4b896f628545451abffbc30b5bcc2e8d9a73d012",
"https://deno.land/x/imagescript@1.3.0/v2/ops/blur.mjs": "80716f1ffab8a2aeb54a036f583bf51a2b9dd37e005adc000add803df8e8a12f",
"https://deno.land/x/imagescript@1.3.0/v2/ops/color.mjs": "5e72cdcbf97dc939a2795223f01e3cb0544c0c56b03ea2aa026050df58348814",
"https://deno.land/x/imagescript@1.3.0/v2/ops/crop.mjs": "69431fa6f687fd9f0c31eff0ec27d7ac925275005e53a37f0c3fab4cc4d9a9ea",
"https://deno.land/x/imagescript@1.3.0/v2/ops/fill.mjs": "cf1b9488314753fbc9ebf03410ac74c2a34ea5a69fb6892cd6e8366cd1930d93",
"https://deno.land/x/imagescript@1.3.0/v2/ops/flip.mjs": "825a34a66567dcf15e76a719f1bf2f66fb106503cd69942292b1b0ae05c5718e",
"https://deno.land/x/imagescript@1.3.0/v2/ops/index.mjs": "423ba687119be2bba8cec72890577d3afa3621b6b8108912242fe937a183f2aa",
"https://deno.land/x/imagescript@1.3.0/v2/ops/iterator.mjs": "c2adf3d90ce00719a02c48c97634574176a3501ff026676259bd71aa8f5d69b9",
"https://deno.land/x/imagescript@1.3.0/v2/ops/overlay.mjs": "7e6e2c2ffd25006d52597ab8babc5f8f503d388a3fdf2fbc0eaea02799a020c9",
"https://deno.land/x/imagescript@1.3.0/v2/ops/resize.mjs": "814e78ebce8eaf8f1f918688db7b52a141405e06a36ed4b25d04413d69e7d17b",
"https://deno.land/x/imagescript@1.3.0/v2/ops/rotate.mjs": "a1b65616717bd2eed8db406affea3263b4674dada46b56441ef38167a187455d",
"https://deno.land/x/imagescript@1.3.0/v2/util/mem.mjs": "4968d400dae069b4bf0ef4767c1802fd2cc7d15d90eda4cfadf5b4cd19b96c6d",
"https://deno.land/x/leaf@v1.0.4/constants.ts": "2b18c5be5a57cea4d3d6298d7c4c636e5db821c580c3197f9c9bcab65f8c3bf0", "https://deno.land/x/leaf@v1.0.4/constants.ts": "2b18c5be5a57cea4d3d6298d7c4c636e5db821c580c3197f9c9bcab65f8c3bf0",
"https://deno.land/x/leaf@v1.0.4/functions/getFileInMem.ts": "cec6c3c6add22c0c3316d8301994ab583feac5c3052df3072ad12976ea2aeec4", "https://deno.land/x/leaf@v1.0.4/functions/getFileInMem.ts": "cec6c3c6add22c0c3316d8301994ab583feac5c3052df3072ad12976ea2aeec4",
"https://deno.land/x/leaf@v1.0.4/functions/getFilePath.ts": "80ce141c1bd9735d3b7961b6ec8736070475c296c342be6bb4e189483f020801", "https://deno.land/x/leaf@v1.0.4/functions/getFilePath.ts": "80ce141c1bd9735d3b7961b6ec8736070475c296c342be6bb4e189483f020801",

BIN
res/img/3DCharades.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

BIN
res/img/Clearcut.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
res/img/CrimsonCauldron.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

BIN
res/img/CyberJunkCity.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
res/img/DiscGolfLake.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
res/img/Dodgeball.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

BIN
res/img/GoldenTrophy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
res/img/Gym.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

BIN
res/img/Hangar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

BIN
res/img/Homestead.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

BIN
res/img/Lounge.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

BIN
res/img/Paddleball.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

BIN
res/img/Paintball.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

BIN
res/img/Park.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

BIN
res/img/PerformanceHall.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
res/img/Quarry.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

BIN
res/img/RecCenter.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

BIN
res/img/RecRoyaleSolos.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

BIN
res/img/RecRoyaleSquads.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

BIN
res/img/River.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

BIN
res/img/Soccer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
res/img/Spillway.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

BIN
res/img/Stadium.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

File diff suppressed because it is too large Load Diff

View File

@@ -4,27 +4,29 @@ 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";
const config = Config.getConfig(); const config = Config.getConfig();
const log = new Logging('APIUtils'); const log = new Logging("APIUtils");
type AppRouter = { type AppRouter = {
path: string, path: string;
router: express.Router router: express.Router;
} };
export function createRouter(path: string) { export function createRouter(path: string) {
const router: AppRouter = { const router: AppRouter = {
path: path, path: path,
router: express.Router() router: express.Router(),
} };
return router; return router;
} }
export function generateRandomString(length: number) { export function generateRandomString(length: number) {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; const characters =
let randomString = ''; "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let randomString = "";
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * characters.length); const randomIndex = Math.floor(Math.random() * characters.length);
@@ -35,11 +37,20 @@ export function generateRandomString(length: number) {
} }
export function checkQueryTypes<T>(typeDef: T) { export function checkQueryTypes<T>(typeDef: T) {
return (rq: express.Request, rs: express.Response, nxt: express.NextFunction) => { return (
rq: express.Request,
rs: express.Response,
nxt: express.NextFunction,
) => {
for (const key in typeDef) { for (const key in typeDef) {
if (typeof rq.query[key] !== typeof (typeDef)[key]) { if (typeof rq.query[key] !== typeof typeDef[key]) {
rs.statusCode = 400; rs.statusCode = 400;
rs.json(genericResponseFormat(true, "One or more query parameters were invalid or not found.")); rs.json(
genericResponseFormat(
true,
"One or more query parameters were invalid or not found.",
),
);
return; return;
} }
} }
@@ -47,12 +58,21 @@ export function checkQueryTypes<T>(typeDef: T) {
}; };
} }
export function checkBodyTypes<T>(typeDef: T) { export function checkBodyTypes<T>(typeDef: T) {
return (rq: express.Request, rs: express.Response, nxt: express.NextFunction) => { return (
for (const key in typeDef) { rq: express.Request,
if (typeof rq.body[key] !== typeof (typeDef)[key]) { rs: express.Response,
nxt: express.NextFunction,
) => {
for (const key in typeDef) {
if (typeof rq.body[key] !== typeof typeDef[key]) {
log.e(`Body check for key '${key}' failed.`); log.e(`Body check for key '${key}' failed.`);
rs.statusCode = 400; rs.statusCode = 400;
rs.json(genericResponseFormat(true, "One or more body values were invalid or not found.")); rs.json(
genericResponseFormat(
true,
"One or more body values were invalid or not found.",
),
);
return; return;
} }
} }
@@ -60,28 +80,40 @@ export function checkBodyTypes<T>(typeDef: T) {
}; };
} }
export function genericResponseFormat(failure: boolean, msg: string | null = null, data: object | null = null) { export function genericResponseFormat(
failure: boolean,
msg: string | null = null,
data: object | null = null,
) {
return { failed: failure, message: msg, data: data }; return { failed: failure, message: msg, data: data };
} }
export function genericResponse(failure: boolean, msg: string | null = null, data: object | null = null) { export function genericResponse(
failure: boolean,
msg: string | null = null,
data: object | null = null,
) {
return (_rq: express.Request, rs: express.Response) => { return (_rq: express.Request, rs: express.Response) => {
rs.json({ failed: failure, message: msg, data: data }); rs.json({ failed: failure, message: msg, data: data });
}; };
} }
type RecNetResponse = { type RecNetResponse = {
Success: boolean, Success: boolean;
Message: string Message: string;
}; };
export function RecNetResponse(success: boolean, message: string) { export function RecNetResponse(success: boolean, message: string) {
const msg: RecNetResponse = { Success: success, Message: message }; const msg: RecNetResponse = { Success: success, Message: message };
return (_rq: express.Request, rs: express.Response) => { return (_rq: express.Request, rs: express.Response) => {
rs.json(msg); rs.json(msg);
} };
} }
export function logBody(rq: express.Request, _rs: express.Response, nxt: express.NextFunction) { export function logBody(
nxt(); rq: express.Request,
_rs: express.Response,
nxt: express.NextFunction,
) {
log.d(`Request body: ${JSON.stringify(rq.body)}`); log.d(`Request body: ${JSON.stringify(rq.body)}`);
nxt();
} }
export function emptyArrayResponse(_rq: express.Request, rs: express.Response) { export function emptyArrayResponse(_rq: express.Request, rs: express.Response) {
@@ -89,27 +121,26 @@ export function emptyArrayResponse(_rq: express.Request, rs: express.Response) {
} }
export function getSrcIpDefault(rq: express.Request) { export function getSrcIpDefault(rq: express.Request) {
const cfIp = rq.header('cf-connecting-ip'); const cfIp = rq.header("cf-connecting-ip");
if (cfIp !== undefined) return cfIp; if (cfIp !== undefined) return cfIp;
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; const ip = typeof rq.ip === "undefined" ? "(unknown source)" : rq.ip;
return ip; return ip;
} }
export function statusResponse(code: number) { export function statusResponse(code: number) {
return (_rq: express.Request, rs: express.Response) => { return (_rq: express.Request, rs: express.Response) => {
rs.sendStatus(code); rs.sendStatus(code);
} };
} }
export class RateLimiter { export class RateLimiter {
#intervalId: number;
#intervalId: number #hitLimit: number;
#hitLimit: number
#addressHits: Map<string, number> = new Map(); #addressHits: Map<string, number> = new Map();
@@ -118,17 +149,15 @@ export class RateLimiter {
* @param limit Number of hits (inclusive) before requests are blocked * @param limit Number of hits (inclusive) before requests are blocked
*/ */
constructor(interval: number, limit: number) { constructor(interval: number, limit: number) {
this.#hitLimit = limit; this.#hitLimit = limit;
this.#intervalId = setInterval(() => { this.#intervalId = setInterval(() => {
this.#addressHits.clear(); this.#addressHits.clear();
}, interval * 1000); }, interval * 1000);
Deno.addSignalListener('SIGINT', () => { Deno.addSignalListener("SIGINT", () => {
this.#close(); this.#close();
}); });
} }
#addressIncrement(address: string) { #addressIncrement(address: string) {
@@ -147,66 +176,89 @@ export class RateLimiter {
} }
middle() { middle() {
return (
return (rq: express.Request, rs: express.Response, nxt: express.NextFunction) => { rq: express.Request,
rs: express.Response,
nxt: express.NextFunction,
) => {
const address = getSrcIpDefault(rq); const address = getSrcIpDefault(rq);
this.#addressIncrement(address); this.#addressIncrement(address);
const hits = this.#getAddressHits(address); const hits = this.#getAddressHits(address);
if (hits && hits > this.#hitLimit) { if (hits && hits > this.#hitLimit) {
rs.statusCode = 429; rs.statusCode = 429;
rs.json(genericResponseFormat(true, `Rate limit for address ${address} reached. Try again in a moment.`)); rs.json(
genericResponseFormat(
true,
`Rate limit for address ${address} reached. Try again in a moment.`,
),
);
return; return;
} else nxt(); } else nxt();
} };
} }
#close() { #close() {
clearInterval(this.#intervalId); clearInterval(this.#intervalId);
} }
} }
export async function UserAuthentication(rq: express.Request, rs: express.Response, nxt: express.NextFunction) { export interface TokenBaseFormat {
typ: AuthType;
iss: string;
nbf: number;
exp: number;
iat: number;
}
export type TokenFormat = UserTokenFormat | ProfileTokenFormat;
export async function Authentication(
rq: express.Request,
rs: express.Response,
nxt: express.NextFunction,
) {
function returnUnauthorized() { function returnUnauthorized() {
rs.statusCode = 401; rs.statusCode = 401;
rs.json(genericResponseFormat(true, 'Authorization required.')); rs.json(genericResponseFormat(true, "Authorization required."));
} }
const token: string | undefined = rq.header('GalvanicAuth'); const token: string | undefined = rq.header("GalvanicAuth");
if (typeof token == 'undefined') { if (typeof token == "undefined") {
returnUnauthorized(); returnUnauthorized();
return; return;
} }
try { try {
const decodedToken = await decode<UserTokenFormat>(token, config.auth.secret, { algorithm: "HS512" }); const decodedToken = await decode<TokenFormat>(
token,
config.auth.secret,
{
algorithm: "HS512",
},
);
const valid = ![ const valid = ![
decodedToken.iss == config.web.publichost, decodedToken.iss == config.web.publichost,
decodedToken.nbf < Math.round(Date.now() / 1000), decodedToken.nbf < Math.round(Date.now() / 1000),
decodedToken.exp > Math.round(Date.now() / 1000), decodedToken.exp > Math.round(Date.now() / 1000),
decodedToken.typ == AuthType.Web
].includes(false); ].includes(false);
if (valid) { if (valid) {
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);
}
nxt(); nxt();
} } else {
else {
returnUnauthorized(); returnUnauthorized();
return; return;
} }
} catch (err) { } catch (err) {
returnUnauthorized(); returnUnauthorized();
log.w(`User Authentication failed: ${err}`); log.w(`User Authentication failed: ${err}`);
} }
} }
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";

View File

@@ -4,58 +4,58 @@ import * as fs from "node:fs";
const log = new Logging("Config"); const log = new Logging("Config");
type RedisConfiguration = { type RedisConfiguration = {
host: string, host: string;
port: number, port: number;
username: string, username: string;
password: string, password: string;
db: number db: number;
} };
type WebConfiguration = { type WebConfiguration = {
port: number, port: number;
host: string, host: string;
publichost: string, publichost: string;
securepublichost: boolean securepublichost: boolean;
} };
type PublicConfiguration = { type PublicConfiguration = {
serverName: string, serverName: string;
serverId: string, serverId: string;
owner: string, owner: string;
motd: string, motd: string;
levelScale: number, levelScale: number;
maxLevels: number maxLevels: number;
patches: string[], patches: string[];
} };
type LoggingConfiguration = { type LoggingConfiguration = {
notfound: boolean, notfound: boolean;
debug: boolean, debug: boolean;
network: boolean network: boolean;
} };
type DiscordConfiguration = { type DiscordConfiguration = {
token: string, token: string;
clientId: string, clientId: string;
guildId: string guildId: string;
} };
type AuthConfiguration = { type AuthConfiguration = {
secret: string, secret: string;
/** /**
* In Hours * In Hours
*/ */
timeout: number timeout: number;
} };
export type GalvanicConfiguration = { export type GalvanicConfiguration = {
redis: RedisConfiguration, redis: RedisConfiguration;
web: WebConfiguration, web: WebConfiguration;
public: PublicConfiguration, public: PublicConfiguration;
logging: LoggingConfiguration, logging: LoggingConfiguration;
discord: DiscordConfiguration | null, discord: DiscordConfiguration | null;
auth: AuthConfiguration auth: AuthConfiguration;
} };
export const defaultConfig: GalvanicConfiguration = { export const defaultConfig: GalvanicConfiguration = {
redis: { redis: {
@@ -63,7 +63,7 @@ export const defaultConfig: GalvanicConfiguration = {
port: 6379, port: 6379,
username: "", username: "",
password: "", password: "",
db: 0 db: 0,
}, },
web: { web: {
port: 3000, port: 3000,
@@ -83,20 +83,20 @@ export const defaultConfig: GalvanicConfiguration = {
logging: { logging: {
notfound: false, notfound: false,
debug: false, debug: false,
network: false network: false,
}, },
discord: null, discord: null,
auth: { auth: {
secret: "CHANGE-ME-PLEASE", secret: "CHANGE-ME-PLEASE",
timeout: 48 timeout: 48,
} },
} };
/** The current configuration. Read and parsed only during startup. */ /** The current configuration. Read and parsed only during startup. */
let config: GalvanicConfiguration; let config: GalvanicConfiguration;
try { try {
if (!configurationExists()) generateDefaultConfig(); if (!configurationExists()) generateDefaultConfig();
config = JSON.parse(fs.readFileSync('./config.json').toString()); config = JSON.parse(fs.readFileSync("./config.json").toString());
} catch (err) { } catch (err) {
log.e(`Could not get config: ${err}`); log.e(`Could not get config: ${err}`);
Deno.exit(1); Deno.exit(1);
@@ -104,12 +104,15 @@ try {
/** Does the configuration file exist on the disk? */ /** Does the configuration file exist on the disk? */
export function configurationExists() { export function configurationExists() {
return fs.existsSync('./config.json'); return fs.existsSync("./config.json");
} }
/** Place [or overwrite] the [existing] default configuration in the current directory */ /** Place [or overwrite] the [existing] default configuration in the current directory */
export function generateDefaultConfig() { export function generateDefaultConfig() {
fs.writeFileSync('./config.json', JSON.stringify(defaultConfig, undefined, ' ')); fs.writeFileSync(
"./config.json",
JSON.stringify(defaultConfig, undefined, " "),
);
} }
/** Get current server configuration */ /** Get current server configuration */
@@ -117,6 +120,6 @@ export function getConfig() {
return config; return config;
} }
export const devMode = Deno.args.includes('--dev'); export const devMode = Deno.args.includes("--dev");
export * as Config from './config.ts'; export * as Config from "./config.ts";

View File

@@ -3,42 +3,46 @@ import { Redis } from "../db.ts";
import { Objectives } from "./objectives.ts"; import { Objectives } from "./objectives.ts";
export type Config = { export type Config = {
Key: string, Key: string;
Value: string Value: string;
} };
export type LevelProgressionItem = { export type LevelProgressionItem = {
Level: number, Level: number;
RequiredXp: number RequiredXp: number;
} };
export type PublicConfig = { export type PublicConfig = {
ServerMaintenance: { ServerMaintenance: {
StartsInMinutes: number StartsInMinutes: number;
}, };
LevelProgressionMaps: LevelProgressionItem[], LevelProgressionMaps: LevelProgressionItem[];
DailyObjectives: Objectives.Objective[][], DailyObjectives: Objectives.Objective[][];
ConfigTable: Config[] ConfigTable: Config[];
} };
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;
const config = c as Config.GalvanicConfiguration; const config = c as Config.GalvanicConfiguration;
function generateLevelProgressionMap() { function generateLevelProgressionMap() {
const m: LevelProgressionItem[] = []; const m: LevelProgressionItem[] = [];
for (let i = 0; i < config.public.maxLevels + 1; i++) for (let i = 0; i < config.public.maxLevels + 1; i++) {
m.push({Level: i, RequiredXp: Math.round(i * config.public.levelScale * 20)}); m.push({
Level: i,
RequiredXp: Math.round(i * config.public.levelScale * 20),
});
}
return m; return m;
} }
const conf: PublicConfig = { const conf: PublicConfig = {
ServerMaintenance: { ServerMaintenance: {
StartsInMinutes: 0 StartsInMinutes: 0,
}, },
LevelProgressionMaps: generateLevelProgressionMap(), LevelProgressionMaps: generateLevelProgressionMap(),
DailyObjectives: [], DailyObjectives: [],
ConfigTable: [] ConfigTable: [],
} };
return conf; return conf;
} }
@@ -46,10 +50,16 @@ export function getConfig() {
export async function getAllGameConfigs() { export async function getAllGameConfigs() {
try { try {
const gameConfigs = new Map<string, string>(); const gameConfigs = new Map<string, string>();
const val = await Redis.Database.hgetall(Redis.buildKey(Redis.KeyGroups.Config.Root, Redis.KeyGroups.Config.Game)); const val = await Redis.Database.hgetall(
Redis.buildKey(
Redis.KeyGroups.Config.Root,
Redis.KeyGroups.Config.Game,
),
);
for (const key of Object.keys(val)) for (const key of Object.keys(val)) {
gameConfigs.set(key, val[key]); gameConfigs.set(key, val[key]);
}
return gameConfigs; return gameConfigs;
} catch (error) { } catch (error) {
@@ -59,10 +69,23 @@ export async function getAllGameConfigs() {
} }
export function setGameConfig(key: string, value: string) { export function setGameConfig(key: string, value: string) {
return Redis.Database.hset(Redis.buildKey(Redis.KeyGroups.Config.Root, Redis.KeyGroups.Config.Game), key, value); return Redis.Database.hset(
Redis.buildKey(
Redis.KeyGroups.Config.Root,
Redis.KeyGroups.Config.Game,
),
key,
value,
);
} }
export function getGameConfig(key: string) { export function getGameConfig(key: string) {
return Redis.Database.hget(Redis.buildKey(Redis.KeyGroups.Config.Root, Redis.KeyGroups.Config.Game), key); return Redis.Database.hget(
Redis.buildKey(
Redis.KeyGroups.Config.Root,
Redis.KeyGroups.Config.Game,
),
key,
);
} }
export * as GameConfigs from "./config.ts"; export * as GameConfigs from "./config.ts";

View File

@@ -13,5 +13,5 @@ enum AvatarItemType {
Wrist = 200, Wrist = 200,
Glove, Glove,
Watch, Watch,
TeamWrist TeamWrist,
} }

View File

@@ -0,0 +1,15 @@
const base = Deno.mainModule.substring(8, Deno.mainModule.length - 11);
export function getBaseImage(name: string) {
try {
return Deno.readFileSync(`${base}/res/img/${name}`);
} catch (_err) {
return null;
}
}
export function getAllBaseImages() {
return Array.from(
Deno.readDirSync(`${base}/res/img/`).map((val) => val.isFile ? val.name : undefined),
).filter((val) => typeof val == "string");
}
// todo: make this async

View File

@@ -5,7 +5,7 @@ export enum Consumable {
CHOCOLATE_FROSTED_DONUTS, CHOCOLATE_FROSTED_DONUTS,
CHEESE_PIZZA, CHEESE_PIZZA,
PEPPERONI_PIZZA, PEPPERONI_PIZZA,
GLAZED_DONUTS GLAZED_DONUTS,
} }
const ids = [ const ids = [
@@ -15,26 +15,21 @@ const ids = [
"mMCGPgK3tki5S_15q2Z81A", "mMCGPgK3tki5S_15q2Z81A",
"5hIAZ9wg5EyG1cILf4FS2A", "5hIAZ9wg5EyG1cILf4FS2A",
"mq23W-RSP0G8iGNLdrcpUw", "mq23W-RSP0G8iGNLdrcpUw",
"7OZ5AE3uuUyqa0P-2W1ptg" "7OZ5AE3uuUyqa0P-2W1ptg",
] as const; ] as const;
export class ConsumableSelection { export class ConsumableSelection {
type: Consumable; type: Consumable;
guid: string; guid: string;
constructor(type: Consumable) { constructor(type: Consumable) {
this.type = type; this.type = type;
this.guid = ids[type]; this.guid = ids[type];
} }
} }
export class ConsumableBuilder { export class ConsumableBuilder {
Id: number; Id: number;
ConsumableItemDesc: string; ConsumableItemDesc: string;
@@ -47,7 +42,13 @@ export class ConsumableBuilder {
IsActive: boolean; IsActive: boolean;
constructor(selection: ConsumableSelection, id: number, createdAt: Date, count: number, active: boolean) { constructor(
selection: ConsumableSelection,
id: number,
createdAt: Date,
count: number,
active: boolean,
) {
this.Id = id; this.Id = id;
this.ConsumableItemDesc = selection.guid; this.ConsumableItemDesc = selection.guid;
this.CreatedAt = createdAt.toUTCString(); this.CreatedAt = createdAt.toUTCString();
@@ -55,5 +56,4 @@ export class ConsumableBuilder {
this.UnlockedLevel = 0; // All players have access to every consumable - avatars and equipment are different this.UnlockedLevel = 0; // All players have access to every consumable - avatars and equipment are different
this.IsActive = active; this.IsActive = active;
} }
} }

View File

@@ -0,0 +1,36 @@
interface InternalScene {
Name: string;
ReplicationId: string;
RoomSceneLocationId: string;
IsSandbox: boolean;
CanMatchmakeInto: boolean;
SupportsJoinInProgress: boolean;
UseLevelBasedMatchmaking: boolean;
UseAgeBasedMatchmaking: boolean;
MaxPlayers: number;
}
interface InternalRoom {
Name: string;
ReplicationId: string;
Description: string;
Accessibility: number;
SupportsLevelVoting: boolean;
CloningAllowed: boolean;
SupportsScreens: boolean;
SupportsWalkVR: boolean;
SupportsTeleportVR: boolean;
Scenes: InternalScene[];
}
const rooms: InternalRoom[] = JSON.parse(
Deno.readTextFileSync(import.meta.dirname + "/res/rooms.json"),
);
export function getInternalRoom(name: string) {
return rooms.find((val) => val.Name == name);
}
export function getAllInternalRooms() {
return rooms;
}
export function getAllInternalRoomNames() {
return rooms.map((val) => val.Name);
}

View File

@@ -0,0 +1,24 @@
import { Buffer } from "node:buffer";
import { Redis } from "../../db.ts";
export async function getImage(filename: string) {
const data = await Redis.Database.get(
Redis.buildKey(
Redis.KeyGroups.Content.Root,
Redis.KeyGroups.Content.Images,
filename,
),
);
if (data == null) return null;
else return Buffer.from(data);
}
export async function setImage(filename: string, data: Uint8Array) {
await Redis.Database.set(
Redis.buildKey(
Redis.KeyGroups.Content.Root,
Redis.KeyGroups.Content.Images,
filename,
),
Buffer.from(data),
);
}

View File

@@ -85,12 +85,12 @@ export enum ObjectiveType {
ArenaBotTags, ArenaBotTags,
RecRoyaleGames = 3000, RecRoyaleGames = 3000,
RecRoyaleWins, RecRoyaleWins,
RecRoyaleTags RecRoyaleTags,
} }
export type Objective = { export type Objective = {
type: ObjectiveType, type: ObjectiveType;
score: number score: number;
} };
export * as Objectives from "./objectives.ts"; export * as Objectives from "./objectives.ts";

View File

@@ -3,86 +3,171 @@ import Dictionary from "./usernames.ts";
import { Config } from "../config.ts"; import { Config } from "../config.ts";
import { AuthType } from "./users.ts"; import { AuthType } from "./users.ts";
import * as JsonWebToken from "@gz/jwt"; import * as JsonWebToken from "@gz/jwt";
import { TokenBaseFormat } from "../apiutils.ts";
const config = Config.getConfig(); const config = Config.getConfig();
interface ProfileInitOptions { interface ProfileInitOptions {
username: string username: string;
} }
interface AccountExport { interface AccountExport {
accountId: number, accountId: number;
profileImage: string, profileImage: string;
isJunior: boolean, isJunior: boolean;
platforms: number, platforms: number;
username: string, username: string;
displayName: string displayName: string;
} }
export type ProfileTokenFormat = { export interface ProfileTokenFormat extends TokenBaseFormat {
iss: string;
sub: number; sub: number;
nbf: number; typ: AuthType.Game;
iat: number;
exp: number;
typ: AuthType;
} }
class Profile { class Profile {
static async exists(id: number) {
return (await Redis.Database.exists(
Redis.buildKey(
Redis.KeyGroups.Profiles.Root,
id.toString(),
Redis.KeyGroups.Profiles.Username,
),
)) >= 1;
}
static async getUniqueId() { static async getUniqueId() {
let id = Math.round(Math.random() * Math.pow(2, 31)); let id = Math.round(Math.random() * Math.pow(2, 31));
while ((await Redis.Database.exists(Redis.buildKey(Redis.KeyGroups.Profiles.Root, id.toString(), Redis.KeyGroups.Profiles.Username))) >= 1) while (
(await Redis.Database.exists(
Redis.buildKey(
Redis.KeyGroups.Profiles.Root,
id.toString(),
Redis.KeyGroups.Profiles.Username,
),
)) >= 1
) {
id = await this.getUniqueId(); id = await this.getUniqueId();
}
return id; return id;
} }
static async byName(name: string) { static async byName(name: string) {
const id = await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Profile_Usernames, name)); const id = await Redis.Database.get(
Redis.buildKey(Redis.KeyGroups.Profile_Usernames, name),
);
if (id == null) return null; if (id == null) return null;
else return new Profile(parseInt(id, 10)); else return new Profile(parseInt(id, 10));
} }
static async getUniqueUsername() { static async getUniqueUsername() {
let username = `${Dictionary.Adjectives[Math.floor(Math.random() * Dictionary.Adjectives.length)]}${Dictionary.Nouns[Math.floor(Math.random() * Dictionary.Nouns.length)]}${Math.round(Math.random() * 10000)}` let username = `${
while ((await Profile.byName(username)) !== null) username = await this.getUniqueUsername(); Dictionary
.Adjectives[
Math.floor(Math.random() * Dictionary.Adjectives.length)
]
}${
Dictionary
.Nouns[Math.floor(Math.random() * Dictionary.Nouns.length)]
}${Math.round(Math.random() * 10000)}`;
while ((await Profile.byName(username)) !== null) {
username = await this.getUniqueUsername();
}
return username; return username;
} }
static async init(options?: ProfileInitOptions) { static async init(options?: ProfileInitOptions) {
const optionsSpecified = typeof options !== "undefined";
const optionsSpecified = typeof options !== 'undefined';
const newId = await this.getUniqueId(); const newId = await this.getUniqueId();
const newUsername = optionsSpecified ? options.username : await this.getUniqueUsername(); const newUsername = optionsSpecified
? options.username
: await this.getUniqueUsername();
await Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Profile_Usernames, newUsername), newId); await Redis.Database.set(
await Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Profiles.Root, newId.toString(), Redis.KeyGroups.Profiles.Username), newUsername); Redis.buildKey(Redis.KeyGroups.Profile_Usernames, newUsername),
newId,
);
await Redis.Database.set(
Redis.buildKey(
Redis.KeyGroups.Profiles.Root,
newId.toString(),
Redis.KeyGroups.Profiles.Username,
),
newUsername,
);
return new Profile(newId); return new Profile(newId);
} }
// surely this can be written better // surely this can be written better
static getExportAccount(id: number): Promise<AccountExport | null> { static getExportAccount(id: number): Promise<AccountExport | null> {
return new Promise((resolve, _reject) => { return new Promise((resolve, _reject) => {
Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Profiles.Root, id.toString(), Redis.KeyGroups.Profiles.Username)).then(val => { Redis.Database.get(
Redis.buildKey(
Redis.KeyGroups.Profiles.Root,
id.toString(),
Redis.KeyGroups.Profiles.Username,
),
).then((val) => {
if (val == null) resolve(null); if (val == null) resolve(null);
else { else {
const promises = { const promises = {
profileImage: Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Profiles.Root, id.toString(), Redis.KeyGroups.Profiles.ProfileImage)), profileImage: Redis.Database.get(
isJunior: Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Profiles.Root, id.toString(), Redis.KeyGroups.Profiles.Junior)), Redis.buildKey(
platforms: Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Profiles.Root, id.toString(), Redis.KeyGroups.Profiles.Platforms)), Redis.KeyGroups.Profiles.Root,
displayName: Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Profiles.Root, id.toString(), Redis.KeyGroups.Profiles.DisplayName)), id.toString(),
username: Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Profiles.Root, id.toString(), Redis.KeyGroups.Profiles.Username)), Redis.KeyGroups.Profiles.ProfileImage,
} ),
),
isJunior: Redis.Database.get(
Redis.buildKey(
Redis.KeyGroups.Profiles.Root,
id.toString(),
Redis.KeyGroups.Profiles.Junior,
),
),
platforms: Redis.Database.get(
Redis.buildKey(
Redis.KeyGroups.Profiles.Root,
id.toString(),
Redis.KeyGroups.Profiles.Platforms,
),
),
displayName: Redis.Database.get(
Redis.buildKey(
Redis.KeyGroups.Profiles.Root,
id.toString(),
Redis.KeyGroups.Profiles.DisplayName,
),
),
username: Redis.Database.get(
Redis.buildKey(
Redis.KeyGroups.Profiles.Root,
id.toString(),
Redis.KeyGroups.Profiles.Username,
),
),
};
Promise.all(Object.values(promises)).then((values) => { Promise.all(Object.values(promises)).then((values) => {
resolve({ resolve({
accountId: id, accountId: id,
profileImage: values[0] == null ? "DefaultProfileImage" : values[0], profileImage: values[0] == null
isJunior: values[1] == null ? false : JSON.parse(values[1]), ? "DefaultProfileImage"
platforms: values[2] == null ? 1 : JSON.parse(values[2]), : values[0],
displayName: values[3] == null ? (values[4] == null ? "DATABASEERROR" : values[4]) : values[3], isJunior: values[1] == null
username: values[4] == null ? "DATABASEERROR" : values[4], ? false
: JSON.parse(values[1]),
platforms: values[2] == null
? 1
: JSON.parse(values[2]),
displayName: values[3] == null
? (values[4] == null
? "DATABASEERROR"
: values[4])
: values[3],
username: values[4] == null
? "DATABASEERROR"
: values[4],
}); });
}); });
} }
@@ -91,10 +176,10 @@ class Profile {
} }
static async getExportAccountsBulk(ids: number[]) { static async getExportAccountsBulk(ids: number[]) {
const accs = await Promise.all(
const accs = await Promise.all(ids.map(val => this.getExportAccount(val))); ids.map((val) => this.getExportAccount(val)),
return accs.filter(val => val !== null); );
return accs.filter((val) => val !== null);
} }
#id: number; #id: number;
@@ -117,12 +202,14 @@ class Profile {
sub: this.#id, sub: this.#id,
nbf: Math.round(Date.now() / 1000) - 200, nbf: Math.round(Date.now() / 1000) - 200,
iat: Math.round(Date.now() / 1000), iat: Math.round(Date.now() / 1000),
exp: Math.round(Date.now() / 1000) + (config.auth.timeout * 60 * 60), exp: Math.round(Date.now() / 1000) +
typ: AuthType.Web (config.auth.timeout * 60 * 60),
typ: AuthType.Game,
}; };
return await JsonWebToken.encode(payload, config.auth.secret, { algorithm: "HS512" }); return await JsonWebToken.encode(payload, config.auth.secret, {
algorithm: "HS512",
});
} }
} }
export default Profile; export default Profile;

View File

@@ -1,6 +1,301 @@
const Dictionary = { const Dictionary = {
Adjectives: ["Amazing","Adventurous","Affable","Agreeable","Ambitious","Amicable","Animated","Approachable","Articulate","Astute","Attractive","Authentic","Benevolent","Blissful","Bold","Bright","Buoyant","Calm","Captivating","Charismatic","Cheerful","Clever","Compassionate","Confident","Considerate","Content","Cooperative","Courageous","Creative","Cultured","Curious","Dashing","Dazzling","Dedicated","Delightful","Dependable","Determined","Diligent","Dynamic","Earnest","Easygoing","Ebullient","Effervescent","Empathetic","Enchanting","Endearing","Energetic","Engaging","Enthusiastic","Exuberant","Fantastic","Fearless","Fervent","Friendly","Funny","Generous","Gentle","Genuine","Gracious","Grateful","Harmonious","Heartwarming","Helpful","Honest","Humble","Humorous","Imaginative","Impeccable","Incisive","Incredible","Independent","Industrious","Ingenious","Insightful","Intelligent","Intuitive","Invigorating","Jovial","Jubilant","Just","Kind","Knowledgeable","Likable","Lively","Lovable","Loving","Loyal","Luminous","Magnetic","Marvelous","Masterful","Mature","Merciful","Methodical","Meticulous","Mindful","Motivated","Natural","Nurturing","Observant","Optimistic","Outgoing","Passionate","Patient","Peaceful","Perceptive","Perseverant","Persistent","Persuasive","Personable","Philanthropic","Placid","Playful","Pleasant","Poised","Positive","Powerful","Pragmatic","Proactive","Proficient","Prudent","Punctual","Purposeful","Radiant","Rational","Real","Receptive","Reflective","Reliable","Resilient","Resourceful","Respectful","Responsible","Robust","Sagacious","Serene","Sincere","Skillful","Smart","Sociable","Spirited","Splendid","Spontaneous","Steady","Sterling","Strong","Sublime","Successful","Supportive","Sympathetic","Talented","Tenacious","Thoughtful","Tireless","Tolerant","Tough","Tranquil","Trustworthy","Unassuming","Understanding","Unique","Unpretentious","Upbeat","Valiant","Vibrant","Virtuous","Visionary","Vivacious","Warmhearted","Welcoming","Wise","Witty","Wonderful","Zealous"], Adjectives: [
Nouns: ["Nomad","Solstice","Elysium","Horizon","Catalyst","Luminescence","Utopia","Eclipse","Nebula","Arcadia","Apex","Harmony","Zenith","Radiant","Infinity","Echo","Quasar","Cascade","Empyrean","Nebula","Odyssey","Aether","Empower","Zephyr","Vibrance","Astral","Jubilant","Ascendancy","Zen","Nebulous","Ecliptic","Stellar","Quantum","Ethereal","Nexus","Synergy","Quantum","Enigma","Luminous","Epoch","Serendipity","Zenithal","Paragon","Panorama","Maverick","Voyager","Luminary","Catalyst","Phoenix","Dynamo","Zenith","Nexus","Pinnacle","Rhapsody","Serenity","Quantum","Apex","Harmony","Odyssey","Endeavor","Visionary","Epoch","Renaissance","Panache","Jubilee","Resonance","Zen","Nimbus","Ethereal","Cascade","Radiance","Synchronicity","Nebula","Equinox","Pulsar","Apex","Ethos","Wanderlust","Zenith","Nebula","Vertex","Equinox","Odyssey","Pantheon","Elysian","Nebulous","Quantum","Harmonic","Luminance","Paragon","Radiant","Epoch","Vortex","Celestia","Infinitum","Empyrean","Zephyr","Nimbus","Seraphic","Enigma","Synergy","Ecliptic","Utopian","Phoenix","Catalyst","Euphoria","Astral","Nebula","Ethereal","Zenith","Nexus","Empower","Panorama","Cascade","Quantum","Jubilant","Zen","Radiance","Labyrinth"] "Amazing",
} "Adventurous",
"Affable",
"Agreeable",
"Ambitious",
"Amicable",
"Animated",
"Approachable",
"Articulate",
"Astute",
"Attractive",
"Authentic",
"Benevolent",
"Blissful",
"Bold",
"Bright",
"Buoyant",
"Calm",
"Captivating",
"Charismatic",
"Cheerful",
"Clever",
"Compassionate",
"Confident",
"Considerate",
"Content",
"Cooperative",
"Courageous",
"Creative",
"Cultured",
"Curious",
"Dashing",
"Dazzling",
"Dedicated",
"Delightful",
"Dependable",
"Determined",
"Diligent",
"Dynamic",
"Earnest",
"Easygoing",
"Ebullient",
"Effervescent",
"Empathetic",
"Enchanting",
"Endearing",
"Energetic",
"Engaging",
"Enthusiastic",
"Exuberant",
"Fantastic",
"Fearless",
"Fervent",
"Friendly",
"Funny",
"Generous",
"Gentle",
"Genuine",
"Gracious",
"Grateful",
"Harmonious",
"Heartwarming",
"Helpful",
"Honest",
"Humble",
"Humorous",
"Imaginative",
"Impeccable",
"Incisive",
"Incredible",
"Independent",
"Industrious",
"Ingenious",
"Insightful",
"Intelligent",
"Intuitive",
"Invigorating",
"Jovial",
"Jubilant",
"Just",
"Kind",
"Knowledgeable",
"Likable",
"Lively",
"Lovable",
"Loving",
"Loyal",
"Luminous",
"Magnetic",
"Marvelous",
"Masterful",
"Mature",
"Merciful",
"Methodical",
"Meticulous",
"Mindful",
"Motivated",
"Natural",
"Nurturing",
"Observant",
"Optimistic",
"Outgoing",
"Passionate",
"Patient",
"Peaceful",
"Perceptive",
"Perseverant",
"Persistent",
"Persuasive",
"Personable",
"Philanthropic",
"Placid",
"Playful",
"Pleasant",
"Poised",
"Positive",
"Powerful",
"Pragmatic",
"Proactive",
"Proficient",
"Prudent",
"Punctual",
"Purposeful",
"Radiant",
"Rational",
"Real",
"Receptive",
"Reflective",
"Reliable",
"Resilient",
"Resourceful",
"Respectful",
"Responsible",
"Robust",
"Sagacious",
"Serene",
"Sincere",
"Skillful",
"Smart",
"Sociable",
"Spirited",
"Splendid",
"Spontaneous",
"Steady",
"Sterling",
"Strong",
"Sublime",
"Successful",
"Supportive",
"Sympathetic",
"Talented",
"Tenacious",
"Thoughtful",
"Tireless",
"Tolerant",
"Tough",
"Tranquil",
"Trustworthy",
"Unassuming",
"Understanding",
"Unique",
"Unpretentious",
"Upbeat",
"Valiant",
"Vibrant",
"Virtuous",
"Visionary",
"Vivacious",
"Warmhearted",
"Welcoming",
"Wise",
"Witty",
"Wonderful",
"Zealous",
],
Nouns: [
"Nomad",
"Solstice",
"Elysium",
"Horizon",
"Catalyst",
"Luminescence",
"Utopia",
"Eclipse",
"Nebula",
"Arcadia",
"Apex",
"Harmony",
"Zenith",
"Radiant",
"Infinity",
"Echo",
"Quasar",
"Cascade",
"Empyrean",
"Nebula",
"Odyssey",
"Aether",
"Empower",
"Zephyr",
"Vibrance",
"Astral",
"Jubilant",
"Ascendancy",
"Zen",
"Nebulous",
"Ecliptic",
"Stellar",
"Quantum",
"Ethereal",
"Nexus",
"Synergy",
"Quantum",
"Enigma",
"Luminous",
"Epoch",
"Serendipity",
"Zenithal",
"Paragon",
"Panorama",
"Maverick",
"Voyager",
"Luminary",
"Catalyst",
"Phoenix",
"Dynamo",
"Zenith",
"Nexus",
"Pinnacle",
"Rhapsody",
"Serenity",
"Quantum",
"Apex",
"Harmony",
"Odyssey",
"Endeavor",
"Visionary",
"Epoch",
"Renaissance",
"Panache",
"Jubilee",
"Resonance",
"Zen",
"Nimbus",
"Ethereal",
"Cascade",
"Radiance",
"Synchronicity",
"Nebula",
"Equinox",
"Pulsar",
"Apex",
"Ethos",
"Wanderlust",
"Zenith",
"Nebula",
"Vertex",
"Equinox",
"Odyssey",
"Pantheon",
"Elysian",
"Nebulous",
"Quantum",
"Harmonic",
"Luminance",
"Paragon",
"Radiant",
"Epoch",
"Vortex",
"Celestia",
"Infinitum",
"Empyrean",
"Zephyr",
"Nimbus",
"Seraphic",
"Enigma",
"Synergy",
"Ecliptic",
"Utopian",
"Phoenix",
"Catalyst",
"Euphoria",
"Astral",
"Nebula",
"Ethereal",
"Zenith",
"Nexus",
"Empower",
"Panorama",
"Cascade",
"Quantum",
"Jubilant",
"Zen",
"Radiance",
"Labyrinth",
],
};
export default Dictionary; export default Dictionary;

View File

@@ -2,36 +2,38 @@ 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";
type UserInitOptions = { type UserInitOptions = {
client_id: string, client_id: string;
pubkey: string pubkey: string;
} };
type UserCreatedObj = { type UserCreatedObj = {
user: User user: User;
} };
export enum AuthType { export enum AuthType {
Game, Game,
Web Web,
} }
export type UserTokenFormat = { export interface UserTokenFormat extends TokenBaseFormat {
iss: string;
sub: string; sub: string;
nbf: number; typ: AuthType.Web;
iat: number;
exp: number;
typ: AuthType;
} }
const config = Config.getConfig(); const config = Config.getConfig();
export class User { export class User {
static async exists(id: string) { static async exists(id: string) {
return (await Redis.Database.exists(Redis.buildKey(Redis.KeyGroups.Users.Root, id, Redis.KeyGroups.Users.Pubkey))) == 1; return (await Redis.Database.exists(
Redis.buildKey(
Redis.KeyGroups.Users.Root,
id,
Redis.KeyGroups.Users.Pubkey,
),
)) == 1;
} }
/** /**
@@ -41,7 +43,14 @@ export class User {
static async init(options: UserInitOptions) { static async init(options: UserInitOptions) {
if (await User.exists(options.client_id)) return null; if (await User.exists(options.client_id)) return null;
Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Users.Root, options.client_id, Redis.KeyGroups.Users.Pubkey), options.pubkey); Redis.Database.set(
Redis.buildKey(
Redis.KeyGroups.Users.Root,
options.client_id,
Redis.KeyGroups.Users.Pubkey,
),
options.pubkey,
);
const user = new User(options.client_id); const user = new User(options.client_id);
return user; return user;
@@ -53,12 +62,18 @@ export class User {
this.#client_id = client_id; this.#client_id = client_id;
} }
getUuid() { getId() {
return this.#client_id; return this.#client_id;
} }
async exists() { async exists() {
return (await Redis.Database.exists(Redis.buildKey(Redis.KeyGroups.Users.Root, this.#client_id, Redis.KeyGroups.Users.Pubkey))) >= 1; return (await Redis.Database.exists(
Redis.buildKey(
Redis.KeyGroups.Users.Root,
this.#client_id,
Redis.KeyGroups.Users.Pubkey,
),
)) >= 1;
} }
async getToken() { async getToken() {
@@ -67,10 +82,13 @@ export class User {
sub: this.#client_id, sub: this.#client_id,
nbf: Math.round(Date.now() / 1000) - 200, nbf: Math.round(Date.now() / 1000) - 200,
iat: Math.round(Date.now() / 1000), iat: Math.round(Date.now() / 1000),
exp: Math.round(Date.now() / 1000) + (config.auth.timeout * 60 * 60), exp: Math.round(Date.now() / 1000) +
typ: AuthType.Web (config.auth.timeout * 60 * 60),
typ: AuthType.Web,
}; };
return await JsonWebToken.encode(payload, config.auth.secret, {algorithm: "HS512"}); return await JsonWebToken.encode(payload, config.auth.secret, {
algorithm: "HS512",
});
} }
async exportAssociatedProfiles() { async exportAssociatedProfiles() {
@@ -79,28 +97,83 @@ export class User {
} }
async getAssociatedProfiles() { async getAssociatedProfiles() {
const list = await Redis.Database.smembers(Redis.buildKey(Redis.KeyGroups.Users.Root, this.#client_id, Redis.KeyGroups.Users.Profiles)); const list = await Redis.Database.smembers(
return new Set<number>(list.filter(val => !Number.isNaN(parseInt(val, 10))).map(val => parseInt(val, 10))); Redis.buildKey(
Redis.KeyGroups.Users.Root,
this.#client_id,
Redis.KeyGroups.Users.Profiles,
),
);
return new Set<number>(
list.filter((val) => !Number.isNaN(parseInt(val, 10))).map((val) =>
parseInt(val, 10)
),
);
} }
async removeAssociatedProfile(id: number) { async removeAssociatedProfile(id: number) {
await Redis.Database.srem(Redis.buildKey(Redis.KeyGroups.Users.Root, this.#client_id, Redis.KeyGroups.Users.Profiles), id); await Redis.Database.srem(
Redis.buildKey(
Redis.KeyGroups.Users.Root,
this.#client_id,
Redis.KeyGroups.Users.Profiles,
),
id,
);
} }
async addAssociatedProfile(id: number) { async addAssociatedProfile(id: number) {
await Redis.Database.sadd(Redis.buildKey(Redis.KeyGroups.Users.Root, this.#client_id, Redis.KeyGroups.Users.Profiles), id); const dbkey = Redis.buildKey(
Redis.KeyGroups.Users.Root,
this.#client_id,
Redis.KeyGroups.Users.Profiles,
);
if ((await Redis.Database.sismember(dbkey, id)) >= 1) return false;
await Redis.Database.sadd(dbkey, id);
return true;
} }
async addAssociatedPlatformId(id: string) { async addAssociatedPlatformId(id: string) {
await Redis.Database.sadd(Redis.buildKey(Redis.KeyGroups.Users.Root, this.#client_id, Redis.KeyGroups.Users.PlatformIds), id); const dbkey = Redis.buildKey(
Redis.KeyGroups.Users.Root,
this.#client_id,
Redis.KeyGroups.Users.AssociatedPlatforms,
);
if ((await Redis.Database.sismember(dbkey, id)) >= 1) return false;
await Redis.Database.sadd(dbkey, id);
return true;
}
async addAssociatedDeviceId(id: string) {
const dbkey = Redis.buildKey(
Redis.KeyGroups.Users.Root,
this.#client_id,
Redis.KeyGroups.Users.AssociatedDeviceIds,
);
if ((await Redis.Database.sismember(dbkey, id)) >= 1) return false;
await Redis.Database.sadd(dbkey, id);
return true;
}
async addAssociatedIp(ip: string) {
const dbkey = Redis.buildKey(
Redis.KeyGroups.Users.Root,
this.#client_id,
Redis.KeyGroups.Users.AssociatedIps,
);
if ((await Redis.Database.sismember(dbkey, ip)) >= 1) return false;
await Redis.Database.sadd(dbkey, ip);
return true;
} }
async addNonce(str: string) { async addNonce(str: string) {
await Redis.Database.sadd(Redis.buildKey(Redis.KeyGroups.Users.Root, this.#client_id, Redis.KeyGroups.Users.Nonces), str); const dbkey = Redis.buildKey(
Redis.KeyGroups.Users.Root,
this.#client_id,
Redis.KeyGroups.Users.Nonces,
);
if ((await Redis.Database.sismember(dbkey, str)) >= 1) return false;
await Redis.Database.sadd(dbkey, str);
return true;
} }
async hasNonce(str: string) {
return (await Redis.Database.sismember(Redis.buildKey(Redis.KeyGroups.Users.Root, this.#client_id, Redis.KeyGroups.Users.Nonces), str)) >= 1;
}
} }

View File

@@ -6,16 +6,16 @@ import chalk from "npm:chalk@^5.3.0";
const log = new Logging("Redis"); const log = new Logging("Redis");
const config = Config.getConfig(); const config = Config.getConfig();
if (typeof config == 'undefined') { if (typeof config == "undefined") {
log.e(`Cannot start: Redis configuration failed`); log.e(`Cannot start: Redis configuration failed`);
Deno.exit(1); Deno.exit(1);
} }
let shuttingDown = false; let shuttingDown = false;
Deno.addSignalListener('SIGINT', () => { Deno.addSignalListener("SIGINT", () => {
if (shuttingDown) return; if (shuttingDown) return;
shuttingDown = true; shuttingDown = true;
log.n('Disconnecting from Redis'); log.n("Disconnecting from Redis");
Database.quit(); Database.quit();
}); });
@@ -25,30 +25,37 @@ export const Database = new Redis({
username: config.redis.username == "" ? undefined : config.redis.username, username: config.redis.username == "" ? undefined : config.redis.username,
password: config.redis.password == "" ? undefined : config.redis.password, password: config.redis.password == "" ? undefined : config.redis.password,
db: config.redis.db, db: config.redis.db,
lazyConnect: true lazyConnect: true,
}); });
Database.on('connect', async () => { Database.on("connect", async () => {
log.i(`Connected to Redis`); log.i(`Connected to Redis`);
if (Deno.args.includes('--db-flush')) await Database.flushall(() => { if (Deno.args.includes("--db-flush")) {
log.w(`${chalk.inverse('The database was flushed.')}`); await Database.flushall(() => {
}); log.w(`${chalk.inverse("The database was flushed.")}`);
});
}
}); });
Database.on('connecting', () => { Database.on("connecting", () => {
log.n('Connecting to Redis..'); log.n("Connecting to Redis..");
}); });
Database.on('error', (err) => { Database.on("error", (err) => {
log.e(`Redis error: ${err.stack}`); log.e(`Redis error: ${err.stack}`);
}); });
export function buildKey(...args: string[]) { export function buildKey(...args: string[]) {
return args.join(':'); return args.join(":");
} }
export const KeyGroups = { export const KeyGroups = {
Config: { Config: {
Root: "config", Root: "config",
Dynamic: "dynamic", Dynamic: "dynamic",
Game: "game" Game: "game",
},
Content: {
Root: "content",
Images: "images",
Rooms: "rooms",
}, },
Profile_Usernames: "profile-usernames", Profile_Usernames: "profile-usernames",
Profiles: { Profiles: {
@@ -57,14 +64,16 @@ export const KeyGroups = {
ProfileImage: "profileImage", ProfileImage: "profileImage",
Junior: "isJunior", Junior: "isJunior",
Platforms: "platforms", Platforms: "platforms",
DisplayName: "displayname" DisplayName: "displayname",
}, },
Users: { Users: {
Root: "users", Root: "users",
Profiles: "profiles", Profiles: "profiles",
Pubkey: "pubkey", Pubkey: "pubkey",
Nonces: "nonces", Nonces: "nonces",
PlatformIds: "associatedPlatforms" AssociatedPlatforms: "associatedPlatforms",
} AssociatedDeviceIds: "associatedDeviceIds",
} AssociatedIps: "associatedIps",
},
};
export * as Redis from "./db.ts"; export * as Redis from "./db.ts";

View File

@@ -5,30 +5,37 @@ import Logging from "@proxnet/undead-logging";
const log = new Logging("Discord"); const log = new Logging("Discord");
const config = Config.getConfig(); const config = Config.getConfig();
if (typeof config == 'undefined') { if (typeof config == "undefined") {
log.e(`Cannot start: Discord configuration is unavailable`); log.e(`Cannot start: Discord configuration is unavailable`);
Deno.exit(1); Deno.exit(1);
} }
export const client = new discord.Client({ intents: [discord.GatewayIntentBits.Guilds, discord.GatewayIntentBits.GuildPresences] }); export const client = new discord.Client({
intents: [
discord.GatewayIntentBits.Guilds,
discord.GatewayIntentBits.GuildPresences,
],
});
client.once(discord.Events.ClientReady, client => { client.once(discord.Events.ClientReady, (client) => {
log.i(`Logged in to Discord as "${client.user.tag}"`); log.i(`Logged in to Discord as "${client.user.tag}"`);
client.user?.setActivity(config.public.motd, { type: discord.ActivityType.Custom }); client.user?.setActivity(config.public.motd, {
type: discord.ActivityType.Custom,
});
}); });
let shuttingDown = false; let shuttingDown = false;
Deno.addSignalListener('SIGINT', () => { Deno.addSignalListener("SIGINT", () => {
if (client.readyTimestamp == null) return; if (client.readyTimestamp == null) return;
if (shuttingDown) return; if (shuttingDown) return;
shuttingDown = true; shuttingDown = true;
log.n('Disconnecting from Discord'); log.n("Disconnecting from Discord");
client.destroy(); client.destroy();
}); });
export function login() { export function login() {
if (config.discord?.token == Config.defaultConfig.discord?.token) { if (config.discord?.token == Config.defaultConfig.discord?.token) {
log.i('Discord not configured, ignoring'); log.i("Discord not configured, ignoring");
return; return;
} }
log.i(`Creating Discord connection..`); log.i(`Creating Discord connection..`);

View File

@@ -15,16 +15,20 @@ log.i(`Starting Galvanic Corrosion..`);
const config = Config.getConfig(); const config = Config.getConfig();
if (typeof config == 'undefined') { if (typeof config == "undefined") {
log.e('Cannot start: Configuration is undefined'); log.e("Cannot start: Configuration is undefined");
Deno.exit(1); Deno.exit(1);
} }
if (config.auth.secret == Config.defaultConfig.auth.secret) { if (config.auth.secret == Config.defaultConfig.auth.secret) {
log.e(`Cannot start: Auth secret is default. Please change 'secrets.authSecret' in 'config.json'`); log.e(
`Cannot start: Auth secret is default. Please change 'secrets.authSecret' in 'config.json'`,
);
Deno.exit(1); Deno.exit(1);
} }
if (config.public.serverId == Config.defaultConfig.public.serverId) { if (config.public.serverId == Config.defaultConfig.public.serverId) {
log.e(`Cannot start: Server ID is default. Please change 'public.serverId' in 'config.json'`); log.e(
`Cannot start: Server ID is default. Please change 'public.serverId' in 'config.json'`,
);
Deno.exit(1); Deno.exit(1);
} }
@@ -45,41 +49,52 @@ log.n(`Starting HTTP server on http://${host}:${port}`);
const app = express(); const app = express();
app.disable('etag'); app.disable("etag");
app.disable('x-powered-by'); app.disable("x-powered-by");
app.use((rq: express.Request, rs: express.Response, nxt: express.NextFunction) => { app.use(
rs.setHeader('Instance', instanceId) (rq: express.Request, rs: express.Response, nxt: express.NextFunction) => {
log.n(`${APIUtils.getSrcIpDefault(rq)} ${rq.method} ${rq.originalUrl}`); rs.setHeader("Instance", instanceId);
nxt(); log.n(`${APIUtils.getSrcIpDefault(rq)} ${rq.method} ${rq.originalUrl}`);
}); nxt();
},
);
app.get('/info', (_rq, rs) => { app.get("/info", (_rq, rs) => {
rs.json({ rs.json({
name: config.public.serverName, name: config.public.serverName,
id: config.public.serverId, id: config.public.serverId,
motd: config.public.motd, motd: config.public.motd,
patches: config.public.patches patches: config.public.patches,
}); });
}); });
// content routes // content routes
const nameserverRouter = await import('./routes/nameserver.ts'); const nameserverRouter = await import("./routes/nameserver.ts");
const apiRouter = await import('./routes/api.ts'); const apiRouter = await import("./routes/api.ts");
const userRouter = await import('./routes/user.ts'); const userRouter = await import("./routes/user.ts");
const authRouter = await import('./routes/auth.ts'); const authRouter = await import("./routes/auth.ts");
const accountRouter = await import('./routes/account.ts'); const accountRouter = await import("./routes/account.ts");
const imgRouter = await import("./routes/img.ts");
app.use(nameserverRouter.route.path, nameserverRouter.route.router); app.use(nameserverRouter.route.path, nameserverRouter.route.router);
app.use(apiRouter.route.path, apiRouter.route.router); app.use(apiRouter.route.path, apiRouter.route.router);
app.use(userRouter.route.path, userRouter.route.router); app.use(userRouter.route.path, userRouter.route.router);
app.use(authRouter.route.path, authRouter.route.router); app.use(authRouter.route.path, authRouter.route.router);
app.use(accountRouter.route.path, accountRouter.route.router); app.use(accountRouter.route.path, accountRouter.route.router);
app.use(imgRouter.route.path, imgRouter.route.router);
app.use((rq: express.Request, rs: express.Response) => { app.use((rq: express.Request, rs: express.Response) => {
log.e(`${APIUtils.getSrcIpDefault(rq)} 404 ${rq.method} ${rq.url.toString()}`); log.e(
`${APIUtils.getSrcIpDefault(rq)} 404 ${rq.method} ${rq.url.toString()}`,
);
rs.statusCode = 404; rs.statusCode = 404;
rs.json(APIUtils.genericResponseFormat(true, 'Endpoint not found. Check your syntax and/or method.')); rs.json(
APIUtils.genericResponseFormat(
true,
"Endpoint not found. Check your syntax and/or method.",
),
);
}); });
try { try {
@@ -87,7 +102,7 @@ try {
log.n(`Listening on http://${config.web.host}:${config.web.port}`); log.n(`Listening on http://${config.web.host}:${config.web.port}`);
let shuttingDown = false; let shuttingDown = false;
Deno.addSignalListener('SIGINT', () => { Deno.addSignalListener("SIGINT", () => {
if (shuttingDown) return; if (shuttingDown) return;
shuttingDown = true; shuttingDown = true;
log.i(`Shutting down`); log.i(`Shutting down`);

View File

@@ -1,6 +1,6 @@
import { APIUtils } from "../apiutils.ts"; import { APIUtils } from "../apiutils.ts";
import { route as AccountRoute } from "./account/account.ts"; import { route as AccountRoute } from "./account/account.ts";
export const route = APIUtils.createRouter('/accountservice'); export const route = APIUtils.createRouter("/accountservice");
route.router.use(AccountRoute.path, AccountRoute.router); route.router.use(AccountRoute.path, AccountRoute.router);

View File

@@ -5,16 +5,23 @@ import Profile from "../../data/profiles.ts";
export const route = APIUtils.createRouter("/account"); export const route = APIUtils.createRouter("/account");
interface CreateAccountRequestBody { interface CreateAccountRequestBody {
platform: string, platform: string;
platformId: string, platformId: string;
deviceId: string deviceId: string;
} }
route.router.post('/create', const rateLimit = new APIUtils.RateLimiter(25, 5);
APIUtils.UserAuthentication, route.router.post("/create",
rateLimit.middle(),
APIUtils.Authentication,
express.urlencoded({ extended: true }), express.urlencoded({ extended: true }),
APIUtils.checkBodyTypes<CreateAccountRequestBody>({platform: "", platformId: "", deviceId: ""}), APIUtils.checkBodyTypes<CreateAccountRequestBody>({
platform: "",
platformId: "",
deviceId: "",
}),
async (_rq, rs) => { async (_rq, rs) => {
const newAcc = await Profile.init(); const newAcc = await Profile.init();
@@ -23,8 +30,43 @@ route.router.post('/create',
rs.json({ rs.json({
success: true, success: true,
value: await newAcc.export() value: await newAcc.export(),
}); });
}, },
);
route.router.get("/bulk",
rateLimit.middle(),
async (rq: express.Request, rs: express.Response) => {
if (typeof rq.query.id == "object") {
const ids = Object.values(rq.query.id).filter((val) => typeof val == "string").map((val) => parseInt(val, 10)).filter((val) => !isNaN(val));
rs.json([...await Profile.getExportAccountsBulk(ids)]);
} else if (typeof rq.query.id == "string") {
const id = parseInt(rq.query.id, 10);
if (isNaN(id)) {
rs.json(
APIUtils.genericResponseFormat(true, "Query data error"),
);
return;
} else {
rs.json(
[await Profile.getExportAccount(id)].filter((val) =>
val !== null
),
);
return;
}
} else {
rs.json([]);
return;
}
},
); );

View File

@@ -3,7 +3,7 @@ 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 { APIUtils } from "../apiutils.ts"; import { APIUtils } from "../apiutils.ts";
export const route = APIUtils.createRouter('/api'); export const route = APIUtils.createRouter("/api");
route.router.use(VersionCheckRoute.path, VersionCheckRoute.router); route.router.use(VersionCheckRoute.path, VersionCheckRoute.router);
route.router.use(ConfigRoute.path, ConfigRoute.router); route.router.use(ConfigRoute.path, ConfigRoute.router);

View File

@@ -1,9 +1,9 @@
import { APIUtils } from "../../apiutils.ts"; import { APIUtils } from "../../apiutils.ts";
import { GameConfigs } from "../../data/config.ts"; import { GameConfigs } from "../../data/config.ts";
export const route = APIUtils.createRouter('/config'); export const route = APIUtils.createRouter("/config");
route.router.get('/v2', (_rq, rs) => { route.router.get("/v2", (_rq, rs) => {
const config = GameConfigs.getConfig(); const config = GameConfigs.getConfig();
if (config == null) rs.sendStatus(500); if (config == null) rs.sendStatus(500);
else rs.json(config); else rs.json(config);

View File

@@ -1,7 +1,7 @@
import { APIUtils } from "../../apiutils.ts"; import { APIUtils } from "../../apiutils.ts";
export const route = APIUtils.createRouter('/gameconfigs'); export const route = APIUtils.createRouter("/gameconfigs");
route.router.get('/v1/all', (_rq, rs) => { route.router.get("/v1/all", (_rq, rs) => {
rs.json([]); rs.json([]);
}); });

View File

@@ -1,34 +1,35 @@
import { APIUtils } from "../../apiutils.ts"; import { APIUtils } from "../../apiutils.ts";
export const route = APIUtils.createRouter('/versioncheck'); export const route = APIUtils.createRouter("/versioncheck");
const validVersion = '20191120'; const validVersion = "20191120";
enum VersionStatus { enum VersionStatus {
ValidForPlay, ValidForPlay,
ValidForMenu, ValidForMenu,
UpdateRequired UpdateRequired,
} }
type ValidVersionResponse = { type ValidVersionResponse = {
VersionStatus: VersionStatus VersionStatus: VersionStatus;
} };
route.router.get('/v4', (rq, rs) => { route.router.get("/v4", (rq, rs) => {
const requestedVer = rq.query['v'];
const pQuery = rq.query['p']; const requestedVer = rq.query["v"];
if (typeof requestedVer == 'undefined' || typeof pQuery == 'undefined') { const pQuery = rq.query["p"];
if (typeof requestedVer == "undefined" || typeof pQuery == "undefined") {
rs.statusCode = 400; rs.statusCode = 400;
rs.json(APIUtils.genericResponseFormat(true, 'One or more query parameters were not found.')); rs.json(APIUtils.genericResponseFormat(true, "One or more query parameters were not found."));
} } else if (requestedVer !== validVersion) {
else if (requestedVer !== validVersion) {
const res: ValidVersionResponse = { const res: ValidVersionResponse = {
VersionStatus: VersionStatus.UpdateRequired VersionStatus: VersionStatus.UpdateRequired,
} };
rs.json(res); rs.json(res);
} else { } else {
const res: ValidVersionResponse = { const res: ValidVersionResponse = {
VersionStatus: VersionStatus.ValidForPlay VersionStatus: VersionStatus.ValidForPlay,
} };
rs.json(res); rs.json(res);
} }
}); });

View File

@@ -2,7 +2,7 @@ import { APIUtils } from "../apiutils.ts";
import { route as CachedLoginRoute } from "./auth/cachedlogin.ts"; import { route as CachedLoginRoute } from "./auth/cachedlogin.ts";
import { route as ConnectRoute } from "./auth/connect.ts"; import { route as ConnectRoute } from "./auth/connect.ts";
export const route = APIUtils.createRouter('/authservice'); export const route = APIUtils.createRouter("/authservice");
route.router.use(CachedLoginRoute.path, CachedLoginRoute.router); route.router.use(CachedLoginRoute.path, CachedLoginRoute.router);
route.router.use(ConnectRoute.path, ConnectRoute.router); route.router.use(ConnectRoute.path, ConnectRoute.router);

View File

@@ -2,14 +2,19 @@ import { APIUtils } from "../../apiutils.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.UserAuthentication, APIUtils.Authentication,
async (_rq, rs) => { async (_rq, rs) => {
const profiles = await rs.locals.user.exportAssociatedProfiles();
rs.json(await rs.locals.user.exportAssociatedProfiles()); rs.json(profiles.map((acc) => ({
platform: 0,
} platformId: rs.locals.user.getId(),
accountId: acc.accountId,
lastLoginTime: new Date().toISOString(),
requirePassword: false,
})));
},
); );

View File

@@ -1,5 +1,94 @@
import { APIUtils } from "../../apiutils.ts"; import { APIUtils, NoBody } from "../../apiutils.ts";
import express from "express";
import Profile from "../../data/profiles.ts";
export const route = APIUtils.createRouter("/connect"); export const route = APIUtils.createRouter("/connect");
//route.router.post() interface TokenRequestBody {
grant_type: string;
account_id: string;
client_id: string;
client_secret: string;
platform: string;
platform_id: string;
device_id: string;
device_class: string;
time: string;
ver: string;
asid: string;
platform_auth: string;
}
interface TokenResponseBody {
error?: string;
error_description?: string;
access_token: string;
refresh_token: string;
}
route.router.post("/token",
APIUtils.Authentication,
express.urlencoded({ extended: true }),
APIUtils.checkBodyTypes<TokenRequestBody>({
grant_type: "",
account_id: "",
client_id: "",
client_secret: "",
platform: "",
platform_id: "",
device_id: "",
device_class: "",
time: "",
ver: "",
asid: "",
platform_auth: "",
}),
async (
rq: express.Request<NoBody, NoBody, TokenRequestBody>,
rs: express.Response<TokenResponseBody>,
) => {
function requestFailed(msg: string = "invalid_request") {
rs.json({
error: msg,
access_token: "",
refresh_token: "",
});
return;
}
const conditionsMet = ![
rq.body.grant_type == "cached_login",
rq.body.client_id == "recroom",
rq.body.platform == "0",
rq.body.ver == '20191120',
!(rq.body.device_id.length > 96),
!(rq.body.client_secret.length > 96),
!(rq.body.platform_id.length > 32),
!(rq.body.time.length > 32),
!(rq.body.asid.length > 32),
].includes(false);
if (conditionsMet) {
const accounts = await rs.locals.user.getAssociatedProfiles();
const targetAccount = parseInt(rq.body.account_id);
if (isNaN(targetAccount)) requestFailed();
if (!accounts.has(targetAccount)) requestFailed("access_denied");
rs.locals.user.addAssociatedDeviceId(rq.body.device_id);
rs.locals.user.addAssociatedPlatformId(rq.body.platform_id);
const profile = new Profile(targetAccount);
if (!(await Profile.exists(profile.getId()))) requestFailed();
const token = await profile.getToken();
rs.json({
access_token: token,
refresh_token: token,
});
} else requestFailed();
},
);

106
src/routes/img.ts Normal file
View File

@@ -0,0 +1,106 @@
import { APIUtils, NoBody } from "../apiutils.ts";
import * as BaseImages from "../data/content/baseimages.ts";
import Logging from "@proxnet/undead-logging";
import express from "express";
import * as Images from "./../data/content/images.ts";
import { Image } from "https://deno.land/x/imagescript@1.3.0/mod.ts";
import { Buffer } from "node:buffer";
export const route = APIUtils.createRouter("/img");
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890. "
.split("");
function sanitizeString(input: string) {
return input.split("").filter((char) => chars.includes(char)).join("");
}
const baseImages = BaseImages.getAllBaseImages();
interface ImageQueryOptions {
cropSquare?: string;
width?: string;
height?: string;
}
route.router.get(
"*",
async (
rq: express.Request<NoBody, NoBody, NoBody, ImageQueryOptions>,
rs: express.Response,
nxt: express.NextFunction,
) => {
const filename = sanitizeString(
rq.path.substring(1, rq.path.length).replaceAll("%20", " "),
);
// why does it think it is never reassigned? line 39
// deno-lint-ignore prefer-const
let image: Image;
const imageSource = baseImages.includes(filename)
? BaseImages.getBaseImage(filename)
: await Images.getImage(filename);
if (imageSource == null) {
nxt();
return;
}
image = await Image.decode(imageSource);
let cropSquare: boolean = false;
if (typeof rq.query.cropSquare == "string") {
const d = JSON.parse(rq.query.cropSquare);
if (typeof d == "boolean" && d) cropSquare = true;
}
let width: number | null = null;
if (typeof rq.query.width == "string") {
const num = parseInt(rq.query.width);
if (isNaN(num)) width = null;
else width = num;
}
let height: number | null = null;
if (typeof rq.query.height == "string") {
const num = parseInt(rq.query.height);
if (isNaN(num)) height = null;
else height = num;
}
if (cropSquare) {
if (image.width > image.height) {
image.crop(
Math.round(image.width / 2) - Math.round(image.height / 2),
0,
image.height,
image.height,
);
} else {image.crop(
0,
Math.round(image.height / 2) - Math.round(image.width / 2),
image.width,
image.width,
);}
}
if (width && height) {
const targetWidth = width > image.width ? image.width : width;
const targetHeight = height > image.height ? image.height : height;
if (image.width > image.height) {
image.resize(Image.RESIZE_AUTO, height);
image.crop(
Math.round(image.width / 2) - Math.round(targetWidth / 2),
0,
targetWidth,
image.height,
);
} else {
image.resize(width, Image.RESIZE_AUTO);
image.crop(
0,
Math.round(image.height / 2) - Math.round(targetHeight / 2),
image.width,
targetHeight,
);
}
} else if (width) image.resize(width, Image.RESIZE_AUTO);
else if (height) image.resize(Image.RESIZE_AUTO, height);
rs.type("png").send(Buffer.from(await image.encode()));
},
);

View File

@@ -2,38 +2,38 @@ 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.securepublichost ? "https" : "http";
export const route = APIUtils.createRouter('/ns'); export const route = APIUtils.createRouter("/ns");
type NameserverHosts = { type NameserverHosts = {
Auth: string, Auth: string;
API: string, API: string;
WWW: string, WWW: string;
Notifications: string, Notifications: string;
Images: string, Images: string;
CDN: string, CDN: string;
Commerce: string, Commerce: string;
Matchmaking: string, Matchmaking: string;
Storage: string, Storage: string;
Chat: string, Chat: string;
Leaderboard: string Leaderboard: string;
} };
const nameserver: NameserverHosts = { const nameserver: NameserverHosts = {
Auth: `${protocol}://${config.web.publichost}/auth`, Auth: `${protocol}://${config.web.publichost}/auth`,
API: `${protocol}://${config.web.publichost}`, API: `${protocol}://${config.web.publichost}`,
WWW: `${protocol}://${config.web.publichost}`, WWW: `${protocol}://${config.web.publichost}`,
Notifications: `${protocol}://${config.web.publichost}/notify`, Notifications: `${protocol}://${config.web.publichost}/notify`,
Images: `${protocol}://${config.web.publichost}/img`, Images: `${protocol}://${config.web.publichost}/img`,
CDN: `${protocol}://${config.web.publichost}/cdn`, CDN: `${protocol}://${config.web.publichost}/cdn`,
Commerce: `${protocol}://${config.web.publichost}/commerce`, Commerce: `${protocol}://${config.web.publichost}/commerce`,
Matchmaking: `${protocol}://${config.web.publichost}/match`, Matchmaking: `${protocol}://${config.web.publichost}/match`,
Storage: `${protocol}://${config.web.publichost}/storage`, Storage: `${protocol}://${config.web.publichost}/storage`,
Chat: `${protocol}://${config.web.publichost}/chat`, Chat: `${protocol}://${config.web.publichost}/chat`,
Leaderboard: `${protocol}://${config.web.publichost}/leaderboard` Leaderboard: `${protocol}://${config.web.publichost}/leaderboard`,
} };
route.router.get('*', (_rq, rs) => { route.router.get("*", (_rq, rs) => {
rs.json(nameserver); rs.json(nameserver);
}); });

View File

@@ -1,4 +1,4 @@
import { APIUtils, NoBody } from "../apiutils.ts"; import { APIUtils, getSrcIpDefault, NoBody } from "../apiutils.ts";
// @ts-types = "npm:@types/express" // @ts-types = "npm:@types/express"
import express from "express"; import express from "express";
import { User } from "../data/users.ts"; import { User } from "../data/users.ts";
@@ -10,25 +10,25 @@ const log = new Logging("UserRoute");
const config = Config.getConfig(); const config = Config.getConfig();
export const route = APIUtils.createRouter('/user'); export const route = APIUtils.createRouter("/user");
interface AuthRequestSec { interface AuthRequestSec {
timestamp: number, timestamp: number;
nonce: string, nonce: string;
server_id: string server_id: string;
} }
interface AuthRequestRoot { interface AuthRequestRoot {
client_id: string, client_id: string;
message: AuthRequestSec, message: AuthRequestSec;
signature: string, signature: string;
pubkey: string pubkey: string;
} }
const rateLimit = new APIUtils.RateLimiter(60, 1); const rateLimit = new APIUtils.RateLimiter(60, 1);
route.router.post('/auth', route.router.post(
"/auth",
rateLimit.middle(), rateLimit.middle(),
express.json(), express.json(),
APIUtils.checkBodyTypes<AuthRequestRoot>({ APIUtils.checkBodyTypes<AuthRequestRoot>({
@@ -36,72 +36,85 @@ route.router.post('/auth',
message: { message: {
timestamp: 0, timestamp: 0,
nonce: "asdf", nonce: "asdf",
server_id: "asdf" server_id: "asdf",
}, },
signature: "asdf", signature: "asdf",
pubkey: "asdf" pubkey: "asdf",
}), }),
async (
async (rq: express.Request<NoBody, NoBody, AuthRequestRoot>, rs: express.Response) => { rq: express.Request<NoBody, NoBody, AuthRequestRoot>,
rs: express.Response,
) => {
function authFailed(msg: string) { function authFailed(msg: string) {
rs.json(APIUtils.genericResponseFormat(true, msg)); rs.json(APIUtils.genericResponseFormat(true, msg));
} }
if (rq.body.message.server_id !== config.public.serverId) { if (rq.body.message.server_id !== config.public.serverId) {
log.w(`Auth request failed (serverId mismatch), config error?\n given ID: '${rq.body.message.server_id}'\n our ID: '${config.public.serverId}'`); log.w(
authFailed('Authentication request not intended for this server.'); `Auth request failed (serverId mismatch), config error?\n given ID: '${rq.body.message.server_id}'\n our ID: '${config.public.serverId}'`,
);
authFailed("Authentication request not intended for this server.");
return; return;
} }
try { try {
const verify = crypto.createVerify('SHA256'); const verify = crypto.createVerify("SHA256");
verify.update(JSON.stringify(rq.body.message)); verify.update(JSON.stringify(rq.body.message));
verify.end(); verify.end();
const publicKey = await crypto.subtle.importKey( const publicKey = await crypto.subtle.importKey(
"spki", "spki",
(Uint8Array.from(atob(rq.body.pubkey), c => c.charCodeAt(0))).buffer, (Uint8Array.from(atob(rq.body.pubkey), (c) => c.charCodeAt(0)))
.buffer,
{ name: "ECDSA", namedCurve: "P-256" }, { name: "ECDSA", namedCurve: "P-256" },
false, false,
["verify"] ["verify"],
);
const messageBytes = new TextEncoder().encode(
JSON.stringify(rq.body.message),
);
const signatureBytes = Uint8Array.from(
atob(rq.body.signature),
(c) => c.charCodeAt(0),
); );
const messageBytes = new TextEncoder().encode(JSON.stringify(rq.body.message));
const signatureBytes = Uint8Array.from(atob(rq.body.signature), c => c.charCodeAt(0));
const isValid = await crypto.subtle.verify( const isValid = await crypto.subtle.verify(
{ name: "ECDSA", hash: "SHA-256" }, { name: "ECDSA", hash: "SHA-256" },
publicKey, publicKey,
signatureBytes.buffer, signatureBytes.buffer,
messageBytes messageBytes,
); );
if (!isValid) { if (!isValid) {
log.w(`Auth failed for clientId '${rq.body.client_id}'`); log.w(`Auth failed for clientId '${rq.body.client_id}'`);
authFailed('Authentication request failed.'); authFailed("Authentication request failed.");
return; return;
} }
} catch (err) { } catch (err) {
log.d(`Error when verifying auth request: ${err}`); log.d(`Error when verifying auth request: ${err}`);
authFailed('Authentication request failed.'); authFailed("Authentication request failed.");
return; return;
} }
let user = new User(rq.body.client_id); let user = new User(rq.body.client_id);
if (!(await user.exists())) { if (!(await user.exists())) {
const obj = await User.init({ client_id: rq.body.client_id, pubkey: rq.body.pubkey }); const obj = await User.init({
client_id: rq.body.client_id,
pubkey: rq.body.pubkey,
});
if (obj == null) { if (obj == null) {
rs.sendStatus(500); rs.sendStatus(500);
return; return;
} else user = obj; } else user = obj;
} }
if (await user.hasNonce(rq.body.message.nonce)) { if (!(await user.addNonce(rq.body.message.nonce))) {
log.w(`Client '${rq.body.client_id}' has already used nonce. Replay attack?`); log.w(
authFailed('Authentication request failed.'); `Client '${rq.body.client_id}' has already used nonce. Replay attack?`,
);
authFailed("Authentication request failed.");
return; return;
} else user.addNonce(rq.body.message.nonce); }
user.addAssociatedIp(getSrcIpDefault(rq));
const token = await user.getToken(); const token = await user.getToken();
rs.json({ token: token }); rs.json({ token: token });
},
}
); );

View File

@@ -4,8 +4,8 @@ import { User } from "../data/users.ts";
declare global { declare global {
namespace Express { namespace Express {
interface Locals { interface Locals {
profile: Profile profile: Profile;
user: User user: User;
} }
} }
} }