diff --git a/packages/phishing/src/bundle.ts b/packages/phishing/src/bundle.ts index bbcca9d75..082c6f383 100644 --- a/packages/phishing/src/bundle.ts +++ b/packages/phishing/src/bundle.ts @@ -12,7 +12,15 @@ export { packageInfo } from './packageInfo'; interface Cache { end: number; - list: T | null; + list: T; +} + +interface CacheAddrList extends Cache { + u8a: [string, Uint8Array[]][]; +} + +interface CacheHostList extends Cache { + parts: string[][]; } // Equivalent to https://raw.githubusercontent.com/polkadot-js/phishing/master/{address,all}.json @@ -21,79 +29,57 @@ const ADDRESS_JSON = `${PHISHING}/address.json`; // 45 minutes cache refresh const CACHE_TIMEOUT = 45 * 60 * 1000; -let cacheAddrU8a: [string, Uint8Array[]][] | null = null; -const cacheAddr: Cache = { +const cacheAddr: CacheAddrList = { end: 0, - list: null -}; -const cacheHost: Record> = { - __: { - end: 0, - list: null - } + list: {}, + u8a: [] }; +const cacheHost: Record = {}; -function extractHostParts (host: string): string[] { +function splitHostParts (host: string): string[] { return host - // remove protocol - .replace(/https:\/\/|http:\/\/|wss:\/\/|ws:\/\//, '') - // get the domain-only part - .split('/')[0] // split domain .split('.') // reverse order .reverse(); } -/** - * Retrieve a list of known phishing addresses - */ -export async function retrieveAddrList (allowCached = true): Promise { +function extractHostParts (host: string): string[] { + return splitHostParts( + host + // remove protocol + .replace(/https:\/\/|http:\/\/|wss:\/\/|ws:\/\//, '') + // get the domain-only part + .split('/')[0] + ); +} + +async function retrieveAddrCache (allowCached = true): Promise { const now = Date.now(); - if (allowCached && cacheAddr.list && (now < cacheAddr.end)) { - return cacheAddr.list; + if (allowCached && (now < cacheAddr.end)) { + return cacheAddr; } const list = await fetchJson(ADDRESS_JSON); cacheAddr.end = now + CACHE_TIMEOUT; cacheAddr.list = list; + cacheAddr.u8a = Object.entries(list).map(([key, addresses]): [string, Uint8Array[]] => + [key, addresses.map((a) => decodeAddress(a))] + ); - return list; + return cacheAddr; } -/** - * Retrieve a list of known phishing addresses in raw Uint8Array format - */ -async function retrieveAddrU8a (allowCached = true): Promise<[string, Uint8Array[]][]> { +async function retrieveHostCache (allowCached = true, root = '*'): Promise { const now = Date.now(); - if (allowCached && cacheAddrU8a && (now < cacheAddr.end)) { - return cacheAddrU8a; + if (allowCached && cacheHost[root] && (now < cacheHost[root].end)) { + return cacheHost[root]; } - const all = await retrieveAddrList(allowCached); - - cacheAddrU8a = Object - .entries(all) - .map(([key, addresses]): [string, Uint8Array[]] => - [key, addresses.map((a) => decodeAddress(a))] - ); - - return cacheAddrU8a; -} - -/** - * Retrieve allow/deny from our list provider - */ -export async function retrieveHostList (allowCached = true, root = '*'): Promise { - const now = Date.now(); - let list = allowCached && cacheHost[root] && (now < cacheHost[root].end) && cacheHost[root].list; - - if (list) { - return list; - } + let list: HostList; try { list = root === '*' @@ -108,31 +94,54 @@ export async function retrieveHostList (allowCached = true, root = '*'): Promise cacheHost[root] = { end: now + CACHE_TIMEOUT, - list + list, + parts: list.deny.map((h) => splitHostParts(h)) }; - return list; + return cacheHost[root]; } -function checkHostParts (items: string[], hostParts: string[]): boolean { - return items.some((item): boolean => { - const checkParts = extractHostParts(item); - - // first we need to ensure it has less or equal parts to our source - if (checkParts.length > hostParts.length) { - return false; - } - +function checkHostParts (items: CacheHostList['parts'], hostParts: string[]): boolean { + return items.some((parts) => + // out list entry needs to have the same (or less) parts + (parts.length <= hostParts.length) && // ensure each section matches - return checkParts.every((part, index) => hostParts[index] === part); - }); + parts.every((part, index) => hostParts[index] === part) + ); +} + +/** + * Retrieve a list of known phishing addresses + * + * @deprecated While not due for removal, it is suggested to rather use the + * checkAddress function (which is more optimal overall) + */ +export async function retrieveAddrList (allowCached = true): Promise { + const cache = await retrieveAddrCache(allowCached); + + return cache.list; +} + +/** + * Retrieve allow/deny from our list provider + * + * @deprecated While not due for removal, it is suggested to rather use the + * checkIfDenied function (which is more optimal overall) + */ +export async function retrieveHostList (allowCached = true, root = '*'): Promise { + const cache = await retrieveHostCache(allowCached, root); + + return cache.list; } /** * Checks a host to see if it appears in the provided list + * + * @deprecated While not due for removal, it is suggested to rather use the + * checkIfDenied function (which is more optimal overall) */ -export function checkHost (items: string[], host: string): boolean { - return checkHostParts(items, extractHostParts(host)); +export function checkHost (list: string[], host: string): boolean { + return checkHostParts(list.map((h) => splitHostParts(h)), extractHostParts(host)); } /** @@ -142,10 +151,12 @@ export function checkHost (items: string[], host: string): boolean { export async function checkAddress (address: string | Uint8Array, allowCached = true): Promise { try { const u8a = decodeAddress(address); - const all = await retrieveAddrU8a(allowCached); - const entry = all.find(([, all]) => all.some((a) => u8aEq(a, u8a))) || [null]; + const cache = await retrieveAddrCache(allowCached); + const entry = cache.u8a.find(([, u8as]) => + u8as.some((a) => u8aEq(a, u8a)) + ); - return entry[0]; + return (entry && entry[0]) || null; } catch { return null; } @@ -158,9 +169,9 @@ export async function checkAddress (address: string | Uint8Array, allowCached = export async function checkIfDenied (host: string, allowCached = true): Promise { try { const hostParts = extractHostParts(host); - const { deny } = await retrieveHostList(allowCached, hostParts[0]); + const cache = await retrieveHostCache(allowCached, hostParts[0]); - return checkHostParts(deny, hostParts); + return checkHostParts(cache.parts, hostParts); } catch { return false; } diff --git a/packages/phishing/src/index.spec.ts b/packages/phishing/src/index.spec.ts index 31003da9b..c80d17395 100644 --- a/packages/phishing/src/index.spec.ts +++ b/packages/phishing/src/index.spec.ts @@ -22,10 +22,13 @@ describe('checkIfDenied', (): void => { ).toEqual(true); }); - it('returns true when host in list (www-prefix)', async (): Promise => { + it('returns true when host + subdoimain is in list', async (): Promise => { expect( await checkIfDenied('www.polkadotfund.com') ).toEqual(true); + expect( + await checkIfDenied('some.where.polkadotfund.com') + ).toEqual(true); }); it('returns true when host in list (protocol)', async (): Promise => { @@ -40,9 +43,9 @@ describe('checkIfDenied', (): void => { ).toEqual(true); }); - it('returns true in list (protocol + path + #)', async (): Promise => { + it('returns true in list (protocol + sub + host + path + #)', async (): Promise => { expect( - await checkIfDenied('https://robonomics-network-xrt.cyberfi-tech-rewards-programs-claims-erc20-token.com/myetherwallet/access-my-wallet/#/input-privatekey-mnemonic-phrase-claim-bonus') + await checkIfDenied('https://subdomain.robonomics-network-xrt.cyberfi-tech-rewards-programs-claims-erc20-token.com/myetherwallet/access-my-wallet/#/input-privatekey-mnemonic-phrase-claim-bonus') ).toEqual(true); }); });