Skip to content

Commit

Permalink
feat: use "enum" for token system type
Browse files Browse the repository at this point in the history
  • Loading branch information
binaryoverload committed Jan 31, 2025
1 parent cad2d0e commit 169e686
Show file tree
Hide file tree
Showing 10 changed files with 72 additions and 30 deletions.
8 changes: 7 additions & 1 deletion src/models/server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Schema, model } from 'mongoose';
import uniqueValidator from 'mongoose-unique-validator';
import { IServer, IServerMethods, ServerModel } from '@/types/mongoose/server';
import type { SystemType } from '@/types/common/token';

const ServerSchema = new Schema<IServer, ServerModel, IServerMethods>({
client_id: String,
Expand All @@ -18,4 +19,9 @@ const ServerSchema = new Schema<IServer, ServerModel, IServerMethods>({

ServerSchema.plugin(uniqueValidator, { message: '{PATH} already in use.' });

export const Server = model<IServer, ServerModel>('Server', ServerSchema);
export const Server = model<IServer, ServerModel>('Server', ServerSchema);

export const serverDeviceToSystemType: Record<number, SystemType> = {
1: 'WIIU',
2: '3DS'
};
3 changes: 1 addition & 2 deletions src/services/api/routes/v1/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,7 @@ router.post('/', async (request: express.Request, response: express.Response): P
}

try {
const systemType = 0x3; // * API
const tokenGeneration = generateOAuthTokens(systemType, pnid, { refreshExpiresIn: 14 * 24 * 60 * 60 }); // * 14 days
const tokenGeneration = generateOAuthTokens('API', pnid, { refreshExpiresIn: 14 * 24 * 60 * 60 }); // * 14 days

response.json({
access_token: tokenGeneration.accessToken,
Expand Down
5 changes: 2 additions & 3 deletions src/services/api/routes/v1/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,10 +365,9 @@ router.post('/', async (request: express.Request, response: express.Response): P
}

await sendConfirmationEmail(pnid);

try {
const systemType = 0x3; // * API
const tokenGeneration = generateOAuthTokens(systemType, pnid, { refreshExpiresIn: 14 * 24 * 60 * 60 }); // * 14 days
const tokenGeneration = generateOAuthTokens('API', pnid, { refreshExpiresIn: 14 * 24 * 60 * 60 }); // * 14 days

response.json({
access_token: tokenGeneration.accessToken,
Expand Down
3 changes: 1 addition & 2 deletions src/services/grpc/api/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,7 @@ export async function login(request: LoginRequest): Promise<DeepPartial<LoginRes
}

try {
const systemType = 0x3; // * API
const tokenGeneration = generateOAuthTokens(systemType, pnid, { refreshExpiresIn: 14 * 24 * 60 * 60 }); // * 14 days
const tokenGeneration = generateOAuthTokens('API', pnid, { refreshExpiresIn: 14 * 24 * 60 * 60 }); // * 14 days

return {
accessToken: tokenGeneration.accessToken,
Expand Down
3 changes: 1 addition & 2 deletions src/services/grpc/api/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,7 @@ export async function register(request: RegisterRequest): Promise<DeepPartial<Lo
await sendConfirmationEmail(pnid);

try {
const systemType = 0x3; // * API
const tokenGeneration = generateOAuthTokens(systemType, pnid, { refreshExpiresIn: 14 * 24 * 60 * 60 }); // * 14 days
const tokenGeneration = generateOAuthTokens('API', pnid, { refreshExpiresIn: 14 * 24 * 60 * 60 }); // * 14 days

return {
accessToken: tokenGeneration.accessToken,
Expand Down
4 changes: 2 additions & 2 deletions src/services/nasc/routes/ac.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ router.post('/', async (request: express.Request, response: express.Response): P

async function processLoginRequest(server: HydratedServerDocument, pid: number, titleID: string): Promise<URLSearchParams> {
const tokenOptions: TokenOptions = {
system_type: 0x2, // * 3DS
system_type: '3DS',
token_type: 'NEX',
pid: pid,
access_level: 0,
Expand All @@ -90,7 +90,7 @@ async function processLoginRequest(server: HydratedServerDocument, pid: number,

async function processServiceTokenRequest(server: HydratedServerDocument, pid: number, titleID: string): Promise<URLSearchParams> {
const tokenOptions: TokenOptions = {
system_type: 0x2, // * 3DS
system_type: '3DS',
token_type: 'SERVICE',
pid: pid,
access_level: 0,
Expand Down
3 changes: 1 addition & 2 deletions src/services/nnas/routes/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,7 @@ router.post('/access_token/generate', deviceCertificateMiddleware, consoleStatus
}

try {
const systemType = 0x1; // * WiiU
const tokenGeneration = generateOAuthTokens(systemType, pnid);
const tokenGeneration = generateOAuthTokens('WIIU', pnid);

response.send(xmlbuilder.create({
OAuth20: {
Expand Down
29 changes: 27 additions & 2 deletions src/services/nnas/routes/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { getServerByClientID, getServerByGameServerID } from '@/database';
import { generateToken, getValueFromHeaders, getValueFromQueryString } from '@/util';
import { NEXAccount } from '@/models/nex-account';
import { TokenOptions } from '@/types/common/token';
import { serverDeviceToSystemType } from '@/models/server';

const router = express.Router();

Expand Down Expand Up @@ -90,8 +91,20 @@ router.get('/service_token/@me', async (request: express.Request, response: expr
return;
}

const systemType = serverDeviceToSystemType[server.device];
if (!systemType) {
response.send(xmlbuilder.create({
errors: {
error: {
code: '1021',
message: 'The requested game server was not found'
}
}
}).end());
}

const tokenOptions: TokenOptions = {
system_type: server.device,
system_type: systemType,
token_type: 'SERVICE',
pid: pnid.pid,
access_level: pnid.access_level,
Expand Down Expand Up @@ -213,8 +226,20 @@ router.get('/nex_token/@me', async (request: express.Request, response: express.
return;
}

const systemType = serverDeviceToSystemType[server.device];
if (!systemType) {
response.send(xmlbuilder.create({
errors: {
error: {
code: '1021',
message: 'The requested game server was not found'
}
}
}).end());
}

const tokenOptions: TokenOptions = {
system_type: server.device,
system_type: systemType,
token_type: 'NEX',
pid: pnid.pid,
access_level: pnid.access_level,
Expand Down
21 changes: 17 additions & 4 deletions src/types/common/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,28 @@ export const TokenTypes = {
SERVICE: 4,
PASSWORD_RESET: 5
} as const;
export type TokenType = keyof typeof TokenTypes;

export function getTokenTypeFromValue(type: number): keyof typeof TokenTypes | undefined {
const keys = Object.keys(TokenTypes) as (keyof typeof TokenTypes)[];
export function getTokenTypeFromValue(type: number): TokenType | undefined {
const keys = Object.keys(TokenTypes) as TokenType[];
return keys.find((key) => TokenTypes[key] === type);
}

export const SystemTypes = {
'WIIU': 1,
'3DS': 2,
'API': 3
} as const;
export type SystemType = keyof typeof SystemTypes;

export function getSystemTypeFromValue(type: number): SystemType | undefined {
const keys = Object.keys(SystemTypes) as SystemType[];
return keys.find((key) => SystemTypes[key] === type);
}

export interface Token {
system_type: number;
token_type: keyof typeof TokenTypes;
system_type: SystemType;
token_type: TokenType;
pid: number;
access_level?: number;
title_id?: bigint;
Expand Down
23 changes: 13 additions & 10 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import crc32 from 'buffer-crc32';
import crc from 'crc';
import { sendMail } from '@/mailer';
import { config, disabledFeatures } from '@/config-manager';
import { getTokenTypeFromValue, OAuthTokenGenerationResponse, OAuthTokenOptions, Token, TokenOptions, TokenTypes } from '@/types/common/token';
import { getSystemTypeFromValue, getTokenTypeFromValue, OAuthTokenGenerationResponse, OAuthTokenOptions, SystemType, SystemTypes, Token, TokenOptions, TokenTypes } from '@/types/common/token';
import { HydratedPNIDDocument, IPNID, IPNIDMethods } from '@/types/mongoose/pnid';
import { SafeQs } from '@/types/common/safe-qs';

Expand Down Expand Up @@ -52,7 +52,7 @@ export function nintendoBase64Encode(decoded: string | Buffer): string {
return encoded.replaceAll('+', '.').replaceAll('/', '-').replaceAll('=', '*');
}

export function generateOAuthTokens(systemType: number, pnid: HydratedPNIDDocument, options?: OAuthTokenOptions): OAuthTokenGenerationResponse {
export function generateOAuthTokens(systemType: SystemType, pnid: HydratedPNIDDocument, options?: OAuthTokenOptions): OAuthTokenGenerationResponse {
const accessTokenExpiresInSecs = options?.accessExpiresIn ?? 60 * 60; // * 1 hour
const refreshTokenExpiresInSecs = options?.refreshExpiresIn ?? 24 * 60 * 60; // * 24 hours

Expand Down Expand Up @@ -92,14 +92,15 @@ export function generateOAuthTokens(systemType: number, pnid: HydratedPNIDDocume
export function generateToken(key: string, options: TokenOptions): Buffer | null {
let dataBuffer = Buffer.alloc(1 + 1 + 4 + 8);

const systemType = SystemTypes[options.system_type];
const tokenType = TokenTypes[options.token_type];

dataBuffer.writeUInt8(options.system_type, 0x0);
dataBuffer.writeUInt8(systemType, 0x0);
dataBuffer.writeUInt8(tokenType, 0x1);
dataBuffer.writeUInt32LE(options.pid, 0x2);
dataBuffer.writeBigUInt64LE(options.expire_time, 0x6);

if ((options.token_type !== 'OAUTH_ACCESS' && options.token_type !== 'OAUTH_REFRESH') || options.system_type === 0x3) {
if ((options.token_type !== 'OAUTH_ACCESS' && options.token_type !== 'OAUTH_REFRESH') || options.system_type === 'API') {
// * Access and refresh tokens have smaller bodies due to size constraints
// * The API does not have this restraint, however
if (options.title_id === undefined || options.access_level === undefined) {
Expand All @@ -125,7 +126,7 @@ export function generateToken(key: string, options: TokenOptions): Buffer | null

let final = encrypted;

if ((options.token_type !== 'OAUTH_ACCESS' && options.token_type !== 'OAUTH_REFRESH') || options.system_type === 0x3) {
if ((options.token_type !== 'OAUTH_ACCESS' && options.token_type !== 'OAUTH_REFRESH') || options.system_type === 'API') {
// * Access and refresh tokens don't get a checksum due to size constraints
const checksum = crc32(dataBuffer);

Expand Down Expand Up @@ -170,15 +171,17 @@ export function decryptToken(token: Buffer, key?: string): Buffer {
}

export function unpackToken(token: Buffer): Token {
const systemTypeNum = token.readUInt8(0x0);
const tokenTypeNum = token.readUInt8(0x1);

const systemType = getSystemTypeFromValue(systemTypeNum);
const tokenType = getTokenTypeFromValue(tokenTypeNum);

if (!tokenType) {
throw new Error('Invalid token type');
}
if (!systemType) throw new Error('Invalid system type');
if (!tokenType) throw new Error('Invalid token type');

const unpacked: Token = {
system_type: token.readUInt8(0x0),
system_type: systemType,
token_type: tokenType,
pid: token.readUInt32LE(0x2),
expire_time: token.readBigUInt64LE(0x6)
Expand Down Expand Up @@ -271,7 +274,7 @@ export async function sendEmailConfirmedEmail(pnid: mongoose.HydratedDocument<IP

export async function sendForgotPasswordEmail(pnid: mongoose.HydratedDocument<IPNID, IPNIDMethods>): Promise<void> {
const tokenOptions: TokenOptions = {
system_type: 0xF, // * API
system_type: 'API',
token_type: 'PASSWORD_RESET',
pid: pnid.pid,
access_level: pnid.access_level,
Expand Down

0 comments on commit 169e686

Please sign in to comment.