Skip fetching data when creating a client with a saved token and add client authentication limit

This commit is contained in:
Samuel Elliott 2022-12-29 14:53:23 +00:00
parent 788083b7f6
commit d3c4c2d1de
No known key found for this signature in database
GPG Key ID: 8420C7CDE43DC4D6
4 changed files with 90 additions and 46 deletions

View File

@ -3,7 +3,7 @@ import { Response } from 'node-fetch';
import CoralApi, { CoralAuthData, Result, ZNCA_CLIENT_ID } from '../api/coral.js';
import { Announcements, Friends, Friend, GetActiveEventResult, WebServices, CoralErrorResponse } from '../api/coral-types.js';
import { NintendoAccountSession, Storage } from './storage/index.js';
import { checkUseLimit } from '../common/auth/util.js';
import { checkUseLimit } from './util.js';
import ZncProxyApi from '../api/znc-proxy.js';
import { ArgumentsCamelCase } from '../util/yargs.js';
import { initStorage } from '../util/storage.js';
@ -24,10 +24,10 @@ export default class Coral {
promise = new Map<string, Promise<void>>();
updated = {
announcements: Date.now(),
friends: Date.now(),
webservices: Date.now(),
active_event: Date.now(),
announcements: null as number | null,
friends: null as number | null,
webservices: null as number | null,
active_event: null as number | null,
};
update_interval = 10 * 1000; // 10 seconds
update_interval_announcements = 30 * 60 * 1000; // 30 minutes
@ -37,15 +37,20 @@ export default class Coral {
constructor(
public api: CoralApi,
public data: CoralAuthData,
public announcements: Result<Announcements>,
public friends: Result<Friends>,
public webservices: Result<WebServices>,
public active_event: Result<GetActiveEventResult>,
) {}
public announcements: Result<Announcements> | null = null,
public friends: Result<Friends> | null = null,
public webservices: Result<WebServices> | null = null,
public active_event: Result<GetActiveEventResult> | null = null,
) {
if (announcements) this.updated.announcements = Date.now();
if (friends) this.updated.friends = Date.now();
if (webservices) this.updated.webservices = Date.now();
if (active_event) this.updated.active_event = Date.now();
}
private async update(key: keyof Coral['updated'], callback: () => Promise<void>, ttl: number) {
if ((this.updated[key] + ttl) < Date.now()) {
const promise = this.promise.get(key) ?? callback.call(null).then(() => {
private update(key: keyof Coral['updated'], callback: () => Promise<void>, ttl: number) {
if (((this.updated[key] ?? 0) + ttl) < Date.now()) {
const promise = this.promise.get(key) ?? Promise.resolve().then(() => callback.call(null)).then(() => {
this.updated[key] = Date.now();
this.promise.delete(key);
}).catch(err => {
@ -55,7 +60,7 @@ export default class Coral {
this.promise.set(key, promise);
await promise;
return promise;
} else {
debug('Not updating %s data for coral user %s', key, this.data.nsoAccount.user.name);
}
@ -67,10 +72,14 @@ export default class Coral {
async getAnnouncements() {
await this.update('announcements', async () => {
this.getFriends();
this.getWebServices();
this.getActiveEvent();
this.announcements = await this.api.getAnnouncements();
}, this.update_interval_announcements);
return this.announcements;
return this.announcements!;
}
async getFriends() {
@ -78,25 +87,31 @@ export default class Coral {
this.friends = await this.api.getFriendList();
}, this.update_interval);
return this.friends.friends;
return this.friends!.friends;
}
async getWebServices() {
await this.update('webservices', async () => {
this.getFriends();
this.getActiveEvent();
const webservices = this.webservices = await this.api.getWebServices();
this.onUpdatedWebServices?.call(null, webservices);
}, this.update_interval);
return this.webservices;
return this.webservices!;
}
async getActiveEvent() {
await this.update('active_event', async () => {
this.getFriends();
this.getWebServices();
this.active_event = await this.api.getActiveEvent();
}, this.update_interval);
return 'id' in this.active_event ? this.active_event : null;
return 'id' in this.active_event! ? this.active_event : null;
}
async addFriend(nsa_id: string) {
@ -115,7 +130,7 @@ export default class Coral {
try {
// Clear the last updated timestamp to force updating the friend list
this.updated.friends = 0;
this.updated.friends = null;
const friends = await this.getFriends();
friend = friends.find(f => f.nsaId === nsa_id) ?? null;
@ -143,17 +158,17 @@ export default class Coral {
static async createWithSession(session: NintendoAccountSession<SavedToken>, oidc: NintendoAccountOIDC) {
const cached_auth_data = await session.getAuthenticationData();
const [coral, auth_data] = cached_auth_data && cached_auth_data.expires_at > Date.now() ?
[CoralApi.createWithSavedToken(cached_auth_data), cached_auth_data] :
const [coral, auth_data, skip_fetch] = cached_auth_data && cached_auth_data.expires_at > Date.now() ?
[CoralApi.createWithSavedToken(cached_auth_data), cached_auth_data, true] :
await this.createWithSessionAuthenticate(session, oidc);
return this.createWithCoralApi(coral, auth_data);
return this.createWithCoralApi(coral, auth_data, skip_fetch);
}
private static async createWithSessionAuthenticate(
session: NintendoAccountSession<SavedToken>, oidc: NintendoAccountOIDC
session: NintendoAccountSession<SavedToken>, oidc: NintendoAccountOIDC, ratelimit = true
) {
// await checkUseLimit(storage, 'coral', jwt.payload.sub, ratelimit);
await checkUseLimit(session, 'coral', ratelimit);
console.warn('Authenticating to Nintendo Switch Online app');
debug('Authenticating to znc with session token');
@ -178,17 +193,17 @@ export default class Coral {
static async createWithProxy(session: NintendoAccountSession<SavedToken>, proxy_url: string) {
const cached_auth_data = await session.getAuthenticationData();
const [coral, auth_data] = cached_auth_data && cached_auth_data.expires_at > Date.now() ?
[new ZncProxyApi(proxy_url, session.token), cached_auth_data] :
const [coral, auth_data, skip_fetch] = cached_auth_data && cached_auth_data.expires_at > Date.now() ?
[new ZncProxyApi(proxy_url, session.token), cached_auth_data, true] :
await this.createWithProxyAuthenticate(session, proxy_url);
return this.createWithCoralApi(coral, auth_data);
return this.createWithCoralApi(coral, auth_data, skip_fetch);
}
private static async createWithProxyAuthenticate(
session: NintendoAccountSession<SavedToken>, proxy_url: string
session: NintendoAccountSession<SavedToken>, proxy_url: string, ratelimit = true
) {
// await checkUseLimit(storage, 'coral', jwt.payload.sub, ratelimit);
await checkUseLimit(session, 'coral', ratelimit);
console.warn('Authenticating to Nintendo Switch Online app');
debug('Authenticating to znc with session token');
@ -205,7 +220,12 @@ export default class Coral {
return [nso, auth_data] as const;
}
static async createWithCoralApi(coral: CoralApi, data: SavedToken) {
static async createWithCoralApi(coral: CoralApi, data: SavedToken, skip_fetch = false) {
if (skip_fetch) {
debug('Already authenticated, skip fetching coral data');
return new Coral(coral, data, null, null, null, null);
}
const [announcements, friends, webservices, active_event] = await Promise.all([
coral.getAnnouncements(),
coral.getFriendList(),
@ -253,6 +273,8 @@ async function renewToken(
// await checkUseLimit(storage, 'coral', jwt.payload.sub, ratelimit);
// }
await checkUseLimit(session, 'coral', ratelimit);
const data = await coral.renewToken(session.token, renew_token_data.auth_data.user);
const auth_data: SavedToken = {

View File

@ -1,7 +1,6 @@
import createDebug from 'debug';
import { NintendoAccountSession } from './storage/index.js';
import { getNintendoAccountToken, getNintendoAccountUser, NintendoAccountToken, NintendoAccountUser } from '../api/na.js';
import Users from './users.js';
const debug = createDebug('nxapi:client:na');

View File

@ -7,6 +7,7 @@ import SplatNet3Api, { PersistedQueryResult, SplatNet3AuthData } from '../api/sp
import Coral, { SavedToken as SavedCoralToken } from './coral.js';
import { ErrorResponse } from '../api/util.js';
import Users from './users.js';
import { checkUseLimit } from './util.js';
const debug = createDebug('nxapi:client:splatnet3');
@ -26,7 +27,7 @@ export default class SplatNet3 {
updated = {
configure_analytics: null as number | null,
current_fest: null as number | null,
home: Date.now(),
home: null as number | null,
friends: null as number | null,
schedules: null as number | null,
};
@ -38,10 +39,11 @@ export default class SplatNet3 {
public data: SplatNet3AuthData,
public configure_analytics: PersistedQueryResult<ConfigureAnalyticsResult> | null = null,
public current_fest: PersistedQueryResult<CurrentFestResult> | null = null,
public home: PersistedQueryResult<HomeResult>,
public home: PersistedQueryResult<HomeResult> | null = null,
) {
if (configure_analytics) this.updated.configure_analytics = Date.now();
if (current_fest) this.updated.current_fest = Date.now();
if (home) this.updated.home = Date.now();
}
protected async update(key: keyof SplatNet3['updated'], callback: () => Promise<void>, ttl: number) {
@ -67,7 +69,7 @@ export default class SplatNet3 {
this.home = await this.api.getHome();
}, this.update_interval);
return this.home.data;
return this.home!.data;
}
async getFriends(): Promise<Friend_friendList[]> {
@ -90,19 +92,21 @@ export default class SplatNet3 {
static async createWithSession(session: NintendoAccountSession<SavedCoralToken>, coral: Coral) {
const cached_auth_data = await session.getItem<SavedToken>('BulletToken');
const [splatnet, auth_data] = cached_auth_data && cached_auth_data.expires_at > Date.now() ?
[SplatNet3Api.createWithSavedToken(cached_auth_data), cached_auth_data] :
const [splatnet, auth_data, skip_fetch] = cached_auth_data && cached_auth_data.expires_at > Date.now() ?
[SplatNet3Api.createWithSavedToken(cached_auth_data), cached_auth_data, true] :
await this.createWithSessionAuthenticate(session, coral);
const renew_token_data = {coral, auth_data};
splatnet.onTokenExpired = createTokenExpiredHandler(session, splatnet, renew_token_data);
splatnet.onTokenShouldRenew = createTokenShouldRenewHandler(session, splatnet, renew_token_data);
return this.createWithSplatNet3Api(splatnet, auth_data);
return this.createWithSplatNet3Api(splatnet, auth_data, skip_fetch);
}
private static async createWithSessionAuthenticate(session: NintendoAccountSession<SavedCoralToken>, coral: Coral) {
//
private static async createWithSessionAuthenticate(
session: NintendoAccountSession<SavedCoralToken>, coral: Coral, ratelimit = true
) {
await checkUseLimit(session, 'bullet', ratelimit);
const {splatnet, data} = await SplatNet3Api.createWithCoral(coral.api, coral.data.user);
@ -111,10 +115,10 @@ export default class SplatNet3 {
return [splatnet, data] as const;
}
static async createWithSplatNet3Api(splatnet: SplatNet3Api, data: SavedToken) {
const home = await splatnet.getHome();
static async createWithSplatNet3Api(splatnet: SplatNet3Api, data: SavedToken, skip_fetch = true) {
const home = skip_fetch ? null : await splatnet.getHome();
const [configure_analytics, current_fest] = await Promise.all([
const [configure_analytics, current_fest] = skip_fetch ? [null, null] : await Promise.all([
splatnet.getConfigureAnalytics().catch(err => {
debug('Error in ConfigureAnalyticsQuery request', err);
}),
@ -165,10 +169,7 @@ async function renewToken(
session: NintendoAccountSession<SavedCoralToken>, splatnet: SplatNet3Api,
renew_token_data: {coral: Coral; auth_data: SavedToken; znc_proxy_url?: string}, ratelimit = true
) {
// if (ratelimit) {
// const [jwt, sig] = Jwt.decode<NintendoAccountSessionTokenJwtPayload>(token);
// await checkUseLimit(storage, 'splatnet3', jwt.payload.sub);
// }
await checkUseLimit(session, 'bullet', ratelimit);
try {
const coral_auth_data = renew_token_data.coral.data ?? await session.getAuthenticationData();

22
src/client/util.ts Normal file
View File

@ -0,0 +1,22 @@
import createDebug from 'debug';
import { LIMIT_PERIOD, LIMIT_REQUESTS } from '../common/auth/util.js';
import { NintendoAccountSession } from './storage/index.js';
const debug = createDebug('nxapi:client:util');
export async function checkUseLimit(
session: NintendoAccountSession<unknown>,
key: string,
/** Set to false to count the attempt but ignore the limit */ ratelimit = true,
/** [requests, period_ms] */ limits: [number, number] = [LIMIT_REQUESTS, LIMIT_PERIOD]
) {
let attempts = await session.getRateLimitAttempts(key);
attempts = attempts.filter(a => a >= Date.now() - limits[1]);
if (ratelimit && attempts.length >= limits[0]) {
throw new Error('Too many attempts to authenticate');
}
attempts.unshift(Date.now());
await session.setRateLimitAttempts(key, attempts);
}