feat: give service tokens state

This commit is contained in:
Jonathan Barrow
2026-01-04 13:14:48 -05:00
parent 685f45a342
commit dbac688cfa
6 changed files with 57 additions and 28 deletions

View File

@@ -5,7 +5,7 @@ import type { Model, HydratedDocument } from 'mongoose';
export interface IIndependentServiceToken {
token: string;
client_id: string;
title_ids: string[];
title_id: string;
pid: number;
info: {
system_type: number;
@@ -27,7 +27,7 @@ export type HydratedIndependentServiceTokenDocument = HydratedDocument<IIndepend
const IndependentServiceTokenSchema = new Schema<IIndependentServiceToken, IndependentServiceTokenModel, IIndependentServiceTokenMethods>({
token: String,
client_id: String,
title_ids: [String],
title_id: String,
pid: Number,
info: {
system_type: Number,

View File

@@ -2,7 +2,7 @@ import crypto from 'node:crypto';
import express from 'express';
import { SystemType } from '@/types/common/system-types';
import { TokenType } from '@/types/common/token-types';
import { nintendoBase64Encode, nintendoBase64Decode, nascDateTime, nascError } from '@/util';
import { nintendoBase64Encode, nintendoBase64Decode, nascDateTime, nascError, createServiceToken } from '@/util';
import { getServerByTitleID } from '@/database';
import { IndependentServiceToken } from '@/models/independent_service_token';
import { NEXToken } from '@/models/nex_token';
@@ -96,18 +96,25 @@ async function processLoginRequest(server: HydratedServerDocument, pid: number,
});
}
async function processServiceTokenRequest(_server: HydratedServerDocument, pid: number, titleID: string): Promise<URLSearchParams> {
const serviceToken = await IndependentServiceToken.create({
token: crypto.randomBytes(36).toString('base64'),
client_id: '', // * Unused in NASC
title_ids: [titleID],
async function processServiceTokenRequest(server: HydratedServerDocument, pid: number, titleID: string): Promise<URLSearchParams> {
const serviceTokenOptions = {
pid: pid,
title_id: titleID,
issued: new Date(),
expires: new Date(Date.now() + 24 * 3600 * 1000)
};
const serviceToken = await IndependentServiceToken.create({
token: nintendoBase64Encode(createServiceToken(server, serviceTokenOptions)),
client_id: server.game_server_id,
title_id: serviceTokenOptions.title_id,
pid: serviceTokenOptions.pid,
info: {
system_type: SystemType.CTR,
token_type: TokenType.IndependentService,
title_id: BigInt(parseInt(titleID, 16)),
issued: new Date(),
expires: new Date(Date.now() + 12 * 3600 * 1000) // * These don't technicaly expire, thats up to the independent server, but adding this as a suggested expiration anyway
issued: serviceTokenOptions.issued,
expires: serviceTokenOptions.expires
}
});

View File

@@ -2,7 +2,7 @@ import crypto from 'node:crypto';
import express from 'express';
import xmlbuilder from 'xmlbuilder';
import { getServerByClientID, getServerByGameServerID } from '@/database';
import { getValueFromHeaders, getValueFromQueryString } from '@/util';
import { createServiceToken, getValueFromHeaders, getValueFromQueryString } from '@/util';
import { TokenType } from '@/types/common/token-types';
import { IndependentServiceToken } from '@/models/independent_service_token';
import { NEXToken } from '@/models/nex_token';
@@ -93,17 +93,24 @@ router.get('/service_token/@me', async (request: express.Request, response: expr
return;
}
const serviceToken = await IndependentServiceToken.create({
token: crypto.randomBytes(36).toString('base64'),
client_id: clientID,
title_ids: [titleID],
const serviceTokenOptions = {
pid: pnid.pid,
title_id: titleID,
issued: new Date(),
expires: new Date(Date.now() + 24 * 3600 * 1000)
};
const serviceToken = await IndependentServiceToken.create({
token: createServiceToken(server, serviceTokenOptions).toString('base64'),
client_id: clientID,
title_id: serviceTokenOptions.title_id,
pid: serviceTokenOptions.pid,
info: {
system_type: server.device,
token_type: TokenType.IndependentService,
title_id: BigInt(parseInt(titleID, 16)),
issued: new Date(),
expires: new Date(Date.now() + 12 * 3600 * 1000) // * These don't technicaly expire, thats up to the independent server, but adding this as a suggested expiration anyway
issued: serviceTokenOptions.issued,
expires: serviceTokenOptions.expires
}
});

View File

@@ -0,0 +1,6 @@
export interface ServiceTokenOptions {
pid: number;
title_id: string;
issued: Date;
expires: Date;
}

View File

@@ -1,11 +0,0 @@
import type { SystemType } from '@/types/common/system-types';
import type { TokenType } from '@/types/common/token-types';
export interface TokenOptions {
system_type: SystemType;
token_type: TokenType;
pid: number;
access_level?: number;
title_id?: bigint;
expire_time: bigint;
}

View File

@@ -14,6 +14,8 @@ import type { ObjectCannedACL } from '@aws-sdk/client-s3';
import type { IncomingHttpHeaders } from 'node:http';
import type { IPNID, IPNIDMethods } from '@/types/mongoose/pnid';
import type { SafeQs } from '@/types/common/safe-qs';
import type { HydratedServerDocument } from '@/types/mongoose/server';
import type { ServiceTokenOptions } from '@/types/common/service-token-options';
let s3: S3;
@@ -53,6 +55,24 @@ export function nintendoBase64Encode(decoded: string | Buffer): string {
return encoded.replaceAll('+', '.').replaceAll('/', '-').replaceAll('=', '*');
}
export function createServiceToken(server: HydratedServerDocument, options: ServiceTokenOptions): Buffer {
const dataBuffer = Buffer.alloc(28);
dataBuffer.writeUInt32BE(options.pid, 0);
dataBuffer.writeBigUInt64BE(BigInt(parseInt(options.title_id, 16)), 4);
dataBuffer.writeBigUInt64BE(BigInt(options.issued.getTime()), 12);
dataBuffer.writeBigUInt64BE(BigInt(options.expires.getTime()), 20);
// * Not using AES anymore but fuck it, it's here already
// TODO - rename the AES field
const hmac = crypto.createHmac('sha256', server.aes_key).update(dataBuffer).digest();
return Buffer.concat([
dataBuffer,
hmac
]);
}
export function fullUrl(request: express.Request): string {
const protocol = request.protocol;
const host = request.host;