From bf48cc201b811f000ad2e5bea34758c5192b3e97 Mon Sep 17 00:00:00 2001 From: Samuel Elliott Date: Sat, 15 Oct 2022 22:20:54 +0100 Subject: [PATCH] Add command to list node-persist data --- src/cli/users.ts | 138 +++++++++++++++++++++++++++++++--------- src/cli/util/index.ts | 1 + src/cli/util/storage.ts | 43 +++++++++++++ src/util/storage.ts | 14 ++++ 4 files changed, 167 insertions(+), 29 deletions(-) create mode 100644 src/cli/util/storage.ts diff --git a/src/cli/users.ts b/src/cli/users.ts index a300f3b..956c3a9 100644 --- a/src/cli/users.ts +++ b/src/cli/users.ts @@ -1,16 +1,26 @@ import createDebug from 'debug'; +import * as persist from 'node-persist'; import Table from './util/table.js'; import type { Arguments as ParentArguments } from '../cli.js'; import { Argv } from '../util/yargs.js'; -import { initStorage } from '../util/storage.js'; +import { initStorage, iterateLocalStorage } from '../util/storage.js'; import { SavedToken } from '../common/auth/coral.js'; import { SavedMoonToken } from '../common/auth/moon.js'; +import { Jwt } from '../util/jwt.js'; +import { NintendoAccountSessionTokenJwtPayload } from '../api/na.js'; const debug = createDebug('cli:users'); +const debugRemove = createDebug('cli:users:remove'); +debugRemove.enabled = true; export const command = 'users '; export const desc = 'Manage authenticated Nintendo Accounts'; +interface AppSavedMonitorState { + users: {id: string;}[]; + discord_presence: {source: {na_id: string;};} | null; +} + export function builder(yargs: Argv) { return yargs.command('list', 'Lists known Nintendo Accounts', () => {}, async argv => { const storage = await initStorage(argv.dataPath); @@ -86,37 +96,107 @@ export function builder(yargs: Argv) { }, async argv => { const storage = await initStorage(argv.dataPath); - const selected: string | undefined = await storage.getItem('SelectedUser'); const nsoToken: string | undefined = await storage.getItem('NintendoAccountToken.' + argv.user); const nsoCache: SavedToken | undefined = nsoToken ? await storage.getItem('NsoToken.' + nsoToken) : undefined; - const moonToken: string | undefined = await storage.getItem('NintendoAccountToken-pctl.' + argv.user); - if (!nsoToken && !moonToken) { - throw new Error('Unknown user'); - } + await removeUserData(storage, argv.user); - if (selected === argv.user) { - await storage.removeItem('SelectedUser'); - await storage.removeItem('SessionToken'); - } - - await storage.removeItem('IksmToken.' + nsoToken); - await storage.removeItem('NookToken.' + nsoToken); - await storage.removeItem('NookUsers.' + nsoToken); - - for (const key of await storage.keys()) { - if (key.startsWith('NookAuthToken.' + nsoToken + '.')) await storage.removeItem(key); - if (nsoCache && key.startsWith('WebServicePersistentData.' + nsoCache.nsoAccount.user.nsaId + '.')) - await storage.removeItem(key); - } - - await storage.removeItem('NintendoAccountToken.' + argv.user); - await storage.removeItem('NsoToken.' + nsoToken); - await storage.removeItem('NintendoAccountToken-pctl.' + argv.user); - await storage.removeItem('MoonToken.' + moonToken); - - const users = new Set(await storage.getItem('NintendoAccountIds') ?? []); - users.delete(argv.user); - await storage.setItem('NintendoAccountIds', [...users]); + console.log('Removed %s%s from storage', argv.user, + nsoCache ? ' (' + nsoCache.user.nickname + '/' + nsoCache.nsoAccount.user.name + ')' : ''); + console.log('Additional cached data about this user may still exist in nxapi\'s storage. Use `nxapi util storage list` to list all data stored with node-persist.'); }); } + +async function removeUserData( + storage: persist.LocalStorage, na_id: string, + only?: ('coral' | 'moon')[], only_cached = false +) { + let coral_saved_token: SavedToken | undefined = undefined; + + for await (const {key, value} of iterateLocalStorage(storage)) { + if (key.startsWith('NsoToken.') && (!only || only.includes('coral'))) { + const session_token = key.substr(9); + const [jwt, sig] = Jwt.decode(session_token); + if (jwt.payload.sub !== na_id) continue; + + if (!coral_saved_token) coral_saved_token = value; + + debugRemove('Removing data for coral session token', session_token); + await removeSavedCoralTokenData(storage, session_token); + } + + if (key.startsWith('MoonToken.') && (!only || only.includes('moon'))) { + const session_token = key.substr(9); + const [jwt, sig] = Jwt.decode(session_token); + if (jwt.payload.sub !== na_id) continue; + + debugRemove('Removing data for moon session token', session_token); + await removeSavedMoonTokenData(storage, session_token); + } + } + + if (coral_saved_token && (!only || only.includes('coral'))) { + for await (const {key} of iterateLocalStorage(storage)) { + if (key.startsWith('WebServicePersistentData.' + coral_saved_token.nsoAccount.user.nsaId + '.')) { + debugRemove('Removing web service persisted data', key.substr(25)); + await storage.removeItem(key); + } + } + } + + const selected: string | undefined = await storage.getItem('SelectedUser'); + let coral_session_token: string | undefined = await storage.getItem('NintendoAccountToken.' + na_id); + let moon_session_token: string | undefined = await storage.getItem('NintendoAccountToken-pctl.' + na_id); + + if (coral_session_token && (!only || only.includes('coral')) && !only_cached) { + debugRemove('Removing coral session token'); + await storage.removeItem('NintendoAccountToken.' + na_id); + + const app_monitors: AppSavedMonitorState | undefined = await storage.getItem('AppMonitors'); + if (app_monitors) { + app_monitors.users = app_monitors.users.filter(u => u.id !== na_id); + if (app_monitors.discord_presence?.source.na_id === na_id) app_monitors.discord_presence = null; + await storage.setItem('AppMonitors', app_monitors); + } + + coral_session_token = undefined; + } + + if (moon_session_token && (!only || only.includes('moon')) && !only_cached) { + debugRemove('Removing moon session token'); + await storage.removeItem('NintendoAccountToken-pctl.' + na_id); + moon_session_token = undefined; + } + + if (!coral_session_token && !moon_session_token && selected === na_id) { + debugRemove('Deselecting user'); + await storage.removeItem('SelectedUser'); + await storage.removeItem('SessionToken'); + } + + const users = new Set(await storage.getItem('NintendoAccountIds') ?? []); + if (!coral_session_token && !moon_session_token && users.has(na_id)) { + debugRemove('Removing user from list'); + users.delete(na_id); + await storage.setItem('NintendoAccountIds', [...users]); + } +} + +async function removeSavedCoralTokenData(storage: persist.LocalStorage, session_token: string) { + await storage.removeItem('SessionToken'); + + await storage.removeItem('IksmToken.' + session_token); + await storage.removeItem('NookToken.' + session_token); + await storage.removeItem('NookUsers.' + session_token); + await storage.removeItem('BulletToken.' + session_token); + + for await (const {key, value} of iterateLocalStorage(storage)) { + if (key.startsWith('NookAuthToken.' + session_token + '.')) await storage.removeItem(key); + + if (value === session_token) await storage.removeItem(key); + } +} + +async function removeSavedMoonTokenData(storage: persist.LocalStorage, session_token: string) { + await storage.removeItem('MoonToken.' + session_token); +} diff --git a/src/cli/util/index.ts b/src/cli/util/index.ts index a135b98..f9ffe9b 100644 --- a/src/cli/util/index.ts +++ b/src/cli/util/index.ts @@ -4,3 +4,4 @@ export * as exportDiscordTitles from './export-discord-titles.js'; export * as discordActivity from './discord-activity.js'; export * as discordRpc from './discord-rpc.js'; export * as remoteConfig from './remote-config.js'; +export * as storage from './storage.js'; diff --git a/src/cli/util/storage.ts b/src/cli/util/storage.ts new file mode 100644 index 0000000..57e61b1 --- /dev/null +++ b/src/cli/util/storage.ts @@ -0,0 +1,43 @@ +import * as util from 'node:util'; +import createDebug from 'debug'; +import type { Arguments as ParentArguments } from '../util.js'; +import { Argv } from '../../util/yargs.js'; +import { initStorage, iterateLocalStorage } from '../../util/storage.js'; +import Table from './table.js'; +import { createHash } from 'node:crypto'; + +const debug = createDebug('cli:util:storage'); + +export const command = 'storage'; +export const desc = 'Manage node-persist data'; + +export function builder(yargs: Argv) { + return yargs.demandCommand().command('list', 'List all object', yargs => {}, async argv => { + const storage = await initStorage(argv.dataPath); + + const table = new Table({ + head: [ + 'File', + 'Key', + 'Value', + ], + colWidths: [10, 42, 80], + }); + + for await (const data of iterateLocalStorage(storage)) { + const value = util.inspect(data.value, { + compact: true, + }); + + table.push([ + createHash('md5').update(data.key).digest('hex'), + data.key.length > 40 ? data.key.substr(0, 37) + '...' : data.key, + value.length > 200 ? value.substr(0, 197) + '...' : value, + ]); + } + + table.sort((a, b) => a[1] > b[1] ? 1 : b[1] > a[1] ? -1 : 0); + + console.log(table.toString()); + }); +} diff --git a/src/util/storage.ts b/src/util/storage.ts index ecef8af..b0791b8 100644 --- a/src/util/storage.ts +++ b/src/util/storage.ts @@ -1,4 +1,5 @@ import * as path from 'node:path'; +import * as fs from 'node:fs/promises'; import createDebug from 'debug'; import persist from 'node-persist'; import getPaths from 'env-paths'; @@ -15,3 +16,16 @@ export async function initStorage(dir: string) { await storage.init(); return storage; } + +export async function* iterateLocalStorage(storage: persist.LocalStorage) { + const dir = (storage as unknown as {options: persist.InitOptions}).options.dir!; + + for await (const file of await fs.opendir(dir)) { + if (!file.isFile()) continue; + + const datum = await storage.readFile(path.join(dir, file.name)) as persist.Datum; + if (!datum || !datum.key) continue; + + yield datum; + } +}