Fix f parameter generation

This fixes `f` parameter generation with nxapi's custom server. This will also work with imink/flapg *if the system time exactly matches the imink/flapg Android device's time*. This commit also makes nxapi's API compatible with imink's API and adds some information about the Android device and NSO app to response headers.
This commit is contained in:
Samuel Elliott
2022-08-23 16:38:08 +01:00
parent 7a37d7f7df
commit 428f7131a1
4 changed files with 259 additions and 97 deletions

View File

@@ -60,7 +60,7 @@ export interface AccountLoginParameter {
naBirthday: string;
naCountry: string;
language: string;
timestamp: string;
timestamp: number;
requestId: string;
f: string;
}
@@ -84,7 +84,7 @@ export type AccountToken = AccountLogin;
export interface AccountTokenParameter {
naIdToken: string;
naBirthday: string;
timestamp: string;
timestamp: number;
requestId: string;
f: string;
}
@@ -188,6 +188,20 @@ export interface WebServiceAttribute {
attrKey: string;
}
export interface WebServiceTokenParameter {
id: number;
registrationToken: string;
timestamp: number;
requestId: string;
f: string;
}
/** /v2/Game/GetWebServiceToken */
export interface WebServiceToken {
accessToken: string;
expiresIn: number;
}
/** /v1/Event/GetActiveEvent */
export type GetActiveEventResult = ActiveEvent | {};
@@ -281,9 +295,3 @@ export interface UpdateCurrentUserPermissionsParameter {
};
etag: string;
}
/** /v2/Game/GetWebServiceToken */
export interface WebServiceToken {
accessToken: string;
expiresIn: number;
}

View File

@@ -1,7 +1,7 @@
import fetch, { Response } from 'node-fetch';
import { v4 as uuidgen } from 'uuid';
import createDebug from 'debug';
import { f, FlapgIid, FResult } from './f.js';
import { f, FResult } from './f.js';
import { AccountLogin, AccountToken, Announcements, CurrentUser, CurrentUserPermissions, Event, Friends, GetActiveEventResult, PresencePermissions, User, WebServices, WebServiceToken, CoralErrorResponse, CoralResponse, CoralStatus, CoralSuccessResponse, FriendCodeUser, FriendCodeUrl, AccountTokenParameter, AccountLoginParameter } from './coral-types.js';
import { getNintendoAccountToken, getNintendoAccountUser, NintendoAccountToken, NintendoAccountUser } from './na.js';
import { ErrorResponse } from './util.js';
@@ -178,17 +178,14 @@ export default class CoralApi {
}
async getWebServiceToken(id: string) {
const uuid = uuidgen();
const timestamp = '' + Math.floor(Date.now() / 1000);
const data = await f(this.token, timestamp, uuid, FlapgIid.APP, this.useragent ?? getAdditionalUserAgents());
const data = await f(this.token, '2', this.useragent ?? getAdditionalUserAgents());
const req = {
id,
registrationToken: this.token,
registrationToken: '',
f: data.f,
requestId: uuid,
timestamp,
requestId: data.request_id,
timestamp: data.timestamp,
};
return this.call<WebServiceToken>('/v2/Game/GetWebServiceToken', req);
@@ -198,27 +195,19 @@ export default class CoralApi {
// Nintendo Account token
const nintendoAccountToken = await getNintendoAccountToken(token, ZNCA_CLIENT_ID);
const uuid = uuidgen();
const timestamp = '' + Math.floor(Date.now() / 1000);
const fdata = await f(
nintendoAccountToken.id_token, timestamp, uuid,
FlapgIid.NSO, this.useragent ?? getAdditionalUserAgents()
);
const fdata = await f(nintendoAccountToken.id_token, '1', this.useragent ?? getAdditionalUserAgents());
const req: AccountTokenParameter = {
naBirthday: user.birthday,
timestamp,
timestamp: fdata.timestamp,
f: fdata.f,
requestId: uuid,
requestId: fdata.request_id,
naIdToken: nintendoAccountToken.id_token,
};
const data = await this.call<AccountToken>('/v3/Account/GetToken', req, false);
return {
uuid,
timestamp,
nintendoAccountToken,
// user,
f: fdata,
@@ -261,13 +250,7 @@ export default class CoralApi {
// Nintendo Account user data
const user = await getNintendoAccountUser(nintendoAccountToken);
const uuid = uuidgen();
const timestamp = '' + Math.floor(Date.now() / 1000);
const fdata = await f(
nintendoAccountToken.id_token, timestamp, uuid,
FlapgIid.NSO, useragent
);
const fdata = await f(nintendoAccountToken.id_token, '1', useragent);
debug('Getting Nintendo Switch Online app token');
@@ -276,8 +259,8 @@ export default class CoralApi {
naBirthday: user.birthday,
naCountry: user.country,
language: user.language,
timestamp,
requestId: uuid,
timestamp: fdata.timestamp,
requestId: fdata.request_id,
f: fdata.f,
};
@@ -314,8 +297,6 @@ export default class CoralApi {
debug('Got Nintendo Switch Online app token', data);
return {
uuid,
timestamp,
nintendoAccountToken,
user,
f: fdata,
@@ -328,8 +309,6 @@ export default class CoralApi {
}
export interface CoralAuthData {
uuid: string;
timestamp: string;
nintendoAccountToken: NintendoAccountToken;
user: NintendoAccountUser;
f: FResult;
@@ -340,7 +319,7 @@ export interface CoralAuthData {
}
export type PartialCoralAuthData =
Pick<CoralAuthData, 'uuid' | 'timestamp' | 'nintendoAccountToken' | 'f' | 'nsoAccount' | 'credential'>;
Pick<CoralAuthData, 'nintendoAccountToken' | 'f' | 'nsoAccount' | 'credential'>;
export interface CoralJwtPayload extends JwtPayload {
isChildRestricted: boolean;

View File

@@ -1,6 +1,7 @@
import process from 'node:process';
import fetch from 'node-fetch';
import createDebug from 'debug';
import { v4 as uuidgen } from 'uuid';
import { ErrorResponse } from './util.js';
import { timeoutSignal } from '../util/misc.js';
import { getUserAgent } from '../util/useragent.js';
@@ -15,7 +16,7 @@ export abstract class ZncaApi {
public useragent?: string
) {}
abstract genf(token: string, timestamp: string, uuid: string, type: FlapgIid): Promise<FResult>;
abstract genf(token: string, hash_method: '1' | '2'): Promise<FResult>;
}
//
@@ -123,12 +124,16 @@ export class ZncaApiFlapg extends ZncaApi {
return getLoginHash(id_token, timestamp, this.useragent);
}
async genf(token: string, timestamp: string, uuid: string, type: FlapgIid) {
const result = await flapg(token, timestamp, uuid, type, this.useragent);
async genf(token: string, hash_method: '1' | '2') {
const timestamp = Date.now();
const request_id = uuidgen();
const type = hash_method === '2' ? FlapgIid.APP : FlapgIid.NSO;
const result = await flapg(token, timestamp, request_id, type, this.useragent);
return {
provider: 'flapg' as const,
token, timestamp, uuid, type,
token, timestamp, request_id, hash_method, type,
f: result.result.f,
result,
};
@@ -198,12 +203,15 @@ export interface IminkFError {
}
export class ZncaApiImink extends ZncaApi {
async genf(token: string, timestamp: string, uuid: string, type: FlapgIid) {
const result = await iminkf(token, timestamp, uuid, type === FlapgIid.APP ? '2' : '1', this.useragent);
async genf(token: string, hash_method: '1' | '2') {
const timestamp = Date.now();
const request_id = uuidgen();
const result = await iminkf(token, timestamp, request_id, hash_method, this.useragent);
return {
provider: 'imink' as const,
token, timestamp, uuid, type,
token, timestamp, request_id, hash_method,
f: result.f,
result,
};
@@ -215,18 +223,19 @@ export class ZncaApiImink extends ZncaApi {
//
export async function genf(
url: string, token: string, timestamp: string | number, uuid: string, type: FlapgIid,
url: string, hash_method: '1' | '2',
token: string, timestamp?: number, request_id?: string,
useragent?: string
) {
debugZncaApi('Getting f parameter', {
url, token, timestamp, uuid,
url, token, timestamp, request_id,
});
const req: AndroidZncaFRequest = {
type,
hash_method,
token,
timestamp: '' + timestamp,
uuid,
timestamp,
request_id,
};
const [signal, cancel] = timeoutSignal();
@@ -251,19 +260,21 @@ export async function genf(
throw new ErrorResponse('[znca-api] ' + data.error, response, data);
}
debugZncaApi('Got f parameter "%s"', data.f);
debugZncaApi('Got f parameter', data, response.headers);
return data;
}
export interface AndroidZncaFRequest {
type: FlapgIid;
hash_method: '1' | '2';
token: string;
timestamp: string;
uuid: string;
timestamp?: string | number;
request_id?: string;
}
export interface AndroidZncaFResponse {
f: string;
timestamp?: number;
request_id?: string;
}
export interface AndroidZncaFError {
error: string;
@@ -274,38 +285,39 @@ export class ZncaApiNxapi extends ZncaApi {
super(useragent);
}
async genf(token: string, timestamp: string, uuid: string, type: FlapgIid) {
const result = await genf(this.url + '/f', token, timestamp, uuid, type, this.useragent);
async genf(token: string, hash_method: '1' | '2') {
const result = await genf(this.url + '/f', hash_method, token, undefined, undefined, this.useragent);
return {
provider: 'nxapi' as const,
url: this.url + '/f',
token, timestamp, uuid, type,
token,
timestamp: result.timestamp!, // will be included as not sent in request
request_id: result.request_id!,
hash_method,
f: result.f,
result,
};
}
}
export async function f(
token: string, timestamp: string | number, uuid: string, type: FlapgIid,
useragent?: string
): Promise<FResult> {
export async function f(token: string, hash_method: '1' | '2', useragent?: string): Promise<FResult> {
const provider = getPreferredZncaApiFromEnvironment(useragent) ?? await getDefaultZncaApi(useragent);
return provider.genf(token, '' + timestamp, uuid, type);
return provider.genf(token, hash_method);
}
export type FResult = {
provider: string;
token: string;
timestamp: string;
uuid: string;
type: FlapgIid;
timestamp: number;
request_id: string;
hash_method: '1' | '2';
f: string;
result: unknown;
} & ({
provider: 'flapg';
type: FlapgIid;
result: FlapgApiResponse;
} | {
provider: 'imink';