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

4
.gitignore vendored
View File

@@ -131,4 +131,6 @@ dist
# galvanic corrosion
build/
config.json
config.json
.vscode

View File

@@ -1,17 +1,23 @@
# Galvanic Corrosion
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"/>
## Compiling Galvanic Corrosion
* Install [Deno 2.x](https://docs.deno.com/runtime/getting_started/installation/)
* Configure project
- Clone this repo
- Install dependencies with `deno i`
- Compile server with `deno run cross-compile`
- Install
[Deno 2.x](https://docs.deno.com/runtime/getting_started/installation/)
- Configure project
- Clone this repo
- Install dependencies with `deno i`
- Compile server with `deno run cross-compile`
## Client Patches
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`.
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`.

View File

@@ -1,27 +1,28 @@
{
"tasks": {
"compile-win": "deno compile --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",
"cross-compile": "deno run compile-win && deno run compile-linux",
"dev": "deno run -A src/main.ts --dev"
},
"imports": {
"@gz/jwt": "jsr:@gz/jwt@^0.1.0",
"@proxnet/undead-logging": "jsr:@proxnet/undead-logging@^1.2.0",
"@std/assert": "jsr:@std/assert@1",
"@types/cookie-parser": "npm:@types/cookie-parser@^1.4.8",
"@types/express": "npm:@types/express@^5.0.0",
"@types/validator": "npm:@types/validator@^13.12.2",
"cookie-parser": "npm:cookie-parser@^1.4.7",
"discord.js": "npm:discord.js@^14.16.3",
"express": "npm:express@^4.21.2",
"ioredis": "npm:ioredis@^5.5.0",
"validator": "npm:validator@^13.12.0",
"bcrypt": "https://deno.land/x/bcrypt@v0.3.0/mod.ts"
},
"compilerOptions": {
"types": [
"./src/types/express.ts"
]
}
"tasks": {
"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 --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",
"dev": "deno run -A src/main.ts --dev"
},
"imports": {
"@gz/jwt": "jsr:@gz/jwt@^0.1.0",
"@proxnet/undead-logging": "jsr:@proxnet/undead-logging@^1.2.0",
"@std/assert": "jsr:@std/assert@1",
"@types/cookie-parser": "npm:@types/cookie-parser@^1.4.8",
"@types/express": "npm:@types/express@^5.0.0",
"@types/validator": "npm:@types/validator@^13.12.2",
"cookie-parser": "npm:cookie-parser@^1.4.7",
"discord.js": "npm:discord.js@^14.16.3",
"express": "npm:express@^4.21.2",
"ioredis": "npm:ioredis@^5.5.0",
"validator": "npm:validator@^13.12.0",
"bcrypt": "https://deno.land/x/bcrypt@v0.3.0/mod.ts"
},
"files": [],
"compilerOptions": {
"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/internal@^1.0.5": "1.0.5",
"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": "1.4.8_@types+express@5.0.0",
"npm:@types/express@*": "5.0.0",
@@ -110,6 +111,9 @@
"ws"
]
},
"@imagemagick/magick-wasm@0.0.31": {
"integrity": "sha512-QNivAUxSaItuiY8ziI/vRy6TtoecD7TOsD1LGZCG3wv8lfbdGbIj2QiJk0FlGkGwAVR966NlD3mkxPNvQrvq0w=="
},
"@ioredis/commands@1.2.0": {
"integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg=="
},
@@ -708,6 +712,9 @@
"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": {
"https://deno.land/std@0.140.0/_util/assert.ts": "e94f2eb37cebd7f199952e242c77654e43333c1ac4c5c700e929ea3aa5489f74",
"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/emit.generated.js": "0728e0cd293b930db2532f8cb5087fdb77aee1f30a059207533780f40250fd6a",
"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/functions/getFileInMem.ts": "cec6c3c6add22c0c3316d8301994ab583feac5c3052df3072ad12976ea2aeec4",
"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 { Config } from "./config.ts";
import { AuthType, User, UserTokenFormat } from "./data/users.ts";
import Profile, { ProfileTokenFormat } from "./data/profiles.ts";
const config = Config.getConfig();
const log = new Logging('APIUtils');
const log = new Logging("APIUtils");
type AppRouter = {
path: string,
router: express.Router
}
path: string;
router: express.Router;
};
export function createRouter(path: string) {
const router: AppRouter = {
path: path,
router: express.Router()
}
router: express.Router(),
};
return router;
}
export function generateRandomString(length: number) {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let randomString = '';
const characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let randomString = "";
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * characters.length);
@@ -35,11 +37,20 @@ export function generateRandomString(length: number) {
}
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) {
if (typeof rq.query[key] !== typeof (typeDef)[key]) {
if (typeof rq.query[key] !== typeof typeDef[key]) {
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;
}
}
@@ -47,12 +58,21 @@ export function checkQueryTypes<T>(typeDef: T) {
};
}
export function checkBodyTypes<T>(typeDef: T) {
return (rq: express.Request, rs: express.Response, nxt: express.NextFunction) => {
for (const key in typeDef) {
if (typeof rq.body[key] !== typeof (typeDef)[key]) {
return (
rq: express.Request,
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.`);
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;
}
}
@@ -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 };
}
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) => {
rs.json({ failed: failure, message: msg, data: data });
};
}
type RecNetResponse = {
Success: boolean,
Message: string
Success: boolean;
Message: string;
};
export function RecNetResponse(success: boolean, message: string) {
const msg: RecNetResponse = { Success: success, Message: message };
return (_rq: express.Request, rs: express.Response) => {
rs.json(msg);
}
};
}
export function logBody(rq: express.Request, _rs: express.Response, nxt: express.NextFunction) {
nxt();
export function logBody(
rq: express.Request,
_rs: express.Response,
nxt: express.NextFunction,
) {
log.d(`Request body: ${JSON.stringify(rq.body)}`);
nxt();
}
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) {
const cfIp = rq.header('cf-connecting-ip');
const cfIp = rq.header("cf-connecting-ip");
if (cfIp !== undefined) return cfIp;
const xrIp = rq.header('x-real-ip');
const xrIp = rq.header("x-real-ip");
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;
}
export function statusResponse(code: number) {
return (_rq: express.Request, rs: express.Response) => {
rs.sendStatus(code);
}
};
}
export class RateLimiter {
#intervalId: number;
#intervalId: number
#hitLimit: number
#hitLimit: number;
#addressHits: Map<string, number> = new Map();
@@ -118,17 +149,15 @@ export class RateLimiter {
* @param limit Number of hits (inclusive) before requests are blocked
*/
constructor(interval: number, limit: number) {
this.#hitLimit = limit;
this.#intervalId = setInterval(() => {
this.#addressHits.clear();
}, interval * 1000);
Deno.addSignalListener('SIGINT', () => {
Deno.addSignalListener("SIGINT", () => {
this.#close();
});
}
#addressIncrement(address: string) {
@@ -147,66 +176,89 @@ export class RateLimiter {
}
middle() {
return (rq: express.Request, rs: express.Response, nxt: express.NextFunction) => {
return (
rq: express.Request,
rs: express.Response,
nxt: express.NextFunction,
) => {
const address = getSrcIpDefault(rq);
this.#addressIncrement(address);
const hits = this.#getAddressHits(address);
if (hits && hits > this.#hitLimit) {
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;
} else nxt();
}
};
}
#close() {
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() {
rs.statusCode = 401;
rs.json(genericResponseFormat(true, 'Authorization required.'));
rs.json(genericResponseFormat(true, "Authorization required."));
}
const token: string | undefined = rq.header('GalvanicAuth');
if (typeof token == 'undefined') {
const token: string | undefined = rq.header("GalvanicAuth");
if (typeof token == "undefined") {
returnUnauthorized();
return;
}
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 = ![
decodedToken.iss == config.web.publichost,
decodedToken.nbf < Math.round(Date.now() / 1000),
decodedToken.exp > Math.round(Date.now() / 1000),
decodedToken.typ == AuthType.Web
].includes(false);
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();
}
else {
} else {
returnUnauthorized();
return;
}
} catch (err) {
returnUnauthorized();
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");
type RedisConfiguration = {
host: string,
port: number,
username: string,
password: string,
db: number
}
host: string;
port: number;
username: string;
password: string;
db: number;
};
type WebConfiguration = {
port: number,
host: string,
publichost: string,
securepublichost: boolean
}
port: number;
host: string;
publichost: string;
securepublichost: boolean;
};
type PublicConfiguration = {
serverName: string,
serverId: string,
owner: string,
motd: string,
levelScale: number,
maxLevels: number
patches: string[],
}
serverName: string;
serverId: string;
owner: string;
motd: string;
levelScale: number;
maxLevels: number;
patches: string[];
};
type LoggingConfiguration = {
notfound: boolean,
debug: boolean,
network: boolean
}
notfound: boolean;
debug: boolean;
network: boolean;
};
type DiscordConfiguration = {
token: string,
clientId: string,
guildId: string
}
token: string;
clientId: string;
guildId: string;
};
type AuthConfiguration = {
secret: string,
secret: string;
/**
* In Hours
*/
timeout: number
}
timeout: number;
};
export type GalvanicConfiguration = {
redis: RedisConfiguration,
web: WebConfiguration,
public: PublicConfiguration,
logging: LoggingConfiguration,
discord: DiscordConfiguration | null,
auth: AuthConfiguration
}
redis: RedisConfiguration;
web: WebConfiguration;
public: PublicConfiguration;
logging: LoggingConfiguration;
discord: DiscordConfiguration | null;
auth: AuthConfiguration;
};
export const defaultConfig: GalvanicConfiguration = {
redis: {
@@ -63,7 +63,7 @@ export const defaultConfig: GalvanicConfiguration = {
port: 6379,
username: "",
password: "",
db: 0
db: 0,
},
web: {
port: 3000,
@@ -83,20 +83,20 @@ export const defaultConfig: GalvanicConfiguration = {
logging: {
notfound: false,
debug: false,
network: false
network: false,
},
discord: null,
auth: {
secret: "CHANGE-ME-PLEASE",
timeout: 48
}
}
timeout: 48,
},
};
/** The current configuration. Read and parsed only during startup. */
let config: GalvanicConfiguration;
try {
if (!configurationExists()) generateDefaultConfig();
config = JSON.parse(fs.readFileSync('./config.json').toString());
config = JSON.parse(fs.readFileSync("./config.json").toString());
} catch (err) {
log.e(`Could not get config: ${err}`);
Deno.exit(1);
@@ -104,12 +104,15 @@ try {
/** Does the configuration file exist on the disk? */
export function configurationExists() {
return fs.existsSync('./config.json');
return fs.existsSync("./config.json");
}
/** Place [or overwrite] the [existing] default configuration in the current directory */
export function generateDefaultConfig() {
fs.writeFileSync('./config.json', JSON.stringify(defaultConfig, undefined, ' '));
fs.writeFileSync(
"./config.json",
JSON.stringify(defaultConfig, undefined, " "),
);
}
/** Get current server configuration */
@@ -117,6 +120,6 @@ export function getConfig() {
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";
export type Config = {
Key: string,
Value: string
}
Key: string;
Value: string;
};
export type LevelProgressionItem = {
Level: number,
RequiredXp: number
}
Level: number;
RequiredXp: number;
};
export type PublicConfig = {
ServerMaintenance: {
StartsInMinutes: number
},
LevelProgressionMaps: LevelProgressionItem[],
DailyObjectives: Objectives.Objective[][],
ConfigTable: Config[]
}
StartsInMinutes: number;
};
LevelProgressionMaps: LevelProgressionItem[];
DailyObjectives: Objectives.Objective[][];
ConfigTable: Config[];
};
export function getConfig() {
const c = Config.getConfig();
if (typeof c == 'undefined') return null;
if (typeof c == "undefined") return null;
const config = c as Config.GalvanicConfiguration;
function generateLevelProgressionMap() {
const m: LevelProgressionItem[] = [];
for (let i = 0; i < config.public.maxLevels + 1; i++)
m.push({Level: i, RequiredXp: Math.round(i * config.public.levelScale * 20)});
for (let i = 0; i < config.public.maxLevels + 1; i++) {
m.push({
Level: i,
RequiredXp: Math.round(i * config.public.levelScale * 20),
});
}
return m;
}
const conf: PublicConfig = {
ServerMaintenance: {
StartsInMinutes: 0
StartsInMinutes: 0,
},
LevelProgressionMaps: generateLevelProgressionMap(),
DailyObjectives: [],
ConfigTable: []
}
ConfigTable: [],
};
return conf;
}
@@ -46,11 +50,17 @@ export function getConfig() {
export async function getAllGameConfigs() {
try {
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]);
}
return gameConfigs;
} catch (error) {
console.error("Error fetching game configs:", error);
@@ -59,10 +69,23 @@ export async function getAllGameConfigs() {
}
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) {
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,
Glove,
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,
CHEESE_PIZZA,
PEPPERONI_PIZZA,
GLAZED_DONUTS
GLAZED_DONUTS,
}
const ids = [
@@ -15,26 +15,21 @@ const ids = [
"mMCGPgK3tki5S_15q2Z81A",
"5hIAZ9wg5EyG1cILf4FS2A",
"mq23W-RSP0G8iGNLdrcpUw",
"7OZ5AE3uuUyqa0P-2W1ptg"
"7OZ5AE3uuUyqa0P-2W1ptg",
] as const;
export class ConsumableSelection {
type: Consumable;
guid: string;
constructor(type: Consumable) {
this.type = type;
this.guid = ids[type];
}
}
export class ConsumableBuilder {
Id: number;
ConsumableItemDesc: string;
@@ -47,7 +42,13 @@ export class ConsumableBuilder {
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.ConsumableItemDesc = selection.guid;
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.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,
RecRoyaleGames = 3000,
RecRoyaleWins,
RecRoyaleTags
RecRoyaleTags,
}
export type Objective = {
type: ObjectiveType,
score: number
}
type: ObjectiveType;
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 { AuthType } from "./users.ts";
import * as JsonWebToken from "@gz/jwt";
import { TokenBaseFormat } from "../apiutils.ts";
const config = Config.getConfig();
interface ProfileInitOptions {
username: string
username: string;
}
interface AccountExport {
accountId: number,
profileImage: string,
isJunior: boolean,
platforms: number,
username: string,
displayName: string
accountId: number;
profileImage: string;
isJunior: boolean;
platforms: number;
username: string;
displayName: string;
}
export type ProfileTokenFormat = {
iss: string;
export interface ProfileTokenFormat extends TokenBaseFormat {
sub: number;
nbf: number;
iat: number;
exp: number;
typ: AuthType;
typ: AuthType.Game;
}
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() {
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();
}
return id;
}
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;
else return new Profile(parseInt(id, 10));
}
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)}`
while ((await Profile.byName(username)) !== null) username = await this.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)}`;
while ((await Profile.byName(username)) !== null) {
username = await this.getUniqueUsername();
}
return username;
}
static async init(options?: ProfileInitOptions) {
const optionsSpecified = typeof options !== "undefined";
const optionsSpecified = typeof options !== 'undefined';
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(Redis.buildKey(Redis.KeyGroups.Profiles.Root, newId.toString(), Redis.KeyGroups.Profiles.Username), newUsername);
await Redis.Database.set(
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);
}
// surely this can be written better
static getExportAccount(id: number): Promise<AccountExport | null> {
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);
else {
const promises = {
profileImage: Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Profiles.Root, id.toString(), 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)),
}
profileImage: Redis.Database.get(
Redis.buildKey(
Redis.KeyGroups.Profiles.Root,
id.toString(),
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) => {
resolve({
accountId: id,
profileImage: values[0] == null ? "DefaultProfileImage" : values[0],
isJunior: values[1] == null ? 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],
profileImage: values[0] == null
? "DefaultProfileImage"
: values[0],
isJunior: values[1] == null
? 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[]) {
const accs = await Promise.all(ids.map(val => this.getExportAccount(val)));
return accs.filter(val => val !== null);
const accs = await Promise.all(
ids.map((val) => this.getExportAccount(val)),
);
return accs.filter((val) => val !== null);
}
#id: number;
@@ -103,7 +188,7 @@ class Profile {
this.#id = id;
}
getId() {
getId() {
return this.#id;
}
@@ -117,12 +202,14 @@ class Profile {
sub: this.#id,
nbf: Math.round(Date.now() / 1000) - 200,
iat: Math.round(Date.now() / 1000),
exp: Math.round(Date.now() / 1000) + (config.auth.timeout * 60 * 60),
typ: AuthType.Web
exp: Math.round(Date.now() / 1000) +
(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 = {
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"],
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"]
}
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",
],
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 { Config } from "../config.ts";
import Profile from "./profiles.ts";
import { TokenBaseFormat } from "../apiutils.ts";
type UserInitOptions = {
client_id: string,
pubkey: string
}
client_id: string;
pubkey: string;
};
type UserCreatedObj = {
user: User
}
user: User;
};
export enum AuthType {
Game,
Web
Web,
}
export type UserTokenFormat = {
iss: string;
export interface UserTokenFormat extends TokenBaseFormat {
sub: string;
nbf: number;
iat: number;
exp: number;
typ: AuthType;
typ: AuthType.Web;
}
const config = Config.getConfig();
export class User {
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,8 +43,15 @@ export class User {
static async init(options: UserInitOptions) {
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);
return user;
}
@@ -53,12 +62,18 @@ export class User {
this.#client_id = client_id;
}
getUuid() {
getId() {
return this.#client_id;
}
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() {
@@ -67,10 +82,13 @@ export class User {
sub: this.#client_id,
nbf: Math.round(Date.now() / 1000) - 200,
iat: Math.round(Date.now() / 1000),
exp: Math.round(Date.now() / 1000) + (config.auth.timeout * 60 * 60),
typ: AuthType.Web
exp: Math.round(Date.now() / 1000) +
(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() {
@@ -79,28 +97,83 @@ export class User {
}
async getAssociatedProfiles() {
const list = await Redis.Database.smembers(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)));
const list = await Redis.Database.smembers(
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) {
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) {
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) {
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) {
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 config = Config.getConfig();
if (typeof config == 'undefined') {
if (typeof config == "undefined") {
log.e(`Cannot start: Redis configuration failed`);
Deno.exit(1);
}
let shuttingDown = false;
Deno.addSignalListener('SIGINT', () => {
Deno.addSignalListener("SIGINT", () => {
if (shuttingDown) return;
shuttingDown = true;
log.n('Disconnecting from Redis');
log.n("Disconnecting from Redis");
Database.quit();
});
@@ -25,30 +25,37 @@ export const Database = new Redis({
username: config.redis.username == "" ? undefined : config.redis.username,
password: config.redis.password == "" ? undefined : config.redis.password,
db: config.redis.db,
lazyConnect: true
lazyConnect: true,
});
Database.on('connect', async () => {
Database.on("connect", async () => {
log.i(`Connected to Redis`);
if (Deno.args.includes('--db-flush')) await Database.flushall(() => {
log.w(`${chalk.inverse('The database was flushed.')}`);
});
if (Deno.args.includes("--db-flush")) {
await Database.flushall(() => {
log.w(`${chalk.inverse("The database was flushed.")}`);
});
}
});
Database.on('connecting', () => {
log.n('Connecting to Redis..');
Database.on("connecting", () => {
log.n("Connecting to Redis..");
});
Database.on('error', (err) => {
Database.on("error", (err) => {
log.e(`Redis error: ${err.stack}`);
});
export function buildKey(...args: string[]) {
return args.join(':');
return args.join(":");
}
export const KeyGroups = {
Config: {
Root: "config",
Dynamic: "dynamic",
Game: "game"
Game: "game",
},
Content: {
Root: "content",
Images: "images",
Rooms: "rooms",
},
Profile_Usernames: "profile-usernames",
Profiles: {
@@ -57,14 +64,16 @@ export const KeyGroups = {
ProfileImage: "profileImage",
Junior: "isJunior",
Platforms: "platforms",
DisplayName: "displayname"
DisplayName: "displayname",
},
Users: {
Root: "users",
Profiles: "profiles",
Pubkey: "pubkey",
Nonces: "nonces",
PlatformIds: "associatedPlatforms"
}
}
export * as Redis from "./db.ts";
AssociatedPlatforms: "associatedPlatforms",
AssociatedDeviceIds: "associatedDeviceIds",
AssociatedIps: "associatedIps",
},
};
export * as Redis from "./db.ts";

View File

@@ -5,34 +5,41 @@ import Logging from "@proxnet/undead-logging";
const log = new Logging("Discord");
const config = Config.getConfig();
if (typeof config == 'undefined') {
if (typeof config == "undefined") {
log.e(`Cannot start: Discord configuration is unavailable`);
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}"`);
client.user?.setActivity(config.public.motd, { type: discord.ActivityType.Custom });
client.user?.setActivity(config.public.motd, {
type: discord.ActivityType.Custom,
});
});
let shuttingDown = false;
Deno.addSignalListener('SIGINT', () => {
Deno.addSignalListener("SIGINT", () => {
if (client.readyTimestamp == null) return;
if (shuttingDown) return;
shuttingDown = true;
log.n('Disconnecting from Discord');
log.n("Disconnecting from Discord");
client.destroy();
});
export function login() {
if (config.discord?.token == Config.defaultConfig.discord?.token) {
log.i('Discord not configured, ignoring');
log.i("Discord not configured, ignoring");
return;
}
log.i(`Creating Discord connection..`);
client.login(config.discord?.token);
}
export * as Discord from "./discord.ts";
export * as Discord from "./discord.ts";

View File

@@ -15,16 +15,20 @@ log.i(`Starting Galvanic Corrosion..`);
const config = Config.getConfig();
if (typeof config == 'undefined') {
log.e('Cannot start: Configuration is undefined');
if (typeof config == "undefined") {
log.e("Cannot start: Configuration is undefined");
Deno.exit(1);
}
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);
}
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);
}
@@ -45,41 +49,52 @@ log.n(`Starting HTTP server on http://${host}:${port}`);
const app = express();
app.disable('etag');
app.disable('x-powered-by');
app.disable("etag");
app.disable("x-powered-by");
app.use((rq: express.Request, rs: express.Response, nxt: express.NextFunction) => {
rs.setHeader('Instance', instanceId)
log.n(`${APIUtils.getSrcIpDefault(rq)} ${rq.method} ${rq.originalUrl}`);
nxt();
});
app.use(
(rq: express.Request, rs: express.Response, nxt: express.NextFunction) => {
rs.setHeader("Instance", instanceId);
log.n(`${APIUtils.getSrcIpDefault(rq)} ${rq.method} ${rq.originalUrl}`);
nxt();
},
);
app.get('/info', (_rq, rs) => {
app.get("/info", (_rq, rs) => {
rs.json({
name: config.public.serverName,
id: config.public.serverId,
motd: config.public.motd,
patches: config.public.patches
patches: config.public.patches,
});
});
// content routes
const nameserverRouter = await import('./routes/nameserver.ts');
const apiRouter = await import('./routes/api.ts');
const userRouter = await import('./routes/user.ts');
const authRouter = await import('./routes/auth.ts');
const accountRouter = await import('./routes/account.ts');
const nameserverRouter = await import("./routes/nameserver.ts");
const apiRouter = await import("./routes/api.ts");
const userRouter = await import("./routes/user.ts");
const authRouter = await import("./routes/auth.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(apiRouter.route.path, apiRouter.route.router);
app.use(userRouter.route.path, userRouter.route.router);
app.use(authRouter.route.path, authRouter.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) => {
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.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 {
@@ -87,7 +102,7 @@ try {
log.n(`Listening on http://${config.web.host}:${config.web.port}`);
let shuttingDown = false;
Deno.addSignalListener('SIGINT', () => {
Deno.addSignalListener("SIGINT", () => {
if (shuttingDown) return;
shuttingDown = true;
log.i(`Shutting down`);
@@ -100,4 +115,4 @@ try {
Deno.exit(1);
}
Discord.login();
Discord.login();

View File

@@ -1,6 +1,6 @@
import { APIUtils } from "../apiutils.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");
interface CreateAccountRequestBody {
platform: string,
platformId: string,
deviceId: string
platform: string;
platformId: 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 }),
APIUtils.checkBodyTypes<CreateAccountRequestBody>({platform: "", platformId: "", deviceId: ""}),
APIUtils.checkBodyTypes<CreateAccountRequestBody>({
platform: "",
platformId: "",
deviceId: "",
}),
async (_rq, rs) => {
const newAcc = await Profile.init();
@@ -23,8 +30,43 @@ route.router.post('/create',
rs.json({
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,8 +3,8 @@ import { route as ConfigRoute } from "./api/config.ts";
import { route as GameConfig } from "./api/gameconfigs.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(ConfigRoute.path, ConfigRoute.router);
route.router.use(GameConfig.path, GameConfig.router);
route.router.use(GameConfig.path, GameConfig.router);

View File

@@ -1,10 +1,10 @@
import { APIUtils } from "../../apiutils.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();
if (config == null) rs.sendStatus(500);
else rs.json(config);
});
});

View File

@@ -1,7 +1,7 @@
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([]);
});
});

View File

@@ -1,34 +1,35 @@
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 {
ValidForPlay,
ValidForMenu,
UpdateRequired
UpdateRequired,
}
type ValidVersionResponse = {
VersionStatus: VersionStatus
}
VersionStatus: VersionStatus;
};
route.router.get('/v4', (rq, rs) => {
const requestedVer = rq.query['v'];
const pQuery = rq.query['p'];
if (typeof requestedVer == 'undefined' || typeof pQuery == 'undefined') {
route.router.get("/v4", (rq, rs) => {
const requestedVer = rq.query["v"];
const pQuery = rq.query["p"];
if (typeof requestedVer == "undefined" || typeof pQuery == "undefined") {
rs.statusCode = 400;
rs.json(APIUtils.genericResponseFormat(true, 'One or more query parameters were not found.'));
}
else if (requestedVer !== validVersion) {
rs.json(APIUtils.genericResponseFormat(true, "One or more query parameters were not found."));
} else if (requestedVer !== validVersion) {
const res: ValidVersionResponse = {
VersionStatus: VersionStatus.UpdateRequired
}
VersionStatus: VersionStatus.UpdateRequired,
};
rs.json(res);
} else {
const res: ValidVersionResponse = {
VersionStatus: VersionStatus.ValidForPlay
}
VersionStatus: VersionStatus.ValidForPlay,
};
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 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(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");
route.router.get('/forplatformid/:platformtype/:platformid',
route.router.get("/forplatformid/:platformtype/:platformid",
APIUtils.UserAuthentication,
APIUtils.Authentication,
async (_rq, rs) => {
rs.json(await rs.locals.user.exportAssociatedProfiles());
const profiles = 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");
//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";
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 = {
Auth: string,
API: string,
WWW: string,
Notifications: string,
Images: string,
CDN: string,
Commerce: string,
Matchmaking: string,
Storage: string,
Chat: string,
Leaderboard: string
}
Auth: string;
API: string;
WWW: string;
Notifications: string;
Images: string;
CDN: string;
Commerce: string;
Matchmaking: string;
Storage: string;
Chat: string;
Leaderboard: string;
};
const nameserver: NameserverHosts = {
Auth: `${protocol}://${config.web.publichost}/auth`,
API: `${protocol}://${config.web.publichost}`,
WWW: `${protocol}://${config.web.publichost}`,
Notifications: `${protocol}://${config.web.publichost}/notify`,
Images: `${protocol}://${config.web.publichost}/img`,
CDN: `${protocol}://${config.web.publichost}/cdn`,
Commerce: `${protocol}://${config.web.publichost}/commerce`,
Matchmaking: `${protocol}://${config.web.publichost}/match`,
Storage: `${protocol}://${config.web.publichost}/storage`,
Chat: `${protocol}://${config.web.publichost}/chat`,
Leaderboard: `${protocol}://${config.web.publichost}/leaderboard`
}
Auth: `${protocol}://${config.web.publichost}/auth`,
API: `${protocol}://${config.web.publichost}`,
WWW: `${protocol}://${config.web.publichost}`,
Notifications: `${protocol}://${config.web.publichost}/notify`,
Images: `${protocol}://${config.web.publichost}/img`,
CDN: `${protocol}://${config.web.publichost}/cdn`,
Commerce: `${protocol}://${config.web.publichost}/commerce`,
Matchmaking: `${protocol}://${config.web.publichost}/match`,
Storage: `${protocol}://${config.web.publichost}/storage`,
Chat: `${protocol}://${config.web.publichost}/chat`,
Leaderboard: `${protocol}://${config.web.publichost}/leaderboard`,
};
route.router.get('*', (_rq, rs) => {
rs.json(nameserver);
route.router.get("*", (_rq, rs) => {
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"
import express from "express";
import { User } from "../data/users.ts";
@@ -10,25 +10,25 @@ const log = new Logging("UserRoute");
const config = Config.getConfig();
export const route = APIUtils.createRouter('/user');
export const route = APIUtils.createRouter("/user");
interface AuthRequestSec {
timestamp: number,
nonce: string,
server_id: string
timestamp: number;
nonce: string;
server_id: string;
}
interface AuthRequestRoot {
client_id: string,
message: AuthRequestSec,
signature: string,
pubkey: string
client_id: string;
message: AuthRequestSec;
signature: string;
pubkey: string;
}
const rateLimit = new APIUtils.RateLimiter(60, 1);
route.router.post('/auth',
route.router.post(
"/auth",
rateLimit.middle(),
express.json(),
APIUtils.checkBodyTypes<AuthRequestRoot>({
@@ -36,72 +36,85 @@ route.router.post('/auth',
message: {
timestamp: 0,
nonce: "asdf",
server_id: "asdf"
server_id: "asdf",
},
signature: "asdf",
pubkey: "asdf"
pubkey: "asdf",
}),
async (rq: express.Request<NoBody, NoBody, AuthRequestRoot>, rs: express.Response) => {
async (
rq: express.Request<NoBody, NoBody, AuthRequestRoot>,
rs: express.Response,
) => {
function authFailed(msg: string) {
rs.json(APIUtils.genericResponseFormat(true, msg));
}
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}'`);
authFailed('Authentication request not intended for this server.');
log.w(
`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;
}
try {
const verify = crypto.createVerify('SHA256');
const verify = crypto.createVerify("SHA256");
verify.update(JSON.stringify(rq.body.message));
verify.end();
const publicKey = await crypto.subtle.importKey(
"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" },
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(
{ name: "ECDSA", hash: "SHA-256" },
publicKey,
signatureBytes.buffer,
messageBytes
messageBytes,
);
if (!isValid) {
log.w(`Auth failed for clientId '${rq.body.client_id}'`);
authFailed('Authentication request failed.');
authFailed("Authentication request failed.");
return;
}
} catch (err) {
log.d(`Error when verifying auth request: ${err}`);
authFailed('Authentication request failed.');
authFailed("Authentication request failed.");
return;
}
let user = new User(rq.body.client_id);
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) {
rs.sendStatus(500);
return;
} else user = obj;
}
if (await user.hasNonce(rq.body.message.nonce)) {
log.w(`Client '${rq.body.client_id}' has already used nonce. Replay attack?`);
authFailed('Authentication request failed.');
if (!(await user.addNonce(rq.body.message.nonce))) {
log.w(
`Client '${rq.body.client_id}' has already used nonce. Replay attack?`,
);
authFailed("Authentication request failed.");
return;
} else user.addNonce(rq.body.message.nonce);
}
user.addAssociatedIp(getSrcIpDefault(rq));
const token = await user.getToken();
rs.json({ token: token });
}
);
},
);

View File

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