128 lines
5.5 KiB
TypeScript
128 lines
5.5 KiB
TypeScript
import { createHonoRoute } from "../../../util/import.ts";
|
|
import z from "zod";
|
|
import { transformCheckEnum, typedZValidator } from "../../../util/validators.ts";
|
|
import { DeviceClass, PlatformType, TokenFormat, TokenType } from "../../../server/platforms/types.ts";
|
|
import { steamAuthTicketSchema } from "../../../server/platforms/base.ts";
|
|
import { gameVerString } from "../../api/routes/versioncheck.ts";
|
|
import Steam from "../../../util/steam/steam.ts";
|
|
import { SteamAuthResult } from "../../../util/steam/SteamAuthTypes.ts";
|
|
import Server from "../../../server/server.ts";
|
|
import Logging from "@proxnet/undead-logging";
|
|
import { verify } from "@hono/hono/jwt";
|
|
import { env } from "../../../env.ts";
|
|
|
|
const log = new Logging("ConnectRouteDebug");
|
|
|
|
export const route = createHonoRoute("/connect");
|
|
|
|
const authBodyBaseSchema = z.object({
|
|
client_id: z.literal("recroom"),
|
|
platform: z.coerce.number().transform((arg, ctx) => { // we only support steam right now
|
|
if (arg !== PlatformType.Steam) ctx.addIssue("platform was not Steam");
|
|
else return PlatformType.Steam;
|
|
}),
|
|
platform_id: z.string().min(4),
|
|
device_id: z.string().min(4),
|
|
device_class: z.coerce.number().transform(transformCheckEnum<DeviceClass>(DeviceClass)),
|
|
time: z.coerce.date(),
|
|
ver: z.literal(gameVerString),
|
|
asid: z.coerce.number(),
|
|
platform_auth: z.string().transform((arg, ctx) => {
|
|
try {
|
|
const parsed = steamAuthTicketSchema.safeParse(JSON.parse(arg))
|
|
if (parsed.success) return parsed.data.Ticket;
|
|
else ctx.addIssue("Steam Auth Ticket could not be parsed")
|
|
} catch {
|
|
ctx.addIssue("Steam Auth Ticket could not be parsed");
|
|
}
|
|
}),
|
|
"x-patch-plugin-hash": z.string()
|
|
});
|
|
|
|
const cachedLoginGrantSchema = authBodyBaseSchema.extend({
|
|
grant_type: z.literal('cached_login'),
|
|
account_id: z.coerce.number(),
|
|
});
|
|
const refreshTokenGrantSchema = authBodyBaseSchema.extend({
|
|
grant_type: z.literal('refresh_token'),
|
|
refresh_token: z.string(),
|
|
});
|
|
|
|
const tokenGrantSchema = z.discriminatedUnion('grant_type', [
|
|
cachedLoginGrantSchema,
|
|
refreshTokenGrantSchema
|
|
]);
|
|
|
|
enum TokenRequestError {
|
|
InvalidRequest = "invalid_request",
|
|
InvalidClient = "invalid_client",
|
|
InvalidGrant = "invalid_grant",
|
|
UnauthorizedClient = "unauthorized_client",
|
|
UnsupportedGrantType = "unsupported_grant_type",
|
|
UnsupportedResponseType = "unsupported_response_type",
|
|
InvalidScope = "invalid_scope",
|
|
AuthorizationPending = "authorization_pending",
|
|
AccessDenied = "access_denied",
|
|
SlowDown = "slow_down",
|
|
ExpiredToken = "expired_token"
|
|
}
|
|
enum TokenRequestErrorDescriptions {
|
|
InvalidUsernameOrPassword = "invalid_username_or_password",
|
|
InvalidTime = "invalid time",
|
|
PlatformVerificationFailed = "platform verification failed",
|
|
InvalidPlatform = "invalid platform",
|
|
InvalidDeviceClass = "invalid device class"
|
|
}
|
|
|
|
route.app.post('/token', typedZValidator('form', tokenGrantSchema), async c => {
|
|
function error(error?: TokenRequestError, desc?: string) {
|
|
return c.json({ error, error_description: desc });
|
|
}
|
|
|
|
const form = c.req.valid('form');
|
|
if (typeof form.platform_auth == 'undefined' || typeof form.platform == 'undefined') return error(TokenRequestError.AccessDenied);
|
|
if (typeof form.device_class !== 'number') return error(TokenRequestError.AccessDenied);
|
|
|
|
const { valid } = await Steam.AuthenticateUserTicket(form.platform_auth, form.platform_id);
|
|
if (valid == SteamAuthResult.Failure) return error(TokenRequestError.AccessDenied, TokenRequestErrorDescriptions.PlatformVerificationFailed);
|
|
|
|
if (Math.abs(Date.now() - new Date(form.time).getTime()) > 3_600_000) return error(TokenRequestError.AccessDenied, TokenRequestErrorDescriptions.InvalidTime);
|
|
|
|
const logins = await Server.Platforms.getCachedLogins(form.platform, form.platform_id, false);
|
|
if (form.grant_type == 'refresh_token') {
|
|
const secret = env["SECRET"];
|
|
if (!secret) {
|
|
log.w(`Secret not set!`);
|
|
return error(TokenRequestError.InvalidRequest);
|
|
}
|
|
try {
|
|
const token = JSON.parse(JSON.stringify(await verify(form.refresh_token, secret))) as TokenFormat;
|
|
|
|
const profile = await Server.Profiles.get(token.sub);
|
|
if (!profile) return error(TokenRequestError.AccessDenied);
|
|
|
|
await profile.Matchmaking.setLastDeviceClass(form.device_class!);
|
|
|
|
return c.json({
|
|
access_token: await Server.Platforms.getToken(profile, TokenType.Access),
|
|
refresh_token: await Server.Platforms.getToken(profile, TokenType.Refresh),
|
|
});
|
|
} catch (err) {
|
|
log.w(`Authentication error (token req): ${(err as Error).stack}`);
|
|
return error(TokenRequestError.InvalidClient);
|
|
}
|
|
}
|
|
|
|
if (logins.find(login => login.accountId === form.account_id)) {
|
|
const profile = await Server.Profiles.get(form.account_id);
|
|
if (!profile) return error(TokenRequestError.InvalidRequest, "No such profile");
|
|
await Server.Platforms.updateLastLoginTime(form.platform, form.platform_id, form.account_id);
|
|
await profile.Matchmaking.setLastDeviceClass(form.device_class!);
|
|
|
|
log.d(`Patch hash: ${form["x-patch-plugin-hash"]}`);
|
|
return c.json({
|
|
access_token: await Server.Platforms.getToken(profile, TokenType.Access),
|
|
refresh_token: await Server.Platforms.getToken(profile, TokenType.Refresh),
|
|
});
|
|
} else return error(TokenRequestError.InvalidRequest, "No such profile");
|
|
}); |