This repository has been archived on 2026-03-19. You can view files and clone it, but cannot push or open issues or pull requests.
Files
galvanic-corrosion/src/routes/auth/connect.ts
zombieb 3b6d905180 Still figuring out initial matchmaking hang (FROSTBITE). Lots of other changes.
- Added missing room images
- Removed internal rooms and disallow cloning some rooms
- License fixes
- Switched target to 20200306
- Socket header fixes
- Sockets are closed upon shutdown
    * Sockets persist after being destroyed, fix
- Config changes for 20200306
- Settings initialized
- Name generation words cannot be longer than 9 characters
- Dorm generation changes and fixes
- Added some settings keys
- Matchmaking additions
    * Instances are not yet updated to be or not to be in-progress
- Instance fixes and logging
- Image operation fixes
- DisplayName route start
- Challenge route start
- Default Amplitude key (i can see althe Amplitude requests are ignored
- Rate limiting ease
- GameConfigs properly queried and sent
- Many 'bulk' endpoints were added in or around 20200306
- Objective routes started
- DormRoom redirection in v2/name
- Client doesn't care if it gets 200 when setting prefs
- Balance/storefronts started
- Matchmaking goto/room timer and fixes
- Selfhosted Photon addition on the client sends matchmaking /photonregionpings, ignore since Photon is only one server in one region
2025-04-13 01:06:23 -04:00

194 lines
5.8 KiB
TypeScript

/* Galvanic Corrosion - Rec Room custom server for communities.
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
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 <https://www.gnu.org/licenses/>. */
import { APIUtils, NoBody } from "../../apiutils.ts";
import express from "express";
import UnifiedProfile, { Profile } from "../../data/profiles.ts";
import { decode } from "@gz/jwt";
import { Config } from "../../config.ts";
import Logging from "@proxnet/undead-logging";
import { z } from "zod";
import { AuthType } from "../../data/users.ts";
import { Redis } from "../../db.ts";
import { validVersions } from "../api/versioncheck.ts";
const config = Config.getConfig();
const log = new Logging("AuthConnectRoute");
export const route = APIUtils.createRouter("/connect");
interface AuthBodyBase {
grant_type: 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 TokenRequest extends AuthBodyBase {
account_id: string;
grant_type: "cached_login"
}
interface RefreshRequest extends AuthBodyBase {
refresh_token: string,
grant_type: "refresh_token"
}
type TokenRequestBody = TokenRequest | RefreshRequest;
const AuthBodyBaseSchema = z.object({
grant_type: z.string(),
client_id: z.string(),
client_secret: z.string(),
platform: z.string(),
platform_id: z.string(),
device_id: z.string(),
device_class: z.string(),
time: z.string(),
ver: z.string(),
asid: z.string(),
platform_auth: z.string(),
});
const TokenRequestSchema = AuthBodyBaseSchema.extend({
grant_type: z.literal('cached_login'),
account_id: z.string(),
});
const RefreshRequestSchema = AuthBodyBaseSchema.extend({
grant_type: z.literal('refresh_token'),
refresh_token: z.string(),
});
const TokenRequestBodySchema = z.discriminatedUnion('grant_type', [
TokenRequestSchema,
RefreshRequestSchema,
]);
interface TokenResponseBody {
error?: string;
error_description?: string;
access_token: string;
refresh_token: string;
}
route.router.post("/token",
APIUtils.startTimer,
APIUtils.Authentication,
APIUtils.AuthenticationType(AuthType.Web),
express.urlencoded({ extended: true }),
APIUtils.validateRequestBody<AuthBodyBase>(TokenRequestBodySchema),
async (
rq: express.Request<NoBody, NoBody, TokenRequestBody>,
rs: express.Response<TokenResponseBody>,
nxt: express.NextFunction
) => {
function requestFailed(msg: string = "invalid_request") {
rs.json({
error: msg,
access_token: "",
refresh_token: "",
});
}
const conditionsMet = ![
rq.body.client_id === "recroom",
rq.body.platform === "0",
validVersions.includes(rq.body.ver),
rq.body.device_class.length === 1,
!isNaN(Number(rq.body.device_class)),
!(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) {
requestFailed();
return;
}
let targetAccount: number;
if (rq.body.grant_type == 'cached_login') targetAccount = parseInt(rq.body.account_id);
else {
const refreshToken = rq.body.refresh_token;
if (typeof refreshToken == 'undefined') {
requestFailed();
return;
}
let decodedToken;
try {
decodedToken = await decode(rq.body.refresh_token, config.auth.secret, { algorithm: "HS512" });
} catch (err) {
log.w(`Refresh token decode failed: ${err}`);
requestFailed();
return;
}
targetAccount = parseInt(decodedToken.sub ? decodedToken.sub : "NaN");
}
if (isNaN(targetAccount)) {
requestFailed();
return;
}
const accounts = await rs.locals.user.getAssociatedProfiles();
if (!accounts.has(targetAccount)) {
requestFailed("access_denied");
return;
}
rs.locals.user.addAssociatedDeviceId(rq.body.device_id);
rs.locals.user.addAssociatedPlatformId(rq.body.platform_id);
Redis.Database.sadd(Redis.buildKey(Redis.KeyGroups.PlatformAssociations, rq.body.platform_id), targetAccount);
const profile = UnifiedProfile.get(targetAccount);
if (!(await Profile.exists(profile.getId()))) {
requestFailed();
return;
}
const details = await profile.export();
log.i(`Player ${details?.username} "${details?.displayName}" (${profile.getId()}) logged in`);
const token = await profile.getToken();
rs.json({
access_token: token,
refresh_token: token,
});
await profile.setKnownDeviceClass(Number(rq.body.device_class));
nxt();
},
APIUtils.stopTimer
);