diff --git a/CONFIG.md b/CONFIG.md
index 42b7e6c..d027011 100644
--- a/CONFIG.md
+++ b/CONFIG.md
@@ -2,6 +2,9 @@
[<-- Click to return to README.md](./README.md)
+We recommend that you store the configuration file `config.json` in a safe place where Galvanic Corrosion can access it (the current directory).
+No other user on your server system should be able to access the file.
+
## Redis
Redis is database software and must be installed for Galvanic Corrosion.
@@ -41,7 +44,8 @@ This section contains basic information regarding your server.
`serverName`: Somewhat invisible to players, but is an official label your server could appear as (to future server lists?)
`serverId`: Used in the authentication process, uniquely identifies your server to clients. Players should never see this.
-Ideally, this should be unique for every server, and can be chosen by the server administrator.
+Ideally, this should be unique for every server, and can be chosen by the server administrator.
+Example: zombieb-cool-gc-server
`owner`: That's you! You can insert your handle for any social networking site or some form of identification here.
@@ -57,20 +61,31 @@ Ideally, this should be unique for every server, and can be chosen by the server
this can be anything *except* for "none" or 4, since there is only one server to connect to and the game uses offline mode when the region ID is set to none.
`initialRoom`: On game startup, redirects the player to this room name instead of their DormRoom. Set to null if a "natural" startup is preferred.
-Ideally, this room should not be private and should be matchmakeable.
+This room must not be private and must be matchmakeable.
## Logging
-These three values expose booleans you can change to enable/disable logging various messages sent by the server used for debugging or troubleshooting purposes.
+These three values expose booleans you can change to enable/disable logging various messages used for debugging or troubleshooting purposes.
## Discord
-Currently unused.
+Can be `null`. Currently unused.
A Discord Bot is planned for interacting with your server outside of the game.
## Auth
Parameters used by the server's authentication mechanisms.
-`secret`: Used to generate tokens. should never be shared (by extension, the whole file) and can be a string of characters containing no words or patterns.
+`secret`: Used to generate tokens. Should never be shared (the entire file) and can be a string of characters containing no words or patterns.
Use secure cryptography APIs in programming languages to generate random strings.
-`timeout`: The maximum age for a token.
\ No newline at end of file
+`timeout`: The maximum age for a token.
+
+### Auth Verification
+Can be `null`. Cloudflare Turnstile is used to verify users before they create their account.
+
+`enabled`: `true` by default. This section may also be `null`.
+This enables the `GET /user/verify` endpoint that is presented to users wishing to create an account.
+`POST /user/verify` is used for Turnstile siteverify.
+
+`sitekey`: Turnstile sitekey from Cloudflare dashboard
+
+`secretkey`: Turnstile secretkey from Cloudflare dashboard
\ No newline at end of file
diff --git a/README.md b/README.md
index e13d490..da9af1a 100644
--- a/README.md
+++ b/README.md
@@ -14,10 +14,8 @@ Galvanic Corrosion and its contributors are **not** associated in **any** form w
* Exit Games Inc.
* Against Gravity
* Photon Network, Photon Engine, or services associated
-* Services in use by or associated with the RecNet platform
* Any person(s) in contact with or employed for or with Rec Room Inc.
-
## Configuration
Read how to configure Galvanic Corrosion [here.](./CONFIG.md)
@@ -29,10 +27,4 @@ Read how to configure Galvanic Corrosion [here.](./CONFIG.md)
- 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.
-
Place desired patch ID strings into the config `public.patches`.
+ - Compile server with `deno run cross-compile`
\ No newline at end of file
diff --git a/src/apiutils.ts b/src/apiutils.ts
index 47c537d..acf6831 100644
--- a/src/apiutils.ts
+++ b/src/apiutils.ts
@@ -365,7 +365,7 @@ export function startTimer(_rq: express.Request, rs: express.Response, nxt: expr
nxt();
}
export function stopTimer(_rq: express.Request, rs: express.Response) {
- log.n(`(${rs.locals.reqId.substring(0, 11)}) Middleware took ${(performance.now() - rs.locals.timer).toString().substring(0, 6)} ms`);
+ log.n(`(${rs.locals.reqId}) Middleware took ${(performance.now() - rs.locals.timer).toString().substring(0, 6)} ms`);
}
export * as APIUtils from "./apiutils.ts";
diff --git a/src/config.ts b/src/config.ts
index 3a814be..23976b8 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -71,6 +71,7 @@ type AuthConfiguration = {
* In Hours
*/
timeout: number;
+ steamkey: string | null;
};
export type GalvanicConfiguration = {
@@ -124,6 +125,7 @@ export const defaultConfig: GalvanicConfiguration = {
auth: {
secret: "CHANGE-ME-PLEASE",
timeout: 3,
+ steamkey: null
},
};
diff --git a/src/data/baseevent.ts b/src/data/baseevent.ts
new file mode 100644
index 0000000..3dd0c64
--- /dev/null
+++ b/src/data/baseevent.ts
@@ -0,0 +1,65 @@
+/* Galvanic Corrosion - Rec Room custom server for communities.
+
+Copyright (C) 2025 @zombieb (Discord / proxnet Gitea)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published
+by the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see . */
+
+import Logging from "@proxnet/undead-logging";
+
+const log = new Logging("BaseEvent");
+
+export interface Event {
+ time: Date
+}
+
+export class EventManager {
+
+ private eventCallbacks: Map void>> = new Map();
+
+ private getSubSet(event: string): Set<(ev: unknown) => void> {
+ let subset = this.eventCallbacks.get(event);
+ if (!subset) {
+ subset = new Set();
+ this.eventCallbacks.set(event, subset);
+ }
+ return subset;
+ }
+
+ on(event: string, cb: (ev: T) => void) {
+ const typeSafeCallback = ((ev: unknown) => {
+ cb(ev as T);
+ });
+
+ this.getSubSet(event).add(typeSafeCallback);
+
+ return typeSafeCallback;
+ }
+
+ off(event: string, cb: (ev: unknown) => void) {
+ const subset = this.getSubSet(event);
+ subset.delete(cb);
+ }
+
+ emit(event: string, ev: T) {
+ const subset = this.getSubSet(event);
+ for (const cb of subset.values()) {
+ try {
+ cb(ev);
+ } catch (err) {
+ if (err instanceof Error) log.e(`Error when executing callback: ${err.stack}`);
+ else log.e(`Error when executing callback: ${err}`);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/data/config.ts b/src/data/config.ts
index eb3beb5..aeefb64 100644
--- a/src/data/config.ts
+++ b/src/data/config.ts
@@ -67,7 +67,7 @@ export function getConfig() {
StartsInMinutes: 0,
},
LevelProgressionMaps: generateLevelProgressionMap(),
- DailyObjectives: [],
+ DailyObjectives: [[{type: -1,score:0},{type: -1,score:0},{type: -1,score:0}],[{type: -1,score:0},{type: -1,score:0},{type: -1,score:0}],[{type: -1,score:0},{type: -1,score:0},{type: -1,score:0}]],
AutoMicMutingConfig: {
MicSpamVolumeThreshold: 1.125,
MicVolumeSampleInterval: 0.25,
diff --git a/src/data/content/data.ts b/src/data/content/data.ts
new file mode 100644
index 0000000..fecb6ec
--- /dev/null
+++ b/src/data/content/data.ts
@@ -0,0 +1,94 @@
+/* Galvanic Corrosion - Rec Room custom server for communities.
+
+Copyright (C) 2025 @zombieb (Discord / proxnet Gitea)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published
+by the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see . */
+
+import { Buffer } from "node:buffer";
+import { Redis } from "../../db.ts";
+import { generateRandomString } from "../../apiutils.ts";
+
+export enum FileType {
+ Unknown,
+ RoomSave,
+ Holotar,
+ Image,
+ Video,
+ Invention
+}
+
+export function getFileName(prefix: string, type: FileType) {
+ switch (type) {
+ case FileType.RoomSave:
+ return `${prefix}.room`;
+ case FileType.Holotar:
+ return `${prefix}.holotar`;
+ case FileType.Image:
+ return `${prefix}.image`;
+ case FileType.Video:
+ return `${prefix}.video`;
+ case FileType.Invention:
+ return `${prefix}.invention`;
+ default:
+ return `${prefix}.unknown`
+ }
+}
+
+export async function getFile(name: string) {
+ const data = await Redis.Database.getBuffer(Redis.buildKey(
+ Redis.KeyGroups.Content.Root,
+ name
+ ));
+
+ if (!data) return null;
+ else return data;
+}
+
+/**
+ * @returns Name of the new file
+ */
+export async function setFile(data: Buffer, type: FileType, name?: string) {
+ let filename = generateRandomString(24);
+ if (name) filename = name;
+ const finalName = getFileName(filename, type);
+
+ await Redis.Database.set(Redis.buildKey(
+ Redis.KeyGroups.Content.Root,
+ finalName
+ ), data);
+ await initFileMeta(filename, type);
+ return filename;
+}
+
+interface FileMeta {
+ created: Date | string;
+ fetchCount: number;
+ type: FileType
+}
+
+async function initFileMeta(filename: string, type: FileType) {
+ const meta: FileMeta = {
+ created: new Date(),
+ fetchCount: 0,
+ type: type
+ }
+ await Redis.Database.hset(Redis.buildKey(
+ Redis.KeyGroups.Content.Files,
+ getFileName(filename, type)
+ ), meta);
+}
+
+export async function incrementFileFetches(name: string, type: FileType) {
+
+}
\ No newline at end of file
diff --git a/src/data/live/instances.ts b/src/data/live/instances.ts
index d37151f..eaa60b0 100644
--- a/src/data/live/instances.ts
+++ b/src/data/live/instances.ts
@@ -20,6 +20,7 @@ import UnifiedProfile, { Profile } from "../profiles.ts";
import { RoomInstance, InstanceOptions } from "./types.ts";
import { Config } from "../../config.ts";
import Presence from "./presence.ts";
+import { RoomFactory } from "../content/rooms/RoomFactory.ts";
const log = new Logging("Instances");
@@ -177,6 +178,9 @@ class InstancesBase {
this.updateSingleInstanceIsFull(instance);
} else log.w(`Instance ${instance.roomInstanceId} is full. Cannot add player ${player.getId()}`);
+
+ const room = await new RoomFactory({ id: instance.roomId }).init();
+ await room?.addVisit();
}
playerIsInInstance(player: Profile, instance: RoomInstance) {
diff --git a/src/data/profile/reputation.ts b/src/data/profile/reputation.ts
index 46d4a1d..450eb21 100644
--- a/src/data/profile/reputation.ts
+++ b/src/data/profile/reputation.ts
@@ -19,6 +19,7 @@ import { ProfileContentManager } from "./base/profilemanagerbase.ts";
export class ProfileReputationManager extends ProfileContentManager {
+ // deno-lint-ignore require-await
async getReputation() { // async temporary
return {
AccountId: this.profileId,
diff --git a/src/data/content/images.ts b/src/data/profileevents.ts
similarity index 55%
rename from src/data/content/images.ts
rename to src/data/profileevents.ts
index c778cdc..7f8e693 100644
--- a/src/data/content/images.ts
+++ b/src/data/profileevents.ts
@@ -15,27 +15,13 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see . */
-import { Buffer } from "node:buffer";
-import { Redis } from "../../db.ts";
+import { Event } from "./baseevent.ts";
+import { Profile } from "./profiles.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),
- );
+export enum ProfileEvents {
+ BaseUpdated = "profile.updated"
}
+
+export interface ProfileUpdatedEvent extends Event {
+ profile: Profile
+}
\ No newline at end of file
diff --git a/src/data/profiles.ts b/src/data/profiles.ts
index 4d6747d..5e19046 100644
--- a/src/data/profiles.ts
+++ b/src/data/profiles.ts
@@ -31,6 +31,8 @@ import { ProfileReputationManager } from "./profile/reputation.ts";
import Logging from "@proxnet/undead-logging";
import { ProfileRelationshipManager } from "./profile/relationships.ts";
import { ProfileAvatarManager } from "./profile/avatar.ts";
+import { EventManager } from "./baseevent.ts";
+import { ProfileEvents, ProfileUpdatedEvent } from "./profileevents.ts";
const config = Config.getConfig();
@@ -53,7 +55,7 @@ interface AccountExport {
username: string;
displayName: string;
}
-interface SelfAccountExport extends AccountExport {
+export interface SelfAccountExport extends AccountExport {
email?: string,
phone?: string,
juniorState?: number,
@@ -67,7 +69,7 @@ export interface ProfileTokenFormat extends TokenBaseFormat {
const reservedIds = [1, 2];
-class Profile {
+class Profile extends EventManager {
static async exists(id: number) {
return (await Redis.Database.exists(
Redis.buildKey(
@@ -167,8 +169,7 @@ class Profile {
isJunior: values[1] == null ? false : JSON.parse(values[1]),
platforms: 1,
username: values[2] == null ? "DATABASEERROR" : values[2],
- displayName: values[3] == null ? (values[2] == null ? "DATABASEERROR" : values[2]) : values[3],
- email: "notanemail@notanemailsite.local" // we are confirmed
+ displayName: values[3] == null ? (values[2] == null ? "DATABASEERROR" : values[2]) : values[3]
});
});
}
@@ -196,6 +197,8 @@ class Profile {
Avatar: ProfileAvatarManager;
constructor(id: number) {
+ super();
+
this.#id = id;
this.Settings = new ProfileSettingsManager(this.#id);
@@ -205,6 +208,14 @@ class Profile {
this.Avatar = new ProfileAvatarManager(this.#id);
}
+ #emitProfileUpdated() {
+ const ev: ProfileUpdatedEvent = {
+ time: new Date(),
+ profile: this
+ }
+ this.emit(ProfileEvents.BaseUpdated, ev);
+ }
+
setInstance(instance: RoomInstance | null) {
this.#instance = instance;
}
@@ -229,6 +240,21 @@ class Profile {
async setBio(bio: string) {
await Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.#id.toString(), Redis.KeyGroups.Profiles.Bio), bio);
+ this.#emitProfileUpdated();
+ }
+
+ async getDisplayName() {
+ const name = await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.#id.toString(), Redis.KeyGroups.Profiles.DisplayName));
+ if (!name) return "DATABASE ERROR";
+ else return name;
+ }
+
+ async setDisplayName(name: string) {
+ await Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.#id.toString(), Redis.KeyGroups.Profiles.DisplayName), name);
+ this.#emitProfileUpdated();
+
+ const acc = await this.export();
+ if (acc) this.getSocketHandler()?.sendNotification("AccountUpdate", acc);
}
async export() {
diff --git a/src/data/steam.ts b/src/data/steam.ts
new file mode 100644
index 0000000..e2f1564
--- /dev/null
+++ b/src/data/steam.ts
@@ -0,0 +1,68 @@
+/* Galvanic Corrosion - Rec Room custom server for communities.
+
+Copyright (C) 2025 @zombieb (Discord / proxnet Gitea)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published
+by the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see . */
+
+import Logging from "@proxnet/undead-logging";
+import { Config } from "../config.ts";
+
+const log = new Logging("Steam");
+
+const config = Config.getConfig();
+
+interface AuthenticateUserTicketSuccess {
+ result: 'OK',
+ steamid: string,
+ ownersteamid: string,
+ vacbanned: boolean,
+ publisherbanned: boolean
+}
+interface AuthenticateUserTicketError {
+ errorcode: number,
+ errordesc: string
+}
+
+interface SteamRes {
+ response: {
+ error?: AuthenticateUserTicketError,
+ params?: AuthenticateUserTicketSuccess
+ }
+}
+
+export async function AuthenticateUserTicket(ticket: string, userid: string) {
+ if (!config.auth.steamkey) return true; // always authenticate if no steam API key was found
+
+ const params = new URLSearchParams();
+ params.append('key', config.auth.steamkey);
+ params.append('appid', "471710");
+ params.append('ticket', ticket);
+
+ const res = await fetch(`https://api.steampowered.com/ISteamUserAuth/AuthenticateUserTicket/v1?${params}`);
+ const resjson = (await res.json()) as SteamRes;
+
+ if (resjson.response.error) {
+ log.w(`Steam Authentication failed: (${resjson.response.error.errorcode}) ${resjson.response.error.errordesc}`);
+ return false;
+ }
+
+ log.d(JSON.stringify(resjson.response));
+ if (resjson.response.params) return resjson.response.params.steamid === userid && resjson.response.params.ownersteamid === userid;
+ else {
+ log.w("Steam Authentication failed: Steam response did not contain params or error! This should never be logged!");
+ return false;
+ }
+}
+
+export * as Steam from "./steam.ts";
\ No newline at end of file
diff --git a/src/data/users.ts b/src/data/users.ts
index 3ac7413..14d72df 100644
--- a/src/data/users.ts
+++ b/src/data/users.ts
@@ -24,6 +24,7 @@ import { TokenBaseFormat } from "../apiutils.ts";
type UserInitOptions = {
client_id: string;
pubkey: string;
+ captcha?: string;
};
export enum AuthType {
diff --git a/src/db.ts b/src/db.ts
index 2261ad4..4395d93 100644
--- a/src/db.ts
+++ b/src/db.ts
@@ -71,8 +71,7 @@ export const KeyGroups = {
},
Content: {
Root: "content",
- Images: "images",
- Rooms: "rooms",
+ Files: "file-meta",
},
Profile_Usernames: "profile-usernames",
PlatformAssociations: "platforms",
diff --git a/src/main.ts b/src/main.ts
index bd7af02..88f9b15 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -29,6 +29,7 @@ import { SocketHandoff } from "./socket/handoff.ts";
import { SignalRSocketHandler } from "./socket/socket.ts";
import Rooms from "./data/content/rooms.ts";
import { GameConfigs } from "./data/config.ts";
+import { RootPath } from "./data/content/baseimages.ts";
const instanceId = generateRandomString(64);
@@ -39,16 +40,16 @@ log.i(`Starting Galvanic Corrosion..`);
const config = Config.getConfig();
if (typeof config == "undefined") {
- log.e("Cannot start: Configuration is undefined");
- Deno.exit(1);
+ log.e("Cannot start: Configuration was not found.");
+ Deno.exit(5);
}
if (config.auth.secret == Config.defaultConfig.auth.secret) {
log.e(`Cannot start: Auth secret is default. Please change 'auth.secret' in 'config.json'`);
- Deno.exit(1);
+ Deno.exit(5);
}
if (config.public.serverId == Config.defaultConfig.public.serverId) {
log.e(`Cannot start: Server ID is default. Please change 'public.serverId' in 'config.json'`);
- Deno.exit(1);
+ Deno.exit(5);
}
Log.MessageTypeVisibility.Network = config.logging.network;
@@ -105,6 +106,8 @@ app.use(matchRouter.route.path, matchRouter.route.router);
app.use(notifyRouter.route.path, notifyRouter.route.router);
app.use(cdnRouter.route.path, cdnRouter.route.router);
+// end content routes
+
app.use((rq: express.Request, rs: express.Response) => {
log.e(`${rs.locals.reqId} ${APIUtils.getSrcIpDefault(rq)} 404 ${rq.method} ${rq.url.toString()}`);
rs.statusCode = 404;
@@ -149,14 +152,6 @@ try {
const abort = new AbortController();
- abort.signal.addEventListener('abort', () => {
- log.n("Closing all sockets");
- const sockets = UnifiedProfile.getAllSockets();
- for (const socket of sockets.values()) {
- socket.destroy(socket)();
- } // I used the socket to destroy the socket
- });
-
// Galvanic WebSocket
Deno.serve({port: config.web.socket.port, hostname: config.web.socket.host, signal: abort.signal, onListen: addr => {
log.n(`Socket listening on http://${addr.hostname}:${addr.port}`);
diff --git a/src/routes/account/account.ts b/src/routes/account/account.ts
index 367ba9c..7e293c9 100644
--- a/src/routes/account/account.ts
+++ b/src/routes/account/account.ts
@@ -103,12 +103,25 @@ route.router.get("/me",
);
-route.router.post("/me/displayname",
+interface DisplayNameUpdate {
+ displayName: string
+}
+const DisplayNameUpdateSchema = z.object({
+ displayName: z.string().max(24, "DisplayName too long!")
+})
+route.router.put("/me/displayname",
APIUtils.Authentication,
APIUtils.AuthenticationType(AuthType.Game),
+ express.urlencoded({ extended: true }),
+ APIUtils.validateRequestBody(DisplayNameUpdateSchema),
+
+ (rq: express.Request<{}, {}, DisplayNameUpdate>, rs: express.Response, nxt: express.NextFunction) => {
+ rs.locals.profile.setDisplayName(rq.body.displayName);
+ nxt();
+ },
- APIUtils.RecNetResponse(true, "DisplayName customization is not yet implemented."),
+ APIUtils.RecNetResponse(true, "Updated DisplayName.")
);
diff --git a/src/routes/auth/account.ts b/src/routes/auth/account.ts
index 5c3149c..5225e4e 100644
--- a/src/routes/auth/account.ts
+++ b/src/routes/auth/account.ts
@@ -34,7 +34,7 @@ route.router.get('/me/haspassword',
APIUtils.Authentication,
APIUtils.AuthenticationType(AuthType.Game),
- (rq, rs) => {
+ (_rq, rs) => {
rs.json(true);
},
diff --git a/src/routes/auth/connect.ts b/src/routes/auth/connect.ts
index 7973ffe..3055bba 100644
--- a/src/routes/auth/connect.ts
+++ b/src/routes/auth/connect.ts
@@ -25,6 +25,7 @@ import { z } from "zod";
import { AuthType } from "../../data/users.ts";
import { Redis } from "../../db.ts";
import { validVersions } from "../api/versioncheck.ts";
+import { Steam } from "../../data/steam.ts";
const config = Config.getConfig();
@@ -53,9 +54,18 @@ interface RefreshRequest extends AuthBodyBase {
refresh_token: string,
grant_type: "refresh_token"
}
+interface SteamPlatformParams {
+ Ticket: string,
+ AppId: string
+}
type TokenRequestBody = TokenRequest | RefreshRequest;
+const SteamPlatformParamsSchema = z.object({
+ Ticket: z.string(),
+ AppId: z.literal('471710')
+});
+
const AuthBodyBaseSchema = z.object({
grant_type: z.string(),
client_id: z.string(),
@@ -98,6 +108,7 @@ route.router.post("/token",
APIUtils.Authentication,
APIUtils.AuthenticationType(AuthType.Web),
express.urlencoded({ extended: true }),
+ APIUtils.logBody,
APIUtils.validateRequestBody(TokenRequestBodySchema),
async (
@@ -125,6 +136,7 @@ route.router.post("/token",
!(rq.body.platform_id.length > 32),
!(rq.body.time.length > 32),
!(rq.body.asid.length > 32),
+ SteamPlatformParamsSchema.safeParse(JSON.parse(rq.body.platform_auth)).success
].includes(false);
if (!conditionsMet) {
@@ -153,6 +165,16 @@ route.router.post("/token",
targetAccount = parseInt(decodedToken.sub ? decodedToken.sub : "NaN");
}
+
+ const platformAuth = (JSON.parse(rq.body.platform_auth)) as SteamPlatformParams;
+
+ if (config.auth.steamkey) {
+ const steamAuthed = await Steam.AuthenticateUserTicket(platformAuth.Ticket, rq.body.platform_id);
+ if (!steamAuthed) {
+ requestFailed();
+ return;
+ }
+ }
if (isNaN(targetAccount)) {
requestFailed();
diff --git a/src/routes/img.ts b/src/routes/img.ts
index 63c8c9b..e9ff74c 100644
--- a/src/routes/img.ts
+++ b/src/routes/img.ts
@@ -18,7 +18,6 @@ along with this program. If not, see . */
import { APIUtils, NoBody } from "../apiutils.ts";
import * as BaseImages from "../data/content/baseimages.ts";
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";
@@ -30,8 +29,6 @@ function sanitizeString(input: string) {
return input.split("").filter((char) => chars.includes(char)).join("");
}
-const baseImages = BaseImages.getAllBaseImages();
-
interface ImageQueryOptions {
cropSquare?: string;
width?: string;
@@ -50,10 +47,12 @@ route.router.get(
rq.path.substring(1, rq.path.length).replaceAll("%20", " "),
);
+ // deno-lint-ignore prefer-const
let image: Image;
- const imageSource = baseImages.includes(filename)
+ /*const imageSource = baseImages.includes(filename)
? BaseImages.getBaseImage(filename)
- : await Images.getImage(filename);
+ : await Images.getImage(filename);*/
+ const imageSource = BaseImages.getBaseImage(filename);
if (imageSource == null) {
nxt();
return;
diff --git a/src/routes/user.ts b/src/routes/user.ts
index b9ec304..090d562 100644
--- a/src/routes/user.ts
+++ b/src/routes/user.ts
@@ -129,7 +129,7 @@ route.router.post("/auth",
} else user = obj;
}
if (!(await user.addNonce(rq.body.message.nonce))) {
- log.w(`Client '${rq.body.client_id}' has already used nonce. Replay attack?`);
+ log.w(`Client '${rq.body.client_id}' has already used nonce.`);
authFailed("Authentication request failed.");
return;
}
diff --git a/src/socket/socket.ts b/src/socket/socket.ts
index 68a2291..ed5a9a4 100644
--- a/src/socket/socket.ts
+++ b/src/socket/socket.ts
@@ -34,6 +34,10 @@ import {
import { SocketTarget } from "./targets/targetbase.ts";
import { PlayerSocketSubscriptionTarget } from "./targets/SubscribeToPlayers.ts";
import Presence from "../data/live/presence.ts";
+import Instances from "../data/live/instances.ts";
+import Matchmaking from "../data/live/base.ts";
+
+const logmessages = true;
export class SignalRSocketHandler {
@@ -55,7 +59,7 @@ export class SignalRSocketHandler {
player.setSocketHandler(this);
- this.#Targets.set('SubscribeToPlayers', new PlayerSocketSubscriptionTarget());
+ this.#Targets.set('SubscribeToPlayers', new PlayerSocketSubscriptionTarget(this));
this.#PeriodicalId = setInterval(async () => {
if (this.#socket.readyState !== this.#socket.CLOSED) {
@@ -86,7 +90,7 @@ export class SignalRSocketHandler {
this.sendRaw({});
return;
} else {
- //this.#log.d(`CLIENT MESSAGE\n Type: ${message.data.type} (${SignalMessageType[message.data.type]})\n ${JSON.stringify(message.data)}`);
+ if (logmessages) this.#log.d(`CLIENT MESSAGE\n Type: ${message.data.type} (${SignalMessageType[message.data.type]})\n ${JSON.stringify(message.data)}`);
if (message.data.type == SignalMessageType.Invocation && message.data.invocationId) { // don't send completion messages for nonblocking invocations
const res = await this.#dispatchTarget(message.data.target, message.data.arguments[0]); // rec room only uses the first index
if (res.type == TargetResultType.Success) {
@@ -153,14 +157,21 @@ export class SignalRSocketHandler {
if (!internal) sock.#socket.close();
sock.#log.i(`Closed socket`);
sock.#profile.clearSocketHandler();
+
+ for (const target of sock.#Targets.values()) target.destroy();
+
+ Instances.removePlayerFromCurrentInstance(this.#profile);
+ Matchmaking.deleteLoginLock(this.#profile);
}
}
sendRaw(data: object) {
this.#socket.send(`${JSON.stringify(data)}\u001e`);
// todo sometime: make the below less confusing
- //const type = `Type: ${JSON.stringify(data) == '{}' ? 'Protocol Message' : `${(data as SignalRMessage).type} (${SignalMessageType[(data as SignalRMessage).type]})`}`;
- //this.#log.d(`SERVER MESSAGE\n ${type}\n ${JSON.stringify(data as SignalRMessage)}`);
+ if (logmessages) {
+ const type = `Type: ${JSON.stringify(data) == '{}' ? 'Protocol Message' : `${(data as SignalRMessage).type} (${SignalMessageType[(data as SignalRMessage).type]})`}`;
+ this.#log.d(`SERVER MESSAGE\n ${type}\n ${JSON.stringify(data as SignalRMessage)}`);
+ }
}
sendNotification(id: PushNotificationId | string, args: object) {
diff --git a/src/socket/targets/SubscribeToPlayers.ts b/src/socket/targets/SubscribeToPlayers.ts
index 83c00af..28777ae 100644
--- a/src/socket/targets/SubscribeToPlayers.ts
+++ b/src/socket/targets/SubscribeToPlayers.ts
@@ -17,6 +17,9 @@ along with this program. If not, see . */
import { z } from "zod";
import { SocketTarget } from "./targetbase.ts";
+import UnifiedProfile, { SelfAccountExport } from "../../data/profiles.ts";
+import { ProfileEvents, ProfileUpdatedEvent } from "../../data/profileevents.ts";
+import { PushNotificationId } from "../types.ts";
const ArgumentSchema = z.object({
PlayerIds: z.array(z.number())
@@ -24,10 +27,35 @@ const ArgumentSchema = z.object({
export class PlayerSocketSubscriptionTarget extends SocketTarget {
- subscriptions: number[] = [];
+ updateSocket(profile: SelfAccountExport) {
+ this.socket.sendNotification(PushNotificationId.SubscriptionUpdateProfile, profile);
+ }
+
+ subscriptions: { id: number, callback: (ev: unknown) => void }[] = [];
setSubscriptions(subs: number[]) {
- this.subscriptions = subs;
+
+ this.clearSubscriptions();
+
+ for (const id of subs)
+ this.subscriptions.push({ id: id, callback: UnifiedProfile.get(id)
+ .on(ProfileEvents.BaseUpdated, (async ev => {
+ const exported = await ev.profile.export();
+ if (exported) this.updateSocket(exported);
+ }
+ )) });
+
+ }
+
+ clearSubscriptions() {
+ for (const sub of this.subscriptions) {
+ const profile = UnifiedProfile.get(sub.id);
+ profile.off(ProfileEvents.BaseUpdated, sub.callback);
+ }
+ }
+
+ override destroy() {
+ this.clearSubscriptions();
}
// deno-lint-ignore require-await
diff --git a/src/socket/targets/targetbase.ts b/src/socket/targets/targetbase.ts
index 69133da..be38d8d 100644
--- a/src/socket/targets/targetbase.ts
+++ b/src/socket/targets/targetbase.ts
@@ -15,18 +15,18 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see . */
+import { SignalRSocketHandler } from "../socket.ts";
+
export class SocketTarget {
- profileNotSetError = new Error("The profile on this target is not set.");
+ socket: SignalRSocketHandler;
- profileId: number | null = null;
-
- setProfile(id: number) {
- this.profileId = id;
+ constructor(socket: SignalRSocketHandler) {
+ this.socket = socket;
}
- profileIsSet() {
- return this.profileId !== null;
+ destroy() {
+ return;
}
// deno-lint-ignore require-await