mirror of
https://github.com/samuelthomas2774/nxapi.git
synced 2026-07-13 14:51:10 -05:00
Use cached web service token to renew SplatNet 3 tokens
This commit is contained in:
parent
b6002cbf8b
commit
5cb66f4249
|
|
@ -41,7 +41,7 @@ export interface ResultData<T> {
|
|||
}
|
||||
|
||||
export default class CoralApi {
|
||||
onTokenExpired: ((data: CoralErrorResponse, res: Response) => Promise<void>) | null = null;
|
||||
onTokenExpired: ((data: CoralErrorResponse, res: Response) => Promise<CoralAuthData | void>) | null = null;
|
||||
/** @internal */
|
||||
_renewToken: Promise<void> | null = null;
|
||||
|
||||
|
|
@ -85,7 +85,9 @@ export default class CoralApi {
|
|||
|
||||
if (data.status === CoralStatus.TOKEN_EXPIRED && _autoRenewToken && !_attempt && this.onTokenExpired) {
|
||||
// _renewToken will be awaited when calling fetch
|
||||
this._renewToken = this._renewToken ?? this.onTokenExpired.call(null, data, response).finally(() => {
|
||||
this._renewToken = this._renewToken ?? this.onTokenExpired.call(null, data, response).then(data => {
|
||||
if (data) this.setTokenWithSavedToken(data);
|
||||
}).finally(() => {
|
||||
this._renewToken = null;
|
||||
});
|
||||
return this.fetch(url, method, body, headers, _autoRenewToken, _attempt + 1);
|
||||
|
|
@ -223,7 +225,9 @@ export default class CoralApi {
|
|||
} catch (err) {
|
||||
if (err instanceof ErrorResponse && err.data.status === CoralStatus.TOKEN_EXPIRED && !_attempt && this.onTokenExpired) {
|
||||
// _renewToken will be awaited when calling getWebServiceToken
|
||||
this._renewToken = this._renewToken ?? this.onTokenExpired.call(null, err.data, err.response as Response).finally(() => {
|
||||
this._renewToken = this._renewToken ?? this.onTokenExpired.call(null, err.data, err.response as Response).then(data => {
|
||||
if (data) this.setTokenWithSavedToken(data);
|
||||
}).finally(() => {
|
||||
this._renewToken = null;
|
||||
});
|
||||
return this.getWebServiceToken(id, _attempt + 1);
|
||||
|
|
@ -261,12 +265,15 @@ export default class CoralApi {
|
|||
|
||||
async renewToken(token: string, user: NintendoAccountUser) {
|
||||
const data = await this.getToken(token, user);
|
||||
|
||||
this.token = data.credential.accessToken;
|
||||
|
||||
this.setTokenWithSavedToken(data);
|
||||
return data;
|
||||
}
|
||||
|
||||
/** @private */
|
||||
setTokenWithSavedToken(data: CoralAuthData | PartialCoralAuthData) {
|
||||
this.token = data.credential.accessToken;
|
||||
}
|
||||
|
||||
static async createWithSessionToken(token: string, useragent = getAdditionalUserAgents()) {
|
||||
const data = await this.loginWithSessionToken(token, useragent);
|
||||
return {nso: this.createWithSavedToken(data, useragent), data};
|
||||
|
|
|
|||
|
|
@ -1046,7 +1046,7 @@ interface FooterSeasonMessage {
|
|||
}
|
||||
|
||||
/** 994cf141e55213e6923426caf37a1934 VsHistoryDetailPagerRefetchQuery */
|
||||
export interface VsHistoryDetailPagerRefetchQueryResult {
|
||||
export interface VsHistoryDetailPagerRefetchResult {
|
||||
vsHistoryDetail: {
|
||||
__typename: 'VsHistoryDetail';
|
||||
nextHistoryDetail: {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { NintendoAccountUser } from './na.js';
|
|||
import { defineResponse, ErrorResponse, HasResponse } from './util.js';
|
||||
import CoralApi from './coral.js';
|
||||
import { timeoutSignal } from '../util/misc.js';
|
||||
import { BankaraBattleHistoriesResult, BattleHistoryCurrentPlayerResult, BulletToken, CurrentFestResult, FriendListResult, GraphQLRequest, GraphQLResponse, HistoryRecordResult, HomeResult, LatestBattleHistoriesResult, PrivateBattleHistoriesResult, RegularBattleHistoriesResult, RequestId, SettingResult, StageScheduleResult, VsHistoryDetailResult, CoopHistoryResult, CoopHistoryDetailResult, FestRecordResult, FestRecordRefetchResult, DetailFestRecordDetailResult, DetailVotingStatusResult, DetailFestVotingStatusRefetchResult, VotesUpdateFestVoteResult } from './splatnet3-types.js';
|
||||
import { BankaraBattleHistoriesResult, BattleHistoryCurrentPlayerResult, BulletToken, CurrentFestResult, FriendListResult, GraphQLRequest, GraphQLResponse, HistoryRecordResult, HomeResult, LatestBattleHistoriesResult, PrivateBattleHistoriesResult, RegularBattleHistoriesResult, RequestId, SettingResult, StageScheduleResult, VsHistoryDetailResult, CoopHistoryResult, CoopHistoryDetailResult, FestRecordResult, FestRecordRefetchResult, DetailFestRecordDetailResult, DetailVotingStatusResult, DetailFestVotingStatusRefetchResult, VotesUpdateFestVoteResult, VsHistoryDetailPagerRefetchResult } from './splatnet3-types.js';
|
||||
|
||||
const debug = createDebug('nxapi:api:splatnet3');
|
||||
|
||||
|
|
@ -21,10 +21,22 @@ const languages = [
|
|||
|
||||
const SPLATNET3_URL = SPLATNET3_WEBSERVICE_URL + '/api';
|
||||
const SHOULD_RENEW_TOKEN_AT = 300; // 5 minutes in seconds
|
||||
const TOKEN_EXPIRES_IN = 2 * 60 * 60 * 1000; // 2 hours in milliseconds
|
||||
|
||||
const AUTH_ERROR_CODES = {
|
||||
204: 'USER_NOT_REGISTERED',
|
||||
400: 'ERROR_INVALID_PARAMETERS',
|
||||
401: 'ERROR_INVALID_GAME_WEB_TOKEN',
|
||||
403: 'ERROR_OBSOLETE_VERSION',
|
||||
429: 'ERROR_RATE_LIMIT',
|
||||
500: 'ERROR_SERVER',
|
||||
503: 'ERROR_SERVER_MAINTENANCE',
|
||||
599: 'ERROR_SERVER',
|
||||
} as const;
|
||||
|
||||
export default class SplatNet3Api {
|
||||
onTokenShouldRenew: ((remaining: number, res: Response) => Promise<void>) | null = null;
|
||||
onTokenExpired: ((res: Response) => Promise<void>) | null = null;
|
||||
onTokenShouldRenew: ((remaining: number, res: Response) => Promise<SplatNet3AuthData | void>) | null = null;
|
||||
onTokenExpired: ((res: Response) => Promise<SplatNet3AuthData | void>) | null = null;
|
||||
/** @internal */
|
||||
_renewToken: Promise<void> | null = null;
|
||||
|
||||
|
|
@ -67,7 +79,9 @@ export default class SplatNet3Api {
|
|||
|
||||
if (response.status === 401 && !_attempt && this.onTokenExpired) {
|
||||
// _renewToken will be awaited when calling fetch
|
||||
this._renewToken = this._renewToken ?? this.onTokenExpired.call(null, response).finally(() => {
|
||||
this._renewToken = this._renewToken ?? this.onTokenExpired.call(null, response).then(data => {
|
||||
if (data) this.setTokenWithSavedToken(data);
|
||||
}).finally(() => {
|
||||
this._renewToken = null;
|
||||
});
|
||||
return this.fetch(url, method, body, headers, _log, _attempt + 1);
|
||||
|
|
@ -81,7 +95,9 @@ export default class SplatNet3Api {
|
|||
|
||||
if (remaining <= SHOULD_RENEW_TOKEN_AT && !_attempt && this.onTokenShouldRenew) {
|
||||
// _renewToken will be awaited when calling fetch
|
||||
this._renewToken = this._renewToken ?? this.onTokenShouldRenew.call(null, remaining, response).finally(() => {
|
||||
this._renewToken = this._renewToken ?? this.onTokenShouldRenew.call(null, remaining, response).then(data => {
|
||||
if (data) this.setTokenWithSavedToken(data);
|
||||
}).finally(() => {
|
||||
this._renewToken = null;
|
||||
});
|
||||
}
|
||||
|
|
@ -205,7 +221,7 @@ export default class SplatNet3Api {
|
|||
}
|
||||
|
||||
async getBattleHistoryDetailPagerRefetch(id: string) {
|
||||
return this.persistedQuery<VsHistoryDetailResult>(RequestId.VsHistoryDetailPagerRefetchQuery, {
|
||||
return this.persistedQuery<VsHistoryDetailPagerRefetchResult>(RequestId.VsHistoryDetailPagerRefetchQuery, {
|
||||
vsResultId: id,
|
||||
});
|
||||
}
|
||||
|
|
@ -220,6 +236,25 @@ export default class SplatNet3Api {
|
|||
});
|
||||
}
|
||||
|
||||
async renewTokenWithCoral(nso: CoralApi, user: NintendoAccountUser) {
|
||||
const data = await SplatNet3Api.loginWithCoral(nso, user);
|
||||
this.setTokenWithSavedToken(data);
|
||||
return data;
|
||||
}
|
||||
|
||||
async renewTokenWithWebServiceToken(webserviceToken: WebServiceToken, user: NintendoAccountUser) {
|
||||
const data = await SplatNet3Api.loginWithWebServiceToken(webserviceToken, user);
|
||||
this.setTokenWithSavedToken(data);
|
||||
return data;
|
||||
}
|
||||
|
||||
protected setTokenWithSavedToken(data: SplatNet3AuthData) {
|
||||
this.bullet_token = data.bullet_token.bulletToken;
|
||||
this.version = data.version;
|
||||
this.language = data.bullet_token.lang;
|
||||
this.useragent = data.useragent;
|
||||
}
|
||||
|
||||
static async createWithCoral(nso: CoralApi, user: NintendoAccountUser) {
|
||||
const data = await this.loginWithCoral(nso, user);
|
||||
return {splatnet: this.createWithSavedToken(data), data};
|
||||
|
|
@ -294,7 +329,7 @@ export default class SplatNet3Api {
|
|||
const cookies = response.headers.get('Set-Cookie');
|
||||
|
||||
const [signal2, cancel2] = timeoutSignal();
|
||||
const token_response = await fetch(SPLATNET3_URL + '/bullet_tokens', {
|
||||
const tr = await fetch(SPLATNET3_URL + '/bullet_tokens', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'User-Agent': SPLATNET3_WEBSERVICE_USERAGENT,
|
||||
|
|
@ -313,21 +348,13 @@ export default class SplatNet3Api {
|
|||
|
||||
debug('fetch %s %s, response %s', 'POST', '/bullet_tokens', response.status);
|
||||
|
||||
if (token_response.status === 401) {
|
||||
throw new ErrorResponse('[splatnet3] ERROR_INVALID_GAME_WEB_TOKEN', token_response, await token_response.text());
|
||||
}
|
||||
if (token_response.status === 403) {
|
||||
throw new ErrorResponse('[splatnet3] ERROR_OBSOLETE_VERSION', token_response, await token_response.text());
|
||||
}
|
||||
if (token_response.status === 204) {
|
||||
throw new ErrorResponse('[splatnet3] USER_NOT_REGISTERED', token_response, await token_response.text());
|
||||
}
|
||||
if (token_response.status !== 201) {
|
||||
throw new ErrorResponse('[splatnet3] Non-201 status code', token_response, await token_response.text());
|
||||
}
|
||||
const error: string | undefined = AUTH_ERROR_CODES[tr.status as keyof typeof AUTH_ERROR_CODES];
|
||||
if (error) throw new ErrorResponse('[splatnet3] ' + error, tr, await tr.text());
|
||||
if (tr.status !== 201) throw new ErrorResponse('[splatnet3] Non-201 status code', tr, await tr.text());
|
||||
|
||||
const bullet_token = await token_response.json() as BulletToken;
|
||||
const expires_at = Date.now() + (2 * 60 * 60 * 1000); // ??
|
||||
const bullet_token = await tr.json() as BulletToken;
|
||||
const created_at = Date.now();
|
||||
const expires_at = created_at + TOKEN_EXPIRES_IN;
|
||||
|
||||
return {
|
||||
webserviceToken,
|
||||
|
|
@ -340,6 +367,7 @@ export default class SplatNet3Api {
|
|||
version,
|
||||
|
||||
bullet_token,
|
||||
created_at,
|
||||
expires_at,
|
||||
useragent: SPLATNET3_WEBSERVICE_USERAGENT,
|
||||
};
|
||||
|
|
@ -357,6 +385,12 @@ export interface SplatNet3AuthData {
|
|||
version: string;
|
||||
|
||||
bullet_token: BulletToken;
|
||||
created_at: number;
|
||||
/**
|
||||
* /api/bullet_tokens does not provide the token validity duration. Instead this assumes
|
||||
* the token is valid for 2 hours. GraphQL responses include the actual remaining time
|
||||
* in the x-bullettoken-remaining header.
|
||||
*/
|
||||
expires_at: number;
|
||||
useragent: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import fetch, { Response } from 'node-fetch';
|
|||
import createDebug from 'debug';
|
||||
import { ActiveEvent, Announcements, CurrentUser, Event, Friend, Presence, PresencePermissions, User, WebService, WebServiceToken, CoralErrorResponse, CoralStatus, CoralSuccessResponse, FriendCodeUser, FriendCodeUrl } from './coral-types.js';
|
||||
import { defineResponse, ErrorResponse, ResponseSymbol } from './util.js';
|
||||
import CoralApi, { CorrelationIdSymbol, ResponseDataSymbol, Result } from './coral.js';
|
||||
import CoralApi, { CoralAuthData, CorrelationIdSymbol, PartialCoralAuthData, ResponseDataSymbol, Result } from './coral.js';
|
||||
import { NintendoAccountUser } from './na.js';
|
||||
import { SavedToken } from '../common/auth/coral.js';
|
||||
import { timeoutSignal } from '../util/misc.js';
|
||||
|
|
@ -12,7 +12,7 @@ const debug = createDebug('nxapi:api:znc-proxy');
|
|||
|
||||
export default class ZncProxyApi implements CoralApi {
|
||||
// Not used by ZncProxyApi
|
||||
onTokenExpired: ((data: CoralErrorResponse, res: Response) => Promise<void>) | null = null;
|
||||
onTokenExpired: ((data: CoralErrorResponse, res: Response) => Promise<CoralAuthData | void>) | null = null;
|
||||
/** @internal */
|
||||
_renewToken: Promise<void> | null = null;
|
||||
|
||||
|
|
@ -147,6 +147,11 @@ export default class ZncProxyApi implements CoralApi {
|
|||
return data;
|
||||
}
|
||||
|
||||
/** @private */
|
||||
setTokenWithSavedToken(data: CoralAuthData | PartialCoralAuthData) {
|
||||
throw new Error('Not supported in ZncProxyApi');
|
||||
}
|
||||
|
||||
static async createWithSessionToken(url: string, token: string) {
|
||||
const nso = new this(url, token);
|
||||
const data = await nso.fetch<SavedToken>('/auth');
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import createDebug from 'debug';
|
||||
import persist from 'node-persist';
|
||||
import { Response } from 'node-fetch';
|
||||
import { getToken, Login } from './coral.js';
|
||||
import { getToken, Login, SavedToken } from './coral.js';
|
||||
import SplatNet3Api, { SplatNet3AuthData } from '../../api/splatnet3.js';
|
||||
import { checkUseLimit, SHOULD_LIMIT_USE } from './util.js';
|
||||
import { Jwt } from '../../util/jwt.js';
|
||||
import { NintendoAccountSessionTokenJwtPayload } from '../../api/na.js';
|
||||
import { ErrorResponse } from '../../api/util.js';
|
||||
|
||||
const debug = createDebug('nxapi:auth:splatnet3');
|
||||
|
||||
|
|
@ -94,6 +95,28 @@ async function renewToken(
|
|||
storage: persist.LocalStorage, token: string, splatnet: SplatNet3Api, previousToken: SavedBulletToken,
|
||||
znc_proxy_url?: string
|
||||
) {
|
||||
try {
|
||||
const data: SavedToken | undefined = await storage.getItem('NsoToken.' + token);
|
||||
|
||||
if (data) {
|
||||
const existingToken: SavedBulletToken =
|
||||
await splatnet.renewTokenWithWebServiceToken(previousToken.webserviceToken, data.user);
|
||||
|
||||
await storage.setItem('BulletToken.' + token, existingToken);
|
||||
|
||||
return;
|
||||
} else {
|
||||
debug('Unable to renew bullet token with saved web services token - cached data for this session token doesn\'t exist??');
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof ErrorResponse && err.response.status === 401) {
|
||||
// Web service token invalid/expired...
|
||||
debug('Web service token expired, authenticating with new token', err);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
const {nso, data} = await getToken(storage, token, znc_proxy_url);
|
||||
|
||||
if (data[Login]) {
|
||||
|
|
@ -103,12 +126,7 @@ async function renewToken(
|
|||
const activeevent = await nso.getActiveEvent();
|
||||
}
|
||||
|
||||
const existingToken: SavedBulletToken = await SplatNet3Api.loginWithCoral(nso, data.user);
|
||||
|
||||
splatnet.bullet_token = existingToken.bullet_token.bulletToken;
|
||||
splatnet.version = existingToken.version;
|
||||
splatnet.language = existingToken.bullet_token.lang;
|
||||
splatnet.useragent = existingToken.useragent;
|
||||
const existingToken: SavedBulletToken = await splatnet.renewTokenWithCoral(nso, data.user);
|
||||
|
||||
await storage.setItem('BulletToken.' + token, existingToken);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user