diff --git a/README.md b/README.md index e700e04..a595e4f 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,8 @@ Access the Nintendo Switch Online and Nintendo Switch Parental Controls app APIs - Interactive Nintendo Account login for the Nintendo Switch Online and Nintendo Switch Parental Controls apps - Automated login to the Nintendo Switch Online app API - This uses splatnet2statink and flapg APIs by default. - - Alternatively a custom server using a rooted Android device/emulator is included. + - Alternatively the imink API or a custom server can be used. + - A custom server using a rooted Android device/emulator is included. - Get Nintendo Switch account information, friends list and game-specific services - Show Discord Rich Presence using your own or a friend's Nintendo Switch presence - Show your account's friend code (or a custom friend code) @@ -313,7 +314,9 @@ The splatnet2statink and flapg APIs are used by default to automate authenticati Specifically, the tokens sent are JSON Web Tokens. The token sent to login to the app includes [this information and is valid for 15 minutes](https://gitlab.fancy.org.uk/samuel/nxapi/-/wikis/Nintendo-tokens#nintendo-account-id_token), and the token sent to login to web services includes [this information and is valid for two hours](https://gitlab.fancy.org.uk/samuel/nxapi/-/wikis/Nintendo-tokens#nintendo-switch-online-app-token). -Alternatively nxapi includes a custom server using Frida on an Android device/emulator that can be used instead of these. +Alternatively the [imink API](https://github.com/JoneWang/imink/wiki/imink-API-Documentation) can be used by setting the `NXAPI_ZNCA_API` environment variable to `imink`. (`NXAPI_ZNCA_API=imink nxapi nso ...`) + +nxapi also includes a custom server using Frida on an Android device/emulator that can be used instead of these. This is only required for Nintendo Switch Online app data. Nintendo Switch Parental Controls data can be fetched without sending an access token to a third-party API. @@ -477,7 +480,7 @@ nxapi nooklink island # Use a specific NookLink user linked to the selected Nintendo Account # If more than 1 NookLink users exist by default the first user will be used nxapi nooklink user --islander 0x0123456789abcdef -# --user can also be used to selecte a different Nintendo Account +# --user can also be used to select a different Nintendo Account nxapi nooklink user --user 0123456789abcdef nxapi nooklink user --user 0123456789abcdef --islander 0x0123456789abcdef ``` @@ -639,14 +642,14 @@ NXAPI_DATA_PATH=`pwd`/data nxapi ... #### Debug logs -Logging uses the `debug` package and can be controlled using the `DEBUG` environment variable. All nxapi logging uses the `api` and `cli` namespaces. +Logging uses the `debug` package and can be controlled using the `DEBUG` environment variable. All nxapi logging uses the `nxapi` and `cli` namespaces. ```sh # Show all debug logs from nxapi -DEBUG=api,api:*,cli,cli:* nxapi ... +DEBUG=nxapi:*,cli,cli:* nxapi ... # Show all API requests -DEBUG=api:* nxapi ... +DEBUG=nxapi:api:* nxapi ... # Show all debug logs DEBUG=* nxapi ... diff --git a/src/api/f.ts b/src/api/f.ts index b325114..f4f2ffb 100644 --- a/src/api/f.ts +++ b/src/api/f.ts @@ -5,8 +5,13 @@ import { version } from '../util/product.js'; const debugS2s = createDebug('nxapi:api:s2s'); const debugFlapg = createDebug('nxapi:api:flapg'); +const debugImink = createDebug('nxapi:api:imink'); const debugZncaApi = createDebug('nxapi:api:znca-api'); +// +// splatnet2statink + flapg +// + export async function getLoginHash(token: string, timestamp: string | number, useragent?: string) { debugS2s('Getting login hash'); @@ -33,6 +38,13 @@ export async function getLoginHash(token: string, timestamp: string | number, us return data.hash; } +export interface LoginHashApiResponse { + hash: string; +} +export interface LoginHashApiError { + error: string; +} + export async function flapg( token: string, timestamp: string | number, guid: string, iid: FlapgIid, useragent?: string @@ -59,9 +71,82 @@ export async function flapg( debugFlapg('Got f parameter "%s"', data.result.f); - return data.result; + return data; } +export enum FlapgIid { + /** Nintendo Switch Online app token */ + NSO = 'nso', + /** Web service token */ + APP = 'app', +} + +export interface FlapgApiResponse { + result: { + f: string; + p1: string; + p2: string; + p3: string; + }; +} + +// +// imink +// + +export async function iminkf( + token: string, timestamp: string | number, uuid: string, hash_method: '1' | '2', + useragent?: string +) { + debugImink('Getting f parameter', { + token, timestamp, uuid, hash_method, + }); + + const req: IminkFRequest = { + hash_method, + token, + timestamp: '' + timestamp, + request_id: uuid, + }; + + const response = await fetch('https://api.imink.app/f', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'User-Agent': (useragent ? useragent + ' ' : '') + 'nxapi/' + version, + }, + body: JSON.stringify(req), + }); + + const data = await response.json() as IminkFResponse | IminkFError; + + if ('error' in data) { + throw new ErrorResponse('[imink] ' + data.reason, response, data); + } + + debugImink('Got f parameter "%s"', data.f); + + return data; +} + +export interface IminkFRequest { + timestamp: string; + request_id: string; + hash_method: '1' | '2'; + token: string; +} +export interface IminkFResponse { + f: string; +} +export interface IminkFError { + reason: string; + error: true; +} + +// +// nxapi znca API server +// + export async function genf( url: string, token: string, timestamp: string | number, uuid: string, type: FlapgIid, useragent?: string @@ -95,57 +180,95 @@ export async function genf( debugZncaApi('Got f parameter "%s"', data.f); - return data.f; + return data; } -export async function genfc( - url: string, token: string, timestamp: string | number, uuid: string, type: FlapgIid, - useragent?: string -) { - const f = await genf(url, token, timestamp, uuid, type, useragent); - - return { - f, - p1: token, - p2: '' + timestamp, - p3: uuid, - - url, - }; +export interface AndroidZncaFRequest { + type: FlapgIid; + token: string; + timestamp: string; + uuid: string; } - -export interface LoginHashApiResponse { - hash: string; +export interface AndroidZncaFResponse { + f: string; } -export interface LoginHashApiError { +export interface AndroidZncaFError { error: string; } -export enum FlapgIid { - /** Nintendo Switch Online app token */ - NSO = 'nso', - /** Web service token */ - APP = 'app', -} +export async function f( + token: string, timestamp: string | number, uuid: string, type: FlapgIid, + useragent?: string, + provider: FApi = getEnvApi() +): Promise { + if (provider === 'flapg') { + const result = await flapg(token, timestamp, uuid, type, useragent); + return { + provider: 'flapg', + token, timestamp: '' + timestamp, uuid, type, + f: result.result.f, result, + }; + } + if (provider === 'imink') { + const result = await iminkf(token, timestamp, uuid, type === FlapgIid.APP ? '2' : '1', useragent); + return { + provider: 'imink', + token, timestamp: '' + timestamp, uuid, type, + f: result.f, result, + }; + } + if (provider[0] === 'nxapi') { + const result = await genf(provider[1], token, timestamp, uuid, type, useragent); + return { + provider: 'nxapi', url: provider[1], + token, timestamp: '' + timestamp, uuid, type, + f: result.f, result, + }; + } -export interface FlapgApiResponse { - result: { - f: string; - p1: string; - p2: string; - p3: string; - }; + throw new Error('Unknown znca API provider'); } -export interface AndroidZncaFRequest { - type: FlapgIid; +export type FApi = + 'flapg' | + 'imink' | + ['nxapi', string]; + +export type FResult = { + provider: string; token: string; timestamp: string; uuid: string; -} -export interface AndroidZncaFResponse { + type: FlapgIid; f: string; -} -export interface AndroidZncaFError { - error: string; + result: unknown; +} & ({ + provider: 'flapg'; + result: FlapgApiResponse; +} | { + provider: 'imink'; + result: IminkFResponse; +} | { + provider: 'nxapi'; + url: string; + result: AndroidZncaFResponse; +}); + +function getEnvApi(): FApi { + if (process.env.NXAPI_ZNCA_API) { + if (process.env.NXAPI_ZNCA_API === 'flapg') { + return 'flapg'; + } + if (process.env.NXAPI_ZNCA_API === 'imink') { + return 'imink'; + } + + throw new Error('Unknown znca API provider'); + } + + if (process.env.ZNCA_API_URL?.startsWith('https://')) { + return ['nxapi', process.env.ZNCA_API_URL + '/f']; + } + + return 'flapg'; } diff --git a/src/api/znc.ts b/src/api/znc.ts index 46cf7c6..14f38e5 100644 --- a/src/api/znc.ts +++ b/src/api/znc.ts @@ -1,7 +1,7 @@ import fetch from 'node-fetch'; import { v4 as uuidgen } from 'uuid'; import createDebug from 'debug'; -import { flapg, FlapgIid, genfc } from './f.js'; +import { f, FlapgIid } from './f.js'; import { AccountLogin, AccountToken, Announcements, CurrentUser, CurrentUserPermissions, Event, Friends, GetActiveEventResult, PresencePermissions, User, WebServices, WebServiceToken, ZncResponse, ZncStatus } from './znc-types.js'; import { getNintendoAccountToken, getNintendoAccountUser, NintendoAccountUser } from './na.js'; import { ErrorResponse } from './util.js'; @@ -126,9 +126,7 @@ export default class ZncApi { const timestamp = '' + Math.floor(Date.now() / 1000); const useragent = this.useragent ?? undefined; - const data = process.env.ZNCA_API_URL ? - await genfc(process.env.ZNCA_API_URL + '/f', this.token, timestamp, uuid, FlapgIid.APP, useragent) : - await flapg(this.token, timestamp, uuid, FlapgIid.APP, useragent); + const data = await f(this.token, timestamp, uuid, FlapgIid.APP, useragent); const req = { id, @@ -150,14 +148,12 @@ export default class ZncApi { const id_token = nintendoAccountToken.id_token; const useragent = this.useragent ?? undefined; - const flapgdata = process.env.ZNCA_API_URL ? - await genfc(process.env.ZNCA_API_URL + '/f', id_token, timestamp, uuid, FlapgIid.NSO, useragent) : - await flapg(id_token, timestamp, uuid, FlapgIid.NSO, useragent); + const fdata = await f(id_token, timestamp, uuid, FlapgIid.NSO, useragent); const req = { naBirthday: user.birthday, timestamp, - f: flapgdata.f, + f: fdata.f, requestId: uuid, naIdToken: id_token, }; @@ -169,7 +165,7 @@ export default class ZncApi { timestamp, nintendoAccountToken, // user, - flapg: flapgdata, + f: fdata, nsoAccount: data.result, credential: data.result.webApiServerCredential, }; @@ -203,9 +199,7 @@ export default class ZncApi { const user = await getNintendoAccountUser(nintendoAccountToken); const id_token = nintendoAccountToken.id_token; - const flapgdata = process.env.ZNCA_API_URL ? - await genfc(process.env.ZNCA_API_URL + '/f', id_token, timestamp, uuid, FlapgIid.NSO, useragent ?? undefined) : - await flapg(id_token, timestamp, uuid, FlapgIid.NSO, useragent ?? undefined); + const fdata = await f(id_token, timestamp, uuid, FlapgIid.NSO, useragent ?? undefined); debug('Getting Nintendo Switch Online app token'); @@ -225,11 +219,12 @@ export default class ZncApi { language: user.language, timestamp, requestId: uuid, - f: flapgdata.f, + f: fdata.f, }, }), }); + debug('fetch %s %s, response %s', 'POST', '/v3/Account/Login', response.status); const data = await response.json() as ZncResponse; if ('errorMessage' in data) { @@ -246,7 +241,7 @@ export default class ZncApi { timestamp, nintendoAccountToken, user, - flapg: flapgdata, + f: fdata, nsoAccount: data.result, credential: data.result.webApiServerCredential, }; diff --git a/src/common/auth/nso.ts b/src/common/auth/nso.ts index b159ef9..0b98c01 100644 --- a/src/common/auth/nso.ts +++ b/src/common/auth/nso.ts @@ -1,6 +1,6 @@ import createDebug from 'debug'; import * as persist from 'node-persist'; -import { FlapgApiResponse } from '../../api/f.js'; +import { FlapgApiResponse, FResult } from '../../api/f.js'; import { NintendoAccountSessionTokenJwtPayload, NintendoAccountToken, NintendoAccountUser } from '../../api/na.js'; import { Jwt } from '../../util/jwt.js'; import { AccountLogin } from '../../api/znc-types.js'; @@ -14,7 +14,8 @@ export interface SavedToken { timestamp: string; nintendoAccountToken: NintendoAccountToken; user: NintendoAccountUser; - flapg: FlapgApiResponse['result']; + f: FResult; + flapg?: FlapgApiResponse['result']; nsoAccount: AccountLogin; credential: AccountLogin['webApiServerCredential'];