From 2d2124c803d4da138b7b18f7e618712c31adb40c Mon Sep 17 00:00:00 2001 From: Nishant Arora <1895906+whizzzkid@users.noreply.github.com> Date: Mon, 3 Apr 2023 19:37:45 -0600 Subject: [PATCH] feat(mv3): :sparkles: Adding rule-recon logic --- .../lib/redirect-handler/blockOrObserve.ts | 200 ++++++++++++++---- 1 file changed, 155 insertions(+), 45 deletions(-) diff --git a/add-on/src/lib/redirect-handler/blockOrObserve.ts b/add-on/src/lib/redirect-handler/blockOrObserve.ts index 998f0d468..98b4ea7ef 100644 --- a/add-on/src/lib/redirect-handler/blockOrObserve.ts +++ b/add-on/src/lib/redirect-handler/blockOrObserve.ts @@ -1,17 +1,58 @@ import browser from 'webextension-polyfill' import debug from 'debug' import { CompanionState } from '../../types/companion.js' +import { dropSlash } from '../ipfs-path.js' const log = debug('ipfs-companion:redirect-handler:blockOrObserve') log.error = debug('ipfs-companion:redirect-handler:blockOrObserve:error') -const savedRegexFilters = new Map() +interface regexFilterMap { + id: number + regexSubstitution: string +} interface redirectHandlerInput { originUrl: string redirectUrl: string } +const savedRegexFilters: Record = {} +const DEFAULT_LOCAL_RULES: redirectHandlerInput[] = [ + { + originUrl: 'http://127.0.0.1', + redirectUrl: 'http://localhost' + }, + { + originUrl: 'http://[::1]', + redirectUrl: 'http://localhost' + } +] + + +/** + * + * @param url + * @returns + */ +export function isLocalHost(url: string): boolean { + return url.startsWith('http://127.0.0.1') || + url.startsWith('http://localhost') || + url.startsWith('http://[::1]') +} + +/** + * Escape the characters that are allowed in the URL, but not in the regex. + * + * @param str URL string to escape + * @returns + */ +function escapeURLRegex (str: string): string { + // these characters are allowed in the URL, but not in the regex. + // eslint-disable-next-line no-useless-escape + const ALLOWED_CHARS_URL_REGEX = /([:\/\?#\[\]@!$&'\(\ )\*\+,;=-_\.~])/g + return str.replace(ALLOWED_CHARS_URL_REGEX, '\\$1') +} + /** * Construct a regex filter and substitution for a redirect. * @@ -23,9 +64,6 @@ function constructRegexFilter ({ originUrl, redirectUrl }: redirectHandlerInput) regexSubstitution: string regexFilter: string } { - // these characters are allowed in the URL, but not in the regex. - // eslint-disable-next-line no-useless-escape - const ALLOWED_CHARS_URL_REGEX = /([:\/\?#\[\]@!$&'\(\ )\*\+,;=-_\.~])/g // We can traverse the URL from the end, and find the first character that is different. let commonIdx = 1 while (commonIdx < Math.min(originUrl.length, redirectUrl.length)) { @@ -36,11 +74,21 @@ function constructRegexFilter ({ originUrl, redirectUrl }: redirectHandlerInput) } // We can now construct the regex filter and substitution. - const regexSubstitution = redirectUrl.slice(0, redirectUrl.length - commonIdx + 1) + '\\1' + let regexSubstitution = redirectUrl.slice(0, redirectUrl.length - commonIdx + 1) + '\\1' // We need to escape the characters that are allowed in the URL, but not in the regex. - const regexFilterFirst = `${originUrl.slice(0, originUrl.length - commonIdx + 1).replace(ALLOWED_CHARS_URL_REGEX, '\\$1')}` + const regexFilterFirst = escapeURLRegex(originUrl.slice(0, originUrl.length - commonIdx + 1)) // We need to match the rest of the URL, so we can use a wildcard. - const regexFilter = `^${regexFilterFirst}(.*)$` + let regexFilter = `^${regexFilterFirst}(.*)$`.replace('https', 'https?') + + // This method does not parse: + // originUrl: "https://awesome.ipfs.io/" + // redirectUrl: "http://localhost:8081/ipns/awesome.ipfs.io/" + // that ends up with capturing all urls which we do not want. + if (regexFilter === '^https?\\:\\/(.*)$') { + const subdomain = new URL(originUrl).hostname + regexFilter = `^https?\\:\\/\\/${escapeURLRegex(subdomain)}(.*)$` + regexSubstitution = regexSubstitution.replace('\\1', `/${subdomain}\\1`) + } return { regexSubstitution, regexFilter } } @@ -58,6 +106,89 @@ export function getExtraInfoSpec (additionalParams: T[] = []): T[] { return additionalParams } +async function reconcileRulesAndRemoveOld(state: CompanionState): Promise { + const rules = await browser.declarativeNetRequest.getDynamicRules() + let addRules: browser.DeclarativeNetRequest.Rule[] = [] + let removeRuleIds: number[] = [] + for (const rule of rules) { + if (rule.action.type === 'redirect') { + if (!rule.action.redirect?.regexSubstitution?.includes(dropSlash(state.gwURLString)) || + savedRegexFilters[rule.condition.regexFilter as string]?.regexSubstitution !== rule.action.redirect?.regexSubstitution || + savedRegexFilters[rule.condition.regexFilter as string]?.id !== rule.id + ) { + // We need to remove the old rule. + removeRuleIds.push(rule.id) + delete savedRegexFilters[rule.condition.regexFilter as string] + } else { + savedRegexFilters[rule.condition.regexFilter as string] = { + id: rule.id, + regexSubstitution: rule.action.redirect?.regexSubstitution as string + } + } + } + } + for (const { originUrl, redirectUrl } of DEFAULT_LOCAL_RULES) { + const { port } = new URL(state.gwURLString) + const regexFilter = `^${escapeURLRegex(`${originUrl}:${port}`)}(.*)$` + const regexSubstitution = `${redirectUrl}:${port}\\1` + + if (!(regexFilter in savedRegexFilters)) { + // We need to add the new rule. + addRules.push(generateRule(regexFilter, regexSubstitution)) + } + } + await browser.declarativeNetRequest.updateDynamicRules({ addRules, removeRuleIds }) +} + +/** + * Generates a rule for the declarativeNetRequest API. + * + * @param regexFilter - The regex filter for the rule. + * @param regexSubstitution - The regex substitution for the rule. + * @param excludedInitiatorDomains - The domains that are excluded from the rule. + * @returns + */ +function generateRule( + regexFilter: string, + regexSubstitution: string, + excludedInitiatorDomains: string[] = [] +): browser.DeclarativeNetRequest.Rule { + // We need to generate a random ID for the rule. + const id = Math.floor(Math.random() * 29999) + // We need to save the regex filter and ID to check if the rule already exists later. + savedRegexFilters[regexFilter] = { id, regexSubstitution } + + return { + id, + priority: 1, + action: { + type: 'redirect', + redirect: { regexSubstitution } + }, + condition: { + regexFilter, + excludedInitiatorDomains, + resourceTypes: [ + 'csp_report', + 'font', + 'image', + 'main_frame', + 'media', + 'object', + 'other', + 'ping', + 'script', + 'stylesheet', + 'sub_frame', + 'webbundle', + 'websocket', + 'webtransport', + 'xmlhttprequest' + ] + } + } +} + /** * Register a redirect rule in the dynamic rule set. * @@ -71,56 +202,35 @@ export function addRuleToDynamicRuleSetGenerator ( const state = getState() // We don't want to redirect to the same URL. Or to the gateway. if (originUrl === redirectUrl || - (originUrl.includes(state.gwURL.host) && !redirectUrl.includes('recovery'))) { + (originUrl.includes(state.gwURL.host) && !redirectUrl.includes('recovery') || + (isLocalHost(redirectUrl) && isLocalHost(originUrl)))) { return } - // We need to generate a random ID for the rule. - const id = Math.floor(Math.random() * 29999) // We need to construct the regex filter and substitution. const { regexSubstitution, regexFilter } = constructRegexFilter({ originUrl, redirectUrl }) + // We need to check if the rule already exists. - if (!savedRegexFilters.has(regexFilter)) { + if (!(regexFilter in savedRegexFilters) || + savedRegexFilters[regexFilter].regexSubstitution !== regexSubstitution) { + let removeRuleIds: number[] = [] + if (regexFilter in savedRegexFilters) { + // We need to remove the old rule. + removeRuleIds.push(savedRegexFilters[regexFilter].id) + delete savedRegexFilters[regexFilter] + } + await browser.declarativeNetRequest.updateDynamicRules( { // We need to add the new rule. - addRules: [ - { - id, - priority: 1, - action: { - type: 'redirect', - redirect: { regexSubstitution } - }, - condition: { - regexFilter, - excludedInitiatorDomains: [state.gwURL.host], - resourceTypes: [ - 'csp_report', - 'font', - 'image', - 'main_frame', - 'media', - 'object', - 'other', - 'ping', - 'script', - 'stylesheet', - 'sub_frame', - 'webbundle', - 'websocket', - 'webtransport', - 'xmlhttprequest' - ] - } - } - ], + addRules: [generateRule(regexFilter, regexSubstitution)], // We need to remove the old rules. - removeRuleIds: await browser.declarativeNetRequest.getDynamicRules().then((rules) => rules.map((rule) => rule.id)) + removeRuleIds } ) - // We need to save the regex filter and ID to check if the rule already exists later. - savedRegexFilters.set(regexFilter, id.toString()) } + + // async call to reconcile rules and remove old ones. + await reconcileRulesAndRemoveOld(state) } }