Fetch timeout

This commit is contained in:
Samuel Elliott 2022-06-12 15:36:00 +01:00
parent ca35545f98
commit 4bf9908362
No known key found for this signature in database
GPG Key ID: 8420C7CDE43DC4D6
11 changed files with 98 additions and 43 deletions

View File

@ -3,6 +3,7 @@ import fetch from 'node-fetch';
import createDebug from 'debug';
import { ErrorResponse } from './util.js';
import { version } from '../util/product.js';
import { timeoutSignal } from '../util/misc.js';
const debugS2s = createDebug('nxapi:api:s2s');
const debugFlapg = createDebug('nxapi:api:flapg');
@ -16,6 +17,7 @@ const debugZncaApi = createDebug('nxapi:api:znca-api');
export async function getLoginHash(token: string, timestamp: string | number, useragent?: string) {
debugS2s('Getting login hash');
const [signal, cancel] = timeoutSignal();
const response = await fetch('https://elifessler.com/s2s/api/gen2', {
method: 'POST',
headers: {
@ -26,7 +28,8 @@ export async function getLoginHash(token: string, timestamp: string | number, us
naIdToken: token,
timestamp: '' + timestamp,
}).toString(),
});
signal,
}).finally(cancel);
const data = await response.json() as LoginHashApiResponse | LoginHashApiError;
@ -56,6 +59,7 @@ export async function flapg(
token, timestamp, guid, iid,
});
const [signal, cancel] = timeoutSignal();
const response = await fetch('https://flapg.com/ika2/api/login?public', {
headers: {
'User-Agent': (useragent ? useragent + ' ' : '') + 'nxapi/' + version,
@ -66,7 +70,8 @@ export async function flapg(
'x-ver': '3',
'x-iid': iid,
},
});
signal,
}).finally(cancel);
const data = await response.json() as FlapgApiResponse;
@ -110,6 +115,7 @@ export async function iminkf(
request_id: uuid,
};
const [signal, cancel] = timeoutSignal();
const response = await fetch('https://api.imink.app/f', {
method: 'POST',
headers: {
@ -117,7 +123,8 @@ export async function iminkf(
'User-Agent': (useragent ? useragent + ' ' : '') + 'nxapi/' + version,
},
body: JSON.stringify(req),
});
signal,
}).finally(cancel);
const data = await response.json() as IminkFResponse | IminkFError;
@ -163,6 +170,7 @@ export async function genf(
uuid,
};
const [signal, cancel] = timeoutSignal();
const response = await fetch(url, {
method: 'POST',
headers: {
@ -170,7 +178,8 @@ export async function genf(
'User-Agent': (useragent ? useragent + ' ' : '') + 'nxapi/' + version,
},
body: JSON.stringify(req),
});
signal,
}).finally(cancel);
const data = await response.json() as AndroidZncaFResponse | AndroidZncaFError;

View File

@ -3,6 +3,7 @@ import createDebug from 'debug';
import { getNintendoAccountToken, getNintendoAccountUser } 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';
const debug = createDebug('nxapi:api:moon');
@ -19,8 +20,9 @@ export default class MoonApi {
) {}
async fetch<T = unknown>(url: string, method = 'GET', body?: string, headers?: object) {
const [signal, cancel] = timeoutSignal();
const response = await fetch(MOON_URL + url, {
method: method,
method,
headers: Object.assign({
'Authorization': 'Bearer ' + this.token,
'Cache-Control': 'no-store',
@ -37,8 +39,9 @@ export default class MoonApi {
'User-Agent': 'moon_ANDROID/' + ZNMA_VERSION + ' (com.nintendo.znma; build:' + ZNMA_BUILD +
'; ANDROID 26)',
}, headers),
body: body,
});
body,
signal,
}).finally(cancel);
debug('fetch %s %s, response %s', method, url, response.status);

View File

@ -2,12 +2,14 @@ import fetch from 'node-fetch';
import createDebug from 'debug';
import { ErrorResponse } from './util.js';
import { JwtPayload } from '../util/jwt.js';
import { timeoutSignal } from '../util/misc.js';
const debug = createDebug('nxapi:api:na');
export async function getNintendoAccountSessionToken(code: string, verifier: string, client_id: string) {
debug('Getting Nintendo Account session token');
const [signal, cancel] = timeoutSignal();
const response = await fetch('https://accounts.nintendo.com/connect/1.0.0/api/session_token', {
method: 'POST',
headers: {
@ -21,7 +23,8 @@ export async function getNintendoAccountSessionToken(code: string, verifier: str
session_token_code: code,
session_token_code_verifier: verifier,
}).toString(),
});
signal,
}).finally(cancel);
const token = await response.json() as NintendoAccountSessionToken | NintendoAccountError;
@ -37,6 +40,7 @@ export async function getNintendoAccountSessionToken(code: string, verifier: str
export async function getNintendoAccountToken(token: string, client_id: string) {
debug('Getting Nintendo Account token');
const [signal, cancel] = timeoutSignal();
const response = await fetch('https://accounts.nintendo.com/connect/1.0.0/api/token', {
method: 'POST',
headers: {
@ -49,7 +53,8 @@ export async function getNintendoAccountToken(token: string, client_id: string)
session_token: token,
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer-session-token',
}),
});
signal,
}).finally(cancel);
const nintendoAccountToken = await response.json() as NintendoAccountToken | NintendoAccountError;
@ -65,6 +70,7 @@ export async function getNintendoAccountToken(token: string, client_id: string)
export async function getNintendoAccountUser(token: NintendoAccountToken) {
debug('Getting Nintendo Account user info');
const [signal, cancel] = timeoutSignal();
const response = await fetch('https://api.accounts.nintendo.com/2.0.0/users/me', {
headers: {
'Accept-Language': 'en-GB',
@ -73,7 +79,8 @@ export async function getNintendoAccountUser(token: NintendoAccountToken) {
'Accept': 'application/json',
'Authorization': 'Bearer ' + token.access_token!,
},
});
signal,
}).finally(cancel);
const user = await response.json() as NintendoAccountUser | NintendoAccountError;

View File

@ -5,6 +5,7 @@ import { NintendoAccountUser } from './na.js';
import { ErrorResponse } from './util.js';
import ZncApi from './znc.js';
import { WebServiceError, Users, AuthToken, UserProfile, Newspapers, Newspaper, Emoticons, Reaction, IslandProfile } from './nooklink-types.js';
import { timeoutSignal } from '../util/misc.js';
const debug = createDebug('nxapi:api:nooklink');
@ -22,8 +23,9 @@ export default class NooklinkApi {
) {}
async fetch<T = unknown>(url: string, method = 'GET', body?: string | FormData, headers?: object) {
const [signal, cancel] = timeoutSignal();
const response = await fetch(NOOKLINK_URL + url, {
method: method,
method,
headers: Object.assign({
'Upgrade-Insecure-Requests': '1',
'User-Agent': this.useragent,
@ -36,8 +38,9 @@ export default class NooklinkApi {
'Content-Type': 'application/json',
'X-Blanco-Version': BLANCO_VERSION,
}, headers),
body: body,
});
body,
signal,
}).finally(cancel);
debug('fetch %s %s, response %s', method, url, response.status);
@ -96,6 +99,7 @@ export default class NooklinkApi {
na_lang: user.language,
}).toString();
const [signal, cancel] = timeoutSignal();
const response = await fetch(url.toString(), {
headers: {
'Upgrade-Insecure-Requests': '1',
@ -106,7 +110,8 @@ export default class NooklinkApi {
'Accept-Language': 'en-GB,en-US;q=0.8',
'X-Requested-With': 'com.nintendo.znca',
},
});
signal,
}).finally(cancel);
debug('fetch %s %s, response %s', 'GET', url, response.status);
@ -154,8 +159,9 @@ export class NooklinkUserApi {
) {}
async fetch<T = unknown>(url: string, method = 'GET', body?: string | FormData, headers?: object) {
const [signal, cancel] = timeoutSignal();
const response = await fetch(NOOKLINK_URL + url, {
method: method,
method,
headers: Object.assign({
'Upgrade-Insecure-Requests': '1',
'User-Agent': this.useragent,
@ -169,8 +175,9 @@ export class NooklinkUserApi {
'Authorization': 'Bearer ' + this.auth_token,
'X-Blanco-Version': BLANCO_VERSION,
}, headers),
body: body,
});
body,
signal,
}).finally(cancel);
debug('fetch %s %s, response %s', method, url, response.status);

View File

@ -6,6 +6,7 @@ import { NintendoAccountUser } from './na.js';
import { ErrorResponse } from './util.js';
import ZncApi from './znc.js';
import { ActiveFestivals, CoopResult, CoopResults, CoopSchedules, HeroRecords, NicknameAndIcons, PastFestivals, Records, Result, Results, Schedules, ShareResponse, ShopMerchandises, Stages, Timeline, WebServiceError, XPowerRankingSummary } from './splatnet2-types.js';
import { timeoutSignal } from '../util/misc.js';
const debug = createDebug('nxapi:api:splatnet2');
@ -29,8 +30,9 @@ export default class SplatNet2Api {
) {}
async fetch<T = unknown>(url: string, method = 'GET', body?: string | FormData, headers?: object) {
const [signal, cancel] = timeoutSignal();
const response = await fetch(SPLATNET2_URL + url, {
method: method,
method,
headers: Object.assign({
'Upgrade-Insecure-Requests': '1',
'User-Agent': this.useragent,
@ -40,8 +42,9 @@ export default class SplatNet2Api {
'Accept-Language': 'en-GB,en-US;q=0.8',
'X-Requested-With': 'com.nintendo.znca',
}, headers),
body: body,
});
body,
signal,
}).finally(cancel);
debug('fetch %s %s, response %s', method, url, response.status);
@ -230,6 +233,7 @@ ${colour}
na_lang: user.language,
}).toString();
const [signal, cancel] = timeoutSignal();
const response = await fetch(url.toString(), {
headers: {
'Upgrade-Insecure-Requests': '1',
@ -240,7 +244,8 @@ ${colour}
'Accept-Language': 'en-GB,en-US;q=0.8',
'X-Requested-With': 'com.nintendo.znca',
},
});
signal,
}).finally(cancel);
debug('fetch %s %s, response %s', 'GET', url, response.status);

View File

@ -6,6 +6,7 @@ import ZncApi from './znc.js';
import { version } from '../util/product.js';
import { NintendoAccountUser } from './na.js';
import { SavedToken } from '../common/auth/nso.js';
import { timeoutSignal } from '../util/misc.js';
const debug = createDebug('nxapi:api:znc-proxy');
@ -26,14 +27,16 @@ export default class ZncProxyApi implements ZncApi {
) {}
async fetch<T = unknown>(url: string, method = 'GET', body?: string, headers?: object) {
const [signal, cancel] = timeoutSignal();
const response = await fetch(this.url + url, {
method: method,
method,
headers: Object.assign({
'Authorization': 'na ' + this.token,
'User-Agent': (this.useragent ? this.useragent + ' ' : '') + 'nxapi/' + version,
}, headers),
body: body,
});
body,
signal,
}).finally(cancel);
debug('fetch %s %s, response %s', method, url, response.status);
@ -178,7 +181,8 @@ export type PresenceUrlResponse =
Friend | {friend: Friend};
export async function getPresenceFromUrl(presence_url: string) {
const response = await fetch(presence_url);
const [signal, cancel] = timeoutSignal();
const response = await fetch(presence_url, {signal}).finally(cancel);
debug('fetch %s %s, response %s', 'GET', presence_url, response.status);

View File

@ -6,6 +6,7 @@ import createDebug from 'debug';
import fetch from 'node-fetch';
import SplatNet2Api, { ShareColour } from '../../api/splatnet2.js';
import { Challenge, NicknameAndIcon, Records, Stages } from '../../api/splatnet2-types.js';
import { timeoutSignal } from '../../util/misc.js';
const debug = createDebug('nxapi:splatnet2:dump-records');
@ -98,10 +99,6 @@ export async function dumpProfileImage(
debug('Fetching profile image URL');
const share = await splatnet.shareProfile(stage?.id ?? stages.stages[0].id, favourite_colour as ShareColour);
debug('Fetching profile image');
const image_response = await fetch(share.url);
const image = await image_response.arrayBuffer();
debug('Writing profile image data %s', filename);
await fs.writeFile(file, JSON.stringify({
share,
@ -109,6 +106,11 @@ export async function dumpProfileImage(
colour: favourite_colour,
}, null, 4) + '\n', 'utf-8');
debug('Fetching profile image', share);
const [signal, cancel] = timeoutSignal();
const image_response = await fetch(share.url, {signal}).finally(cancel);
const image = await image_response.arrayBuffer();
debug('Writing profile image %s', image_filename);
await fs.writeFile(image_file, Buffer.from(image));
@ -135,13 +137,14 @@ export async function dumpChallenges(
debug('Fetching challenge image URL for %s', challenge.key);
const share = await splatnet.shareChallenge(challenge.key, season);
debug('Fetching challenge image for %s', challenge.key);
const image_response = await fetch(share.url);
const image = await image_response.arrayBuffer();
debug('Writing challenge image data %s', filename);
await fs.writeFile(file, JSON.stringify({share}, null, 4) + '\n', 'utf-8');
debug('Fetching challenge image for %s', challenge.key, share);
const [signal, cancel] = timeoutSignal();
const image_response = await fetch(share.url, {signal}).finally(cancel);
const image = await image_response.arrayBuffer();
debug('Writing challenge image %s', filename);
await fs.writeFile(image_file, Buffer.from(image));
}

View File

@ -5,6 +5,7 @@ import createDebug from 'debug';
import fetch from 'node-fetch';
import SplatNet2Api from '../../api/splatnet2.js';
import { NicknameAndIcon } from '../../api/splatnet2-types.js';
import { timeoutSignal } from '../../util/misc.js';
const debug = createDebug('nxapi:splatnet2:dump-results');
@ -45,15 +46,16 @@ export async function dumpResults(
debug('Fetching battle results summary image URL');
const share = await splatnet.shareResultsSummary();
debug('Fetching battle results summary image');
const image_response = await fetch(share.url);
const image = await image_response.arrayBuffer();
debug('Writing battle results summary image data %s', filename);
await fs.writeFile(file, JSON.stringify({
share,
}, null, 4) + '\n', 'utf-8');
debug('Fetching battle results summary image', share);
const [signal, cancel] = timeoutSignal();
const image_response = await fetch(share.url, {signal}).finally(cancel);
const image = await image_response.arrayBuffer();
debug('Writing battle results summary image %s', filename);
await fs.writeFile(image_file, Buffer.from(image));
}
@ -102,15 +104,16 @@ export async function dumpResults(
debug('Fetching battle results image URL');
const share = await splatnet.shareResult(item.battle_number);
debug('Fetching battle results image');
const image_response = await fetch(share.url);
const image = await image_response.arrayBuffer();
debug('Writing battle results image data %s', filename);
await fs.writeFile(file, JSON.stringify({
share,
}, null, 4) + '\n', 'utf-8');
debug('Fetching battle results image', share);
const [signal, cancel] = timeoutSignal();
const image_response = await fetch(share.url, {signal}).finally(cancel);
const image = await image_response.arrayBuffer();
debug('Writing battle results image %s', filename);
await fs.writeFile(image_file, Buffer.from(image));
}

View File

@ -5,6 +5,7 @@ import createDebug from 'debug';
import mkdirp from 'mkdirp';
import { dir, version } from '../util/product.js';
import { paths } from '../util/storage.js';
import { timeoutSignal } from '../util/misc.js';
const debug = createDebug('nxapi:update');
@ -41,7 +42,8 @@ export async function checkUpdates() {
debug('Checking for updates');
try {
const response = await fetch(RELEASES_URL);
const [signal, cancel] = timeoutSignal();
const response = await fetch(RELEASES_URL, {signal}).finally(cancel);
const releases = await response.json() as Release[];
const current = releases.find(r => r.tag_name === 'v' + version);

View File

@ -3,6 +3,7 @@ import { Buffer } from 'node:buffer';
import createDebug from 'debug';
import persist from 'node-persist';
import fetch from 'node-fetch';
import { timeoutSignal } from './misc.js';
const debug = createDebug('nxapi:util:jwt');
@ -135,7 +136,8 @@ export async function getJwks(url: string, storage?: persist.LocalStorage) {
if (!cached_keyset || cached_keyset.expires_at <= Date.now()) {
debug('Downloading JSON Web Key Set from %s', url);
const response = await fetch(url);
const [signal, cancel] = timeoutSignal();
const response = await fetch(url, {signal}).finally(cancel);
const jwks = await response.json() as Jwks;

View File

@ -18,3 +18,13 @@ export function hrduration(duration: number, short = false) {
return minutes + ' ' + minute_str + (minutes === 1 ? '' : 's');
}
}
export function timeoutSignal(ms = 10 * 1000) {
const controller = new AbortController();
const timeout = setTimeout(() => {
controller.abort(new Error('Timeout'));
}, ms);
return [controller.signal, () => clearTimeout(timeout)] as const;
}