From ac948be9570a89521aa4bfb8b01656d852662609 Mon Sep 17 00:00:00 2001 From: Fernando Virdia Date: Fri, 13 Dec 2024 16:53:07 -0300 Subject: [PATCH 1/9] Code for communicating with Privacy Pass extension --- shared/src/background.js | 122 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 119 insertions(+), 3 deletions(-) diff --git a/shared/src/background.js b/shared/src/background.js index 14e844b..40a979d 100644 --- a/shared/src/background.js +++ b/shared/src/background.js @@ -83,8 +83,6 @@ async function saveToken( return; } - await updateRules(); - // tell the extension popup to update the UI await browser.runtime.sendMessage({ type: 'synced', @@ -145,6 +143,13 @@ async function updateRules() { }); } +async function removeRules() { + await browser.declarativeNetRequest.updateDynamicRules({ + addRules: [], + removeRuleIds: [1], + }); +} + /* * Attempts to grab sessions from existing Kagi windows. * This allows us to track the users last session without @@ -168,10 +173,32 @@ async function checkForSession() { await saveToken({ token, sync: true }); } + // we want to always make sure to update the rules, even if the sessionToken did not change + // this allows the header to be reapplied in the case where the PP extension is used to set + // PP mode on, and then the extension is uninstalled (without setting PP mode off). + await updateRules(); +} + +async function applyHeader() { + // check if PP mode is enabled, if so remove X-Kagi-Authorization header + await requestPPMode(); + + const pp_mode_enabled = await isPPModeEnabled(); + if (pp_mode_enabled) { + // we reset syncSessionFromExisting so that once PP mode is set off + // (or if the PP extension is uninstalled), checkForSession() reapplies + // the X-Kagi-Authorize header + syncSessionFromExisting = true; + await removeRules(); + return; + } + + // PP mode is not enabled, proceed with header application + await checkForSession(); } browser.webRequest.onBeforeRequest.addListener( - checkForSession, + applyHeader, { urls: ['https://*.kagi.com/*'] }, [], ); @@ -290,3 +317,92 @@ browser.contextMenus.onClicked.addListener(async (info, tab) => { kagiImageSearch(info, tab); } }); + +// Communication with Kagi Privacy Pass extension + +/* + The following code is designed for Firefox. This should not affect compatibility with Safari, + for which we are not publishing a Privacy Pass extension. +*/ + +/* + This extension makes the browser send a custom X-Kagi-Authorization header + to kagi.com, to authenticate users even when using incognito mode. + This can enter a "race condition" with the Kagi Privacy Pass extension, + which strips all de-anonymising information sent to kagi.com, such as X-Kagi-Authorization, + whenever "Privacy Pass mode" is in use. + + To avoid this race, we let the two extensions communicate, so that this extenesion removes + (respectively, adds) the header when "Privacy Pass mode" is active (respectively, "PP mode" + is inactive or the other extension is not installed/enabled). + + We achieve this syncronization with a simple messaging protocol outlined below: + + The Privacy Pass extension will send this extension single messages: + - When being enabled (installed, activated) reports whether "PP mode" is enabled + - When activating/deactivating "PP mode" + Due to Chromium extension limitations, it cannot send a message when uninstalled/deactivated. + + The main extension (this one) keeps track of whether the "PP mode" is acrive or not by keeping state. + This state is updated by the following actions: + - When this extension is being enabled (installed, activated), it asks the PP extension for the "PP mode". + - When it receives a status report from the PP extension, updates its state. + + Having both extensions send / request the "PP mode" status allows for the following: + - When both are installed and active, whenever "PP mode" is toggled, this extension is informed and adjusts + - Whenever one extension is installed, it attempts to sync with the other on whether "PP mode" is active + + There is one limitation, due to the PP extension being unable to signal to this one that it was uninstalled. + This means that in theory, one could have a scenario where first PP mode is enabled, this extension removes + X-Kagi-Authorization, and then the PP extension is uninstalled. In Incognito mode, where the kagi_session + cookie is not sent by the browser, this would cause failed authentication with Kagi. + + Possible solutions: + 1. have PP extension open a URL on uninstall, that signals this extension to update the header. This is possible + but it means adding an extra new tab on uninstall. + 2. Have this extension periodically poll whether the other one was uninstalled. This adds needless communication. + Polling only when applying the header is not sufficient (as the PP extension could be uninstalled without + webRequest.onBeforeRequest being triggered). + + In practice neither of these solutions seems necessary. Instead, we have this extension poll the PP extension every + time it checks whether to apply the header. This means that even in the case where the PP extension is uninstalled while + PP mode was set on, at most one query to kagi.com will fail to authenticate. Such query will then trigger webRequest.onBeforeRequest, + which will then find out the PP extension was uninstalled, and hence reinstate X-Kagi-Authorize. +*/ + +const KAGI_PRIVACY_PASS_EXTENSION_ID = "privacypass@kagi.com"; + +async function requestPPMode() { + let pp_mode_enabled = false; + try { + pp_mode_enabled = await browser.runtime.sendMessage(KAGI_PRIVACY_PASS_EXTENSION_ID, "status_report"); + } catch (ex) { + // other end does not exist, likely Privacy Pass extension disabled/not installed + pp_mode_enabled = false; // PP mode not enabled + } + await browser.storage.local.set({ "pp_mode_enabled": pp_mode_enabled }) +} + +async function isPPModeEnabled() { + const { pp_mode_enabled } = await browser.storage.local.get({ "pp_mode_enabled": false }); + return pp_mode_enabled; +} + +// PP extension sent an unsolicited status report +// We update our internal assumption, and update header application +browser.runtime.onMessageExternal.addListener(async (request, sender, sendResponse) => { + if (sender.id !== KAGI_PRIVACY_PASS_EXTENSION_ID) { + // ignore messages from extensions other than the PP one + return; + } + // check the message is about the PP mode + if ('enabled' in request) { + // update X-Kagi-Authorization header application + await applyHeader(); + } +}); + +// when extension is started, ask for status report, and apply header accordingly +(async () => { + await applyHeader(); +})(); From 2959eadebdc9b86233bffe7cb473758aba896043 Mon Sep 17 00:00:00 2001 From: Fernando Virdia Date: Wed, 22 Jan 2025 01:07:29 +0000 Subject: [PATCH 2/9] guarding Privacy Pass code to run only inside Firefox and not Safari. Also fixing a bug that would stop Firefox from saving the session token --- shared/src/background.js | 96 +++++++++++++++++++++++----------------- 1 file changed, 55 insertions(+), 41 deletions(-) diff --git a/shared/src/background.js b/shared/src/background.js index ce9297c..91624aa 100644 --- a/shared/src/background.js +++ b/shared/src/background.js @@ -19,7 +19,7 @@ let IS_CHROME = true; // Very hacky, but currently works flawlessly if (typeof browser.runtime.getBrowserInfo === 'function') { - IS_CHROME = false; + IS_CHROME = false; // really, this test for Firefox, not for Chrome } // Force acceptance since we do not show the policy on chrome. @@ -83,6 +83,8 @@ async function saveToken( return; } + await applyHeader(true); + // tell the extension popup to update the UI await browser.runtime.sendMessage({ type: 'synced', @@ -155,23 +157,25 @@ async function removeRules() { * This allows us to track the users last session without * having to force them to input it in to the extension. */ -async function checkForSession() { - if (!syncSessionFromExisting) return; - if (!sessionPrivacyConsent) return; - - const cookie = await browser.cookies.get({ - url: 'https://kagi.com', - name: 'kagi_session', - }); +async function checkForSession(isManual = false) { + if (!isManual) { + if (!syncSessionFromExisting) return; + if (!sessionPrivacyConsent) return; + + const cookie = await browser.cookies.get({ + url: 'https://kagi.com', + name: 'kagi_session', + }); - if (!cookie || !cookie.value) return; + if (!cookie || !cookie.value) return; - const token = cookie.value; + const token = cookie.value; - if (sessionToken !== token) { - sessionToken = token; + if (sessionToken !== token) { + sessionToken = token; - await saveToken({ token, sync: true }); + await saveToken({ token, sync: true }); + } } // we want to always make sure to update the rules, even if the sessionToken did not change // this allows the header to be reapplied in the case where the PP extension is used to set @@ -179,22 +183,24 @@ async function checkForSession() { await updateRules(); } -async function applyHeader() { - // check if PP mode is enabled, if so remove X-Kagi-Authorization header - await requestPPMode(); - - const pp_mode_enabled = await isPPModeEnabled(); - if (pp_mode_enabled) { - // we reset syncSessionFromExisting so that once PP mode is set off - // (or if the PP extension is uninstalled), checkForSession() reapplies - // the X-Kagi-Authorize header - syncSessionFromExisting = true; - await removeRules(); - return; +async function applyHeader(isManual = false) { + if (!IS_CHROME) { + // check if PP mode is enabled, if so remove X-Kagi-Authorization header + await requestPPMode(); + + const pp_mode_enabled = await isPPModeEnabled(); + if (pp_mode_enabled) { + // we reset syncSessionFromExisting so that once PP mode is set off + // (or if the PP extension is uninstalled), checkForSession() reapplies + // the X-Kagi-Authorize header + syncSessionFromExisting = true; + await removeRules(); + return; + } } // PP mode is not enabled, proceed with header application - await checkForSession(); + await checkForSession(isManual); } browser.webRequest.onBeforeRequest.addListener( @@ -373,9 +379,12 @@ if (browser.contextMenus !== undefined) { which will then find out the PP extension was uninstalled, and hence reinstate X-Kagi-Authorize. */ -const KAGI_PRIVACY_PASS_EXTENSION_ID = "privacypass@kagi.com"; +const KAGI_PRIVACY_PASS_EXTENSION_ID = "privacypass@kagi.com"; // Firefox only async function requestPPMode() { + if (IS_CHROME) { + return; + } let pp_mode_enabled = false; try { pp_mode_enabled = await browser.runtime.sendMessage(KAGI_PRIVACY_PASS_EXTENSION_ID, "status_report"); @@ -387,23 +396,28 @@ async function requestPPMode() { } async function isPPModeEnabled() { + if (IS_CHROME) { + return false; + } const { pp_mode_enabled } = await browser.storage.local.get({ "pp_mode_enabled": false }); return pp_mode_enabled; } -// PP extension sent an unsolicited status report -// We update our internal assumption, and update header application -browser.runtime.onMessageExternal.addListener(async (request, sender, sendResponse) => { - if (sender.id !== KAGI_PRIVACY_PASS_EXTENSION_ID) { - // ignore messages from extensions other than the PP one - return; - } - // check the message is about the PP mode - if ('enabled' in request) { - // update X-Kagi-Authorization header application - await applyHeader(); - } -}); +if (!IS_CHROME) { + // PP extension sent an unsolicited status report + // We update our internal assumption, and update header application + browser.runtime.onMessageExternal.addListener(async (request, sender, sendResponse) => { + if (sender.id !== KAGI_PRIVACY_PASS_EXTENSION_ID) { + // ignore messages from extensions other than the PP one + return; + } + // check the message is about the PP mode + if ('enabled' in request) { + // update X-Kagi-Authorization header application + await applyHeader(); + } + }); +} // when extension is started, ask for status report, and apply header accordingly (async () => { From acb6d2298eb1494490b93bc1c842f69c4d530c03 Mon Sep 17 00:00:00 2001 From: Fernando Virdia Date: Wed, 22 Jan 2025 02:10:38 +0000 Subject: [PATCH 3/9] Fixing bug causing token no to be recovered by extension --- shared/src/background.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/shared/src/background.js b/shared/src/background.js index 91624aa..627ede9 100644 --- a/shared/src/background.js +++ b/shared/src/background.js @@ -204,7 +204,9 @@ async function applyHeader(isManual = false) { } browser.webRequest.onBeforeRequest.addListener( - applyHeader, + async (details) => { + await applyHeader() + }, { urls: ['https://*.kagi.com/*'] }, [], ); From 95b86ef43cbf3d62cc3d741cabd98cda14aafb48 Mon Sep 17 00:00:00 2001 From: Fernando Virdia Date: Sun, 26 Jan 2025 00:52:17 +0000 Subject: [PATCH 4/9] update of autosuggest endpoint --- firefox/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firefox/manifest.json b/firefox/manifest.json index 9b5d5c3..d77bb9a 100644 --- a/firefox/manifest.json +++ b/firefox/manifest.json @@ -33,7 +33,7 @@ "favicon_url": "icons/icon_32px.png", "keyword": "@kagi", "is_default": true, - "suggest_url": "https://kagi.com/api/autosuggest?q={searchTerms}", + "suggest_url": "https://kagisuggest.com/api/autosuggest?q={searchTerms}", "encoding": "UTF-8" } }, From b10523c56a7457aa97d7f9ae9ef5f61b4c93bc29 Mon Sep 17 00:00:00 2001 From: Fernando Virdia Date: Sun, 26 Jan 2025 00:52:41 +0000 Subject: [PATCH 5/9] enable/disable summarize menu entry depending on PP mode --- shared/src/background.js | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/shared/src/background.js b/shared/src/background.js index 627ede9..5017075 100644 --- a/shared/src/background.js +++ b/shared/src/background.js @@ -183,6 +183,18 @@ async function checkForSession(isManual = false) { await updateRules(); } +function createSummarizeMenuEntry() { + browser.contextMenus.create({ + id: 'kagi-summarize', + title: 'Kagi Summarize', + contexts: ['link', 'page'], // Show the menu item when clicked on a link or elsewhere on page with no matching contexts + }); +} + +function removeSummarizeMenuEntry() { + browser.contextMenus.remove('kagi-summarize'); +} + async function applyHeader(isManual = false) { if (!IS_CHROME) { // check if PP mode is enabled, if so remove X-Kagi-Authorization header @@ -195,10 +207,16 @@ async function applyHeader(isManual = false) { // the X-Kagi-Authorize header syncSessionFromExisting = true; await removeRules(); + + // disable summarizer button + removeSummarizeMenuEntry(); return; } } + // enable summarizer button + createSummarizeMenuEntry(); + // PP mode is not enabled, proceed with header application await checkForSession(isManual); } @@ -303,11 +321,9 @@ function kagiImageSearch(info) { // FF Android does not support context menus if (browser.contextMenus !== undefined) { // Create a context menu item. - browser.contextMenus.create({ - id: 'kagi-summarize', - title: 'Kagi Summarize', - contexts: ['link', 'page'], // Show the menu item when clicked on a link or elsewhere on page with no matching contexts - }); + + // a context menu item for Summarize is added in applyHeader() + // to match the status of Privacy Pass browser.contextMenus.create({ id: 'kagi-image-search', @@ -331,11 +347,6 @@ if (browser.contextMenus !== undefined) { // Communication with Kagi Privacy Pass extension -/* - The following code is designed for Firefox. This should not affect compatibility with Safari, - for which we are not publishing a Privacy Pass extension. -*/ - /* This extension makes the browser send a custom X-Kagi-Authorization header to kagi.com, to authenticate users even when using incognito mode. From 2ec8105db5b381fb9a63a63a9b915589205ad8d8 Mon Sep 17 00:00:00 2001 From: Fernando Virdia Date: Sun, 26 Jan 2025 00:59:26 +0000 Subject: [PATCH 6/9] applied linter suggestions --- shared/src/background.js | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/shared/src/background.js b/shared/src/background.js index 5017075..50c0d3b 100644 --- a/shared/src/background.js +++ b/shared/src/background.js @@ -223,7 +223,7 @@ async function applyHeader(isManual = false) { browser.webRequest.onBeforeRequest.addListener( async (details) => { - await applyHeader() + await applyHeader(); }, { urls: ['https://*.kagi.com/*'] }, [], @@ -392,7 +392,7 @@ if (browser.contextMenus !== undefined) { which will then find out the PP extension was uninstalled, and hence reinstate X-Kagi-Authorize. */ -const KAGI_PRIVACY_PASS_EXTENSION_ID = "privacypass@kagi.com"; // Firefox only +const KAGI_PRIVACY_PASS_EXTENSION_ID = 'privacypass@kagi.com'; // Firefox only async function requestPPMode() { if (IS_CHROME) { @@ -400,36 +400,43 @@ async function requestPPMode() { } let pp_mode_enabled = false; try { - pp_mode_enabled = await browser.runtime.sendMessage(KAGI_PRIVACY_PASS_EXTENSION_ID, "status_report"); + pp_mode_enabled = await browser.runtime.sendMessage( + KAGI_PRIVACY_PASS_EXTENSION_ID, + 'status_report', + ); } catch (ex) { // other end does not exist, likely Privacy Pass extension disabled/not installed pp_mode_enabled = false; // PP mode not enabled } - await browser.storage.local.set({ "pp_mode_enabled": pp_mode_enabled }) + await browser.storage.local.set({ pp_mode_enabled: pp_mode_enabled }); } async function isPPModeEnabled() { if (IS_CHROME) { return false; } - const { pp_mode_enabled } = await browser.storage.local.get({ "pp_mode_enabled": false }); + const { pp_mode_enabled } = await browser.storage.local.get({ + pp_mode_enabled: false, + }); return pp_mode_enabled; } if (!IS_CHROME) { // PP extension sent an unsolicited status report // We update our internal assumption, and update header application - browser.runtime.onMessageExternal.addListener(async (request, sender, sendResponse) => { - if (sender.id !== KAGI_PRIVACY_PASS_EXTENSION_ID) { - // ignore messages from extensions other than the PP one - return; - } - // check the message is about the PP mode - if ('enabled' in request) { - // update X-Kagi-Authorization header application - await applyHeader(); - } - }); + browser.runtime.onMessageExternal.addListener( + async (request, sender, sendResponse) => { + if (sender.id !== KAGI_PRIVACY_PASS_EXTENSION_ID) { + // ignore messages from extensions other than the PP one + return; + } + // check the message is about the PP mode + if ('enabled' in request) { + // update X-Kagi-Authorization header application + await applyHeader(); + } + }, + ); } // when extension is started, ask for status report, and apply header accordingly From abd3d319b51e0067396b816fa4bdfb0345beca52 Mon Sep 17 00:00:00 2001 From: Fernando Virdia Date: Sun, 26 Jan 2025 01:00:57 +0000 Subject: [PATCH 7/9] version bump --- firefox/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firefox/manifest.json b/firefox/manifest.json index d77bb9a..484b960 100644 --- a/firefox/manifest.json +++ b/firefox/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Kagi Search for Firefox", - "version": "0.7.4", + "version": "0.7.5", "description": "A simple helper extension for setting Kagi as a default search engine, and automatically logging in to Kagi in incognito browsing windows.", "background": { "page": "src/background_page.html" From 57f79c2535ff8707dca290bee4c03162d9fad547 Mon Sep 17 00:00:00 2001 From: Fernando Virdia Date: Sun, 26 Jan 2025 01:41:04 +0000 Subject: [PATCH 8/9] bugfixes to support FF for Android --- firefox/manifest.json | 2 +- shared/src/background.js | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/firefox/manifest.json b/firefox/manifest.json index 484b960..084a222 100644 --- a/firefox/manifest.json +++ b/firefox/manifest.json @@ -49,7 +49,7 @@ "browser_specific_settings": { "gecko": { "id": "search@kagi.com", - "strict_min_version": "102.0" + "strict_min_version": "120.0" } } } diff --git a/shared/src/background.js b/shared/src/background.js index 50c0d3b..c874b1a 100644 --- a/shared/src/background.js +++ b/shared/src/background.js @@ -184,15 +184,21 @@ async function checkForSession(isManual = false) { } function createSummarizeMenuEntry() { - browser.contextMenus.create({ - id: 'kagi-summarize', - title: 'Kagi Summarize', - contexts: ['link', 'page'], // Show the menu item when clicked on a link or elsewhere on page with no matching contexts - }); + // FF Android does not support context menus + if (browser.contextMenus !== undefined) { + browser.contextMenus.create({ + id: 'kagi-summarize', + title: 'Kagi Summarize', + contexts: ['link', 'page'], // Show the menu item when clicked on a link or elsewhere on page with no matching contexts + }); + } } function removeSummarizeMenuEntry() { - browser.contextMenus.remove('kagi-summarize'); + // FF Android does not support context menus + if (browser.contextMenus !== undefined) { + browser.contextMenus.remove('kagi-summarize'); + } } async function applyHeader(isManual = false) { From 1b09aba0ad051fcb38795fe61da0fa5e152f78ac Mon Sep 17 00:00:00 2001 From: Fernando Virdia Date: Sat, 1 Feb 2025 04:33:07 +0000 Subject: [PATCH 9/9] using onStarted listener to make first call to applyHeaders --- firefox/manifest.json | 2 +- shared/src/background.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/firefox/manifest.json b/firefox/manifest.json index 084a222..83e0c60 100644 --- a/firefox/manifest.json +++ b/firefox/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Kagi Search for Firefox", - "version": "0.7.5", + "version": "0.7.6", "description": "A simple helper extension for setting Kagi as a default search engine, and automatically logging in to Kagi in incognito browsing windows.", "background": { "page": "src/background_page.html" diff --git a/shared/src/background.js b/shared/src/background.js index c874b1a..3f6b5e1 100644 --- a/shared/src/background.js +++ b/shared/src/background.js @@ -446,6 +446,6 @@ if (!IS_CHROME) { } // when extension is started, ask for status report, and apply header accordingly -(async () => { +browser.runtime.onStartup.addListener(async (details) => { await applyHeader(); -})(); +}) \ No newline at end of file