Update config URL and use config data

This commit is contained in:
Samuel Elliott
2022-07-29 17:59:17 +01:00
parent e8e5a2f5a4
commit 2d52f17022
14 changed files with 227 additions and 94 deletions

View File

@@ -55,6 +55,16 @@ export enum CoralStatus {
export type CoralResponse<T = unknown> = CoralSuccessResponse<T> | CoralErrorResponse;
export interface AccountLoginParameter {
naIdToken: string;
naBirthday: string;
naCountry: string;
language: string;
timestamp: string;
requestId: string;
f: string;
}
/** /v3/Account/Login */
export interface AccountLogin {
user: CurrentUser;
@@ -71,6 +81,14 @@ export interface AccountLogin {
/** /v3/Account/GetToken */
export type AccountToken = AccountLogin;
export interface AccountTokenParameter {
naIdToken: string;
naBirthday: string;
timestamp: string;
requestId: string;
f: string;
}
/** /v1/Announcement/List */
export type Announcements = Announcement[];
@@ -254,6 +272,16 @@ export interface CurrentUserPermissions {
};
}
export interface UpdateCurrentUserPermissionsParameter {
permissions: {
presence: {
toValue: PresencePermissions;
fromValue: PresencePermissions;
};
};
etag: string;
}
/** /v2/Game/GetWebServiceToken */
export interface WebServiceToken {
accessToken: string;

View File

@@ -1,9 +1,9 @@
import fetch, { Response } from 'node-fetch';
import { v4 as uuidgen } from 'uuid';
import createDebug from 'debug';
import { f, FlapgIid } from './f.js';
import { AccountLogin, AccountToken, Announcements, CurrentUser, CurrentUserPermissions, Event, Friends, GetActiveEventResult, PresencePermissions, User, WebServices, WebServiceToken, CoralErrorResponse, CoralResponse, CoralStatus, CoralSuccessResponse, FriendCodeUser, FriendCodeUrl } from './coral-types.js';
import { getNintendoAccountToken, getNintendoAccountUser, NintendoAccountUser } from './na.js';
import { f, FlapgIid, 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';
import { JwtPayload } from '../util/jwt.js';
import { getAdditionalUserAgents } from '../util/useragent.js';
@@ -26,9 +26,11 @@ export default class CoralApi {
/** @internal */
_renewToken: Promise<void> | null = null;
constructor(
protected constructor(
public token: string,
public useragent: string | null = getAdditionalUserAgents()
public useragent: string | null = getAdditionalUserAgents(),
readonly znca_version = ZNCA_VERSION,
readonly znca_useragent = ZNCA_USER_AGENT,
) {}
async fetch<T = unknown>(
@@ -44,10 +46,10 @@ export default class CoralApi {
method: method,
headers: Object.assign({
'X-Platform': ZNCA_PLATFORM,
'X-ProductVersion': ZNCA_VERSION,
'X-ProductVersion': this.znca_version,
'Authorization': 'Bearer ' + this.token,
'Content-Type': 'application/json; charset=utf-8',
'User-Agent': ZNCA_USER_AGENT,
'User-Agent': this.znca_useragent,
}, headers),
body: body,
});
@@ -185,22 +187,24 @@ export default class CoralApi {
return this.call<WebServiceToken>('/v2/Game/GetWebServiceToken', req);
}
async getToken(token: string, user: NintendoAccountUser) {
const uuid = uuidgen();
const timestamp = '' + Math.floor(Date.now() / 1000);
async getToken(token: string, user: NintendoAccountUser): Promise<PartialCoralAuthData> {
// Nintendo Account token
const nintendoAccountToken = await getNintendoAccountToken(token, ZNCA_CLIENT_ID);
const id_token = nintendoAccountToken.id_token;
const fdata = await f(id_token, timestamp, uuid, FlapgIid.NSO, this.useragent ?? getAdditionalUserAgents());
const uuid = uuidgen();
const timestamp = '' + Math.floor(Date.now() / 1000);
const req = {
const fdata = await f(
nintendoAccountToken.id_token, timestamp, uuid,
FlapgIid.NSO, this.useragent ?? getAdditionalUserAgents()
);
const req: AccountTokenParameter = {
naBirthday: user.birthday,
timestamp,
f: fdata.f,
requestId: uuid,
naIdToken: id_token,
naIdToken: nintendoAccountToken.id_token,
};
const data = await this.call<AccountToken>('/v3/Account/GetToken', req, false);
@@ -216,15 +220,6 @@ export default class CoralApi {
};
}
static async createWithSessionToken(token: string, useragent = getAdditionalUserAgents()) {
const data = await this.loginWithSessionToken(token, useragent);
return {
nso: new this(data.credential.accessToken, useragent),
data,
};
}
async renewToken(token: string, user: NintendoAccountUser) {
const data = await this.getToken(token, user);
@@ -233,9 +228,25 @@ export default class CoralApi {
return data;
}
static async loginWithSessionToken(token: string, useragent = getAdditionalUserAgents()) {
const uuid = uuidgen();
const timestamp = '' + Math.floor(Date.now() / 1000);
static async createWithSessionToken(token: string, useragent = getAdditionalUserAgents()) {
const data = await this.loginWithSessionToken(token, useragent);
return {nso: this.createWithSavedToken(data, useragent), data};
}
static createWithSavedToken(data: CoralAuthData, useragent = getAdditionalUserAgents()) {
return new this(
data.credential.accessToken,
useragent,
data.znca_version,
data.znca_useragent,
);
}
static async loginWithSessionToken(token: string, useragent = getAdditionalUserAgents()): Promise<CoralAuthData> {
const { default: { coral: config } } = await import('../common/remote-config.js');
if (!config) throw new Error('Remote configuration prevents Coral authentication');
const znca_useragent = `com.nintendo.znca/${config.znca_version}(${ZNCA_PLATFORM}/${ZNCA_PLATFORM_VERSION})`;
// Nintendo Account token
const nintendoAccountToken = await getNintendoAccountToken(token, ZNCA_CLIENT_ID);
@@ -243,29 +254,36 @@ export default class CoralApi {
// Nintendo Account user data
const user = await getNintendoAccountUser(nintendoAccountToken);
const id_token = nintendoAccountToken.id_token;
const fdata = await f(id_token, timestamp, uuid, FlapgIid.NSO, useragent);
const uuid = uuidgen();
const timestamp = '' + Math.floor(Date.now() / 1000);
const fdata = await f(
nintendoAccountToken.id_token, timestamp, uuid,
FlapgIid.NSO, useragent
);
debug('Getting Nintendo Switch Online app token');
const parameter: AccountLoginParameter = {
naIdToken: nintendoAccountToken.id_token,
naBirthday: user.birthday,
naCountry: user.country,
language: user.language,
timestamp,
requestId: uuid,
f: fdata.f,
};
const response = await fetch(ZNC_URL + '/v3/Account/Login', {
method: 'POST',
headers: {
'X-Platform': ZNCA_PLATFORM,
'X-ProductVersion': ZNCA_VERSION,
'X-ProductVersion': config.znca_version,
'Content-Type': 'application/json; charset=utf-8',
'User-Agent': ZNCA_USER_AGENT,
'User-Agent': znca_useragent,
},
body: JSON.stringify({
parameter: {
naIdToken: nintendoAccountToken.id_token,
naBirthday: user.birthday,
naCountry: user.country,
language: user.language,
timestamp,
requestId: uuid,
f: fdata.f,
},
parameter,
}),
});
@@ -289,10 +307,27 @@ export default class CoralApi {
f: fdata,
nsoAccount: data.result,
credential: data.result.webApiServerCredential,
znca_version: config.znca_version,
znca_useragent,
};
}
}
export interface CoralAuthData {
uuid: string;
timestamp: string;
nintendoAccountToken: NintendoAccountToken;
user: NintendoAccountUser;
f: FResult;
nsoAccount: AccountLogin;
credential: AccountLogin['webApiServerCredential'];
znca_version: string;
znca_useragent: string;
}
export type PartialCoralAuthData =
Pick<CoralAuthData, 'uuid' | 'timestamp' | 'nintendoAccountToken' | 'f' | 'nsoAccount' | 'credential'>;
export interface CoralJwtPayload extends JwtPayload {
isChildRestricted: boolean;
membership: {

View File

@@ -23,6 +23,9 @@ export abstract class ZncaApi {
//
export async function getLoginHash(token: string, timestamp: string | number, useragent?: string) {
const { default: { coral_auth: { splatnet2statink: config } } } = await import('../common/remote-config.js');
if (!config) throw new Error('Remote configuration prevents splatnet2statink API use');
debugS2s('Getting login hash');
const [signal, cancel] = timeoutSignal();
@@ -61,6 +64,9 @@ export async function flapg(
token: string, timestamp: string | number, guid: string, iid: FlapgIid,
useragent?: string
) {
const { default: { coral_auth: { flapg: config } } } = await import('../common/remote-config.js');
if (!config) throw new Error('Remote configuration prevents flapg API use');
const hash = await getLoginHash(token, timestamp, useragent);
debugFlapg('Getting f parameter', {
@@ -129,6 +135,9 @@ export async function iminkf(
token: string, timestamp: string | number, uuid: string, hash_method: '1' | '2',
useragent?: string
) {
const { default: { coral_auth: { imink: config } } } = await import('../common/remote-config.js');
if (!config) throw new Error('Remote configuration prevents imink API use');
debugImink('Getting f parameter', {
token, timestamp, uuid, hash_method,
});
@@ -266,7 +275,7 @@ export async function f(
token: string, timestamp: string | number, uuid: string, type: FlapgIid,
useragent?: string
): Promise<FResult> {
const provider = getZncaApiFromEnvironment(useragent);
const provider = getPreferredZncaApiFromEnvironment(useragent) ?? await getDefaultZncaApi(useragent);
return provider.genf(token, '' + timestamp, uuid, type);
}
@@ -291,7 +300,7 @@ export type FResult = {
result: AndroidZncaFResponse;
});
export function getZncaApiFromEnvironment(useragent?: string): ZncaApi {
export function getPreferredZncaApiFromEnvironment(useragent?: string): ZncaApi | null {
if (process.env.NXAPI_ZNCA_API) {
if (process.env.NXAPI_ZNCA_API === 'flapg') {
return new ZncaApiFlapg(useragent);
@@ -307,5 +316,22 @@ export function getZncaApiFromEnvironment(useragent?: string): ZncaApi {
return new ZncaApiNxapi(process.env.ZNCA_API_URL, useragent);
}
return new ZncaApiFlapg(useragent);
return null;
}
export async function getDefaultZncaApi(useragent?: string) {
const { default: { coral_auth: { default: provider } } } = await import('../common/remote-config.js');
if (provider === 'flapg') {
return new ZncaApiFlapg(useragent);
}
if (provider === 'imink') {
return new ZncaApiImink(useragent);
}
if (provider[0] === 'nxapi') {
return new ZncaApiNxapi(provider[1], useragent);
}
throw new Error('Invalid znca API provider');
}

View File

@@ -1,6 +1,6 @@
import fetch from 'node-fetch';
import createDebug from 'debug';
import { getNintendoAccountToken, getNintendoAccountUser } from './na.js';
import { getNintendoAccountToken, getNintendoAccountUser, NintendoAccountToken, NintendoAccountUser } from './na.js';
import { ErrorResponse } from './util.js';
import { DailySummaries, Devices, MonthlySummaries, MonthlySummary, MoonError, ParentalControlSettingState, SmartDevices, User } from './moon-types.js';
import { timeoutSignal } from '../util/misc.js';
@@ -12,11 +12,16 @@ export const ZNMA_CLIENT_ID = '54789befb391a838';
const ZNMA_VERSION = '1.17.0';
const ZNMA_BUILD = '261';
const ZNMA_USER_AGENT = 'moon_ANDROID/' + ZNMA_VERSION + ' (com.nintendo.znma; build:' + ZNMA_BUILD +
'; ANDROID 26)';
export default class MoonApi {
constructor(
protected constructor(
public token: string,
public naId: string
public naId: string,
readonly znma_version = ZNMA_VERSION,
readonly znma_build = ZNMA_BUILD,
readonly znma_useragent = ZNMA_USER_AGENT,
) {}
async fetch<T = unknown>(url: string, method = 'GET', body?: string, headers?: object) {
@@ -34,10 +39,9 @@ export default class MoonApi {
'X-Moon-TimeZone': 'Europe/London',
'X-Moon-Os-Language': 'en-GB',
'X-Moon-App-Language': 'en-GB',
'X-Moon-App-Display-Version': ZNMA_VERSION,
'X-Moon-App-Internal-Version': ZNMA_BUILD,
'User-Agent': 'moon_ANDROID/' + ZNMA_VERSION + ' (com.nintendo.znma; build:' + ZNMA_BUILD +
'; ANDROID 26)',
'X-Moon-App-Display-Version': this.znma_version,
'X-Moon-App-Internal-Version': this.znma_build,
'User-Agent': this.znma_useragent,
}, headers),
body,
signal,
@@ -82,15 +86,6 @@ export default class MoonApi {
return this.fetch<ParentalControlSettingState>('/v1/devices/' + id + '/parental_control_setting_state');
}
static async createWithSessionToken(token: string) {
const data = await this.loginWithSessionToken(token);
return {
moon: new this(data.nintendoAccountToken.access_token!, data.user.id),
data,
};
}
async renewToken(token: string) {
const data = await MoonApi.loginWithSessionToken(token);
@@ -100,7 +95,29 @@ export default class MoonApi {
return data;
}
static async loginWithSessionToken(token: string) {
static async createWithSessionToken(token: string) {
const data = await this.loginWithSessionToken(token);
return {moon: this.createWithSavedToken(data), data};
}
static createWithSavedToken(data: MoonAuthData) {
return new this(
data.nintendoAccountToken.access_token!,
data.user.id,
data.znma_version,
data.znma_build,
data.znma_useragent,
);
}
static async loginWithSessionToken(token: string): Promise<MoonAuthData> {
const { default: { moon: config } } = await import('../common/remote-config.js');
if (!config) throw new Error('Remote configuration prevents Moon authentication');
const znma_useragent = 'moon_ANDROID/' + config.znma_version +
' (com.nintendo.znma; build:' + config.znma_build + '; ANDROID 26)';
// Nintendo Account token
const nintendoAccountToken = await getNintendoAccountToken(token, ZNMA_CLIENT_ID);
@@ -110,6 +127,17 @@ export default class MoonApi {
return {
nintendoAccountToken,
user,
znma_version: config.znma_version,
znma_build: config.znma_build,
znma_useragent: znma_useragent,
};
}
}
export interface MoonAuthData {
nintendoAccountToken: NintendoAccountToken;
user: NintendoAccountUser;
znma_version: string;
znma_build: string;
znma_useragent: string;
}

View File

@@ -16,6 +16,9 @@ export default class ZncProxyApi implements CoralApi {
/** @internal */
_renewToken: Promise<void> | null = null;
readonly znca_version = '';
readonly znca_useragent = '';
constructor(
private url: string,
// ZncApi uses the NSO token (valid for a few hours)