Skip to content

Commit 98910a5

Browse files
authored
Fixing waste of tokens on browser open, force new rules on extension update, automatic reload after one-off double spend (#16)
This PR fixes an important and a minor bugs in the extensions, and improves UX: # Bug Before this patch, when opening the browser with the extension enabled, one token per endpoint (about 50) would be discarded automatically. This is now resolved. # Minor bug The way that the header rules were being applied for the /html/* search pages, was causing the wrong header being sent due to confusion between /html and /html/*. # UX Feature 1 After extension update, new header rules were not being automatically applied and the user had to manually disable and re-enable PP mode. This is now done automatically. # UX Feature 2 Sometimes due to a race-condition or a header rule update, the user may encounter an error message about a token having been already spent. With this update, we check if this error had already happened in the prior minute. If not, then the page displaying the error is reloaded using a new token, which likely resolves the problem. If the error had just happened (eg, if after the page reload the problem is not resolved), the new error page is left on display.
1 parent 9824ac7 commit 98910a5

9 files changed

+142
-19
lines changed

src/background.js

+34-8
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,11 @@ chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => {
8282

8383
chrome.runtime.onInstalled.addListener(async (details) => {
8484
// in install, enable the extension and fetch some tokens
85+
console.log("onInstalled")
8586
if (details.reason == "install") {
8687
await chrome.storage.local.set({ 'enabled': true });
87-
await setEnabled();
88-
await onStart();
88+
// onStart (which will be executed in approximately 1 second)
89+
// will pick up 'enabled': true and use it to enable the extension
8990
} else if (details.reason == "update") {
9091
// if extension was enabled before receiving the oupdate,
9192
// force a disable-enable cycle in order to apply any changes
@@ -116,18 +117,35 @@ async function onStart() {
116117
if (VERBOSE) {
117118
debug_log(`onStart: ${new Date().toISOString().match(/(\d{2}:){2}\d{2}/)[0]}`);
118119
}
119-
// reset enabled/disabled status depending on what the user left it as
120-
const { enabled } = await browser.storage.local.get({ 'enabled': false });
121-
if (enabled) {
120+
console.log(`onStart: ${new Date().toISOString().match(/(\d{2}:){2}\d{2}/)[0]}`);
121+
// The browser is being started up, or the extension being enabled.
122+
// When an extension is disabled or the browser is turned off,
123+
// the declarativeNetRequest rules used to send tokens to Kagi are removed.
124+
// However, there is no "onBrowserClose" or "onExtensionDisable" listener
125+
// allowing us to unload the tokens that were loaded in those rules.
126+
// If we don't do anything, those tokens will be lost.
127+
// Hence, we have to recover them from localStorage. An easy way that does not
128+
// require writing any new code is to trigger the code used when the PP mode toggle
129+
// is disabled.
130+
const was_enabled = (await browser.storage.local.get({ 'enabled': false }))['enabled'];
131+
// emulate PP mode being disabled
132+
await browser.storage.local.set({ 'enabled': false })
133+
await setDisabled();
134+
// if the extension was last enabled, simulate the PP mode toggle being enabled
135+
if (was_enabled) {
136+
await browser.storage.local.set({ 'enabled': true })
122137
await setEnabled();
123-
} else {
124-
await setDisabled();
125138
}
139+
// refresh extension icon
140+
await update_extension_icon(was_enabled);
126141
// when coming online, send status to Kagi Search extension
127142
await sendPPModeStatus();
128143
}
129144

130-
browser.runtime.onStartup.addListener(onStart)
145+
browser.runtime.onStartup.addListener(async () => {
146+
// dummy operation to make sure background.js is run
147+
await browser.runtime.getPlatformInfo();
148+
})
131149

132150
// -- keep background.js alive (to address non-persistency of manifest V3 extensions)
133151

@@ -146,3 +164,11 @@ chrome.alarms.onAlarm.addListener(async (info) => {
146164
await browser.runtime.getPlatformInfo();
147165
}
148166
});
167+
168+
// init the extension
169+
(async () => {
170+
console.log(`background.js: ${new Date().toISOString().match(/(\d{2}:){2}\d{2}/)[0]}`);
171+
setTimeout(async () => {
172+
await onStart();
173+
}, 1000)
174+
})()

src/popup/enable_toggle.js

+5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import {
2+
update_extension_icon
3+
} from '../scripts/icon.js'
4+
15
const enabled_checkbox = document.querySelector("#kagipp-enabled")
26
const status_message_indicator = document.querySelector("#status-message-indicator")
37

@@ -23,6 +27,7 @@ async function is_enabled() {
2327
const { enabled } = await browser.storage.local.get({ 'enabled': false })
2428
enabled_checkbox.checked = enabled;
2529
await update_indicator_opacity(enabled);
30+
await update_extension_icon(enabled);
2631
}
2732

2833
async function set_enabled() {

src/popup/popup.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ if (enabled_checkbox) {
135135

136136
(async () => {
137137
// try reading right away
138-
await is_enabled()
138+
await is_enabled();
139139
// add CSS transition style
140140
setTimeout(() => {
141141
let sheet = window.document.styleSheets[0];
@@ -145,7 +145,7 @@ if (enabled_checkbox) {
145145
setTimeout(() => {
146146
setIntervalAndFire(async () => {
147147
await is_enabled();
148-
});
148+
}, 1000);
149149
}, 1000)
150150
})()
151151

src/scripts/anonymization.js

+2
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ export const ACCEPT_QUICK_ANSWER_OFFSET = 152;
5757
export const ACCEPT_QUICK_ANSWER_DOC_OFFSET = 154;
5858
export const ACCEPT_TRANSLATE_JSON_OFFSET = 156;
5959
export const ACCEPT_TRANSLATE_TURSNTILE_OFFSET = 158;
60+
export const KAGI_HTML_SLASH_REDIRECT = 180;
61+
export const ONION_HTML_SLASH_REDIRECT = 182;
6062

6163
const INITIAL_HTTP_AUTHORIZATION_ID = 200
6264
const INITIAL_NO_TOKEN_REDIRECT_ID = 400

src/scripts/config.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -40,21 +40,21 @@ const REDEMPTION_SERVICES = [
4040
]
4141
let REDEMPTION_ENDPOINTS = [
4242
`${SCHEME}://${DOMAIN_PORT}/|`,
43-
`${SCHEME}://${DOMAIN_PORT}/html`,
43+
`${SCHEME}://${DOMAIN_PORT}/html|`,
4444
`${SCHEME}://${DOMAIN_PORT}/settings`,
4545
`${SCHEME}://${DOMAIN_PORT}/api/quick_settings/landing`,
4646
`${SCHEME}://${DOMAIN_PORT}/mother/context`,
4747
`${SCHEME}://${DOMAIN_PORT}/mother/summarize_document`,
4848
`${ONION_SCHEME}://${ONION_DOMAIN_PORT}/|`,
49-
`${ONION_SCHEME}://${ONION_DOMAIN_PORT}/html`,
49+
`${ONION_SCHEME}://${ONION_DOMAIN_PORT}/html|`,
5050
`${ONION_SCHEME}://${ONION_DOMAIN_PORT}/settings`,
5151
`${ONION_SCHEME}://${ONION_DOMAIN_PORT}/api/quick_settings/landing`,
5252
`${ONION_SCHEME}://${ONION_DOMAIN_PORT}/mother/context`,
5353
`${ONION_SCHEME}://${ONION_DOMAIN_PORT}/mother/summarize_document`
5454
]
5555
if (STAGING) {
5656
REDEMPTION_ENDPOINTS.push(`${SCHEME}://stage.${DOMAIN_PORT}/|`)
57-
REDEMPTION_ENDPOINTS.push(`${SCHEME}://stage.${DOMAIN_PORT}/html`)
57+
REDEMPTION_ENDPOINTS.push(`${SCHEME}://stage.${DOMAIN_PORT}/html|`)
5858
REDEMPTION_ENDPOINTS.push(`${SCHEME}://stage.${DOMAIN_PORT}/settings`)
5959
REDEMPTION_ENDPOINTS.push(`${SCHEME}://stage.${DOMAIN_PORT}/api/quick_settings/landing`)
6060
REDEMPTION_ENDPOINTS.push(`${SCHEME}://stage.${DOMAIN_PORT}/mother/context`)
@@ -108,7 +108,7 @@ export {
108108
let WEBREQUEST_REDEMPTION_ENDPOINTS = []
109109
for (let i = 0; i < REDEMPTION_ENDPOINTS.length; i++) {
110110
let endpoint = REDEMPTION_ENDPOINTS[i];
111-
if (endpoint.endsWith('/|')) {
111+
if (endpoint.endsWith('|')) {
112112
// webRequest does not recognize urlFilter's |
113113
// so we remove the trailing |
114114
endpoint = endpoint.substring(0, endpoint.length-1)

src/scripts/generation_and_redemption.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ async function setPPHeadersListener(details) {
181181
const url = new URL(details.url);
182182
const scheme_domain_port = url.origin;
183183
const pathname = url.pathname; // comes with a leading /
184-
const endpoint = (pathname == "/") ? `${scheme_domain_port}${pathname}|` : `${scheme_domain_port}${pathname}`;
184+
const endpoint = (pathname == "/" || pathname.endsWith('/html')) ? `${scheme_domain_port}${pathname}|` : `${scheme_domain_port}${pathname}`;
185185
await setPPHeaders(endpoint);
186186
}
187187

src/scripts/headers.js

+52
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import {
2424
ACCEPT_QUICK_ANSWER_OFFSET,
2525
ACCEPT_QUICK_ANSWER_DOC_OFFSET,
2626
ACCEPT_TRANSLATE_TURSNTILE_OFFSET,
27+
KAGI_HTML_SLASH_REDIRECT,
28+
ONION_HTML_SLASH_REDIRECT,
2729
ANONYMIZING_RULES_OFFSET,
2830
ANONYMIZING_RULESET,
2931
REFERER_RULES_OFFSET,
@@ -303,6 +305,54 @@ async function unsetLocaRedirectorHeader() {
303305
});
304306
}
305307

308+
async function setHTMLIndexRedirector() {
309+
if (VERBOSE) {
310+
debug_log(`setHTMLIndexRedirector`)
311+
}
312+
const rules = {
313+
addRules: [{
314+
id: KAGI_HTML_SLASH_REDIRECT,
315+
priority: 1,
316+
condition: {
317+
urlFilter: `||${DOMAIN_PORT}/html/|`,
318+
resourceTypes: ["main_frame", "sub_frame"]
319+
},
320+
action: {
321+
type: "redirect",
322+
redirect: {
323+
url: `https://${DOMAIN_PORT}/html`
324+
}
325+
}
326+
},{
327+
id: ONION_HTML_SLASH_REDIRECT,
328+
priority: 1,
329+
condition: {
330+
urlFilter: `||${ONION_DOMAIN_PORT}/html/|`,
331+
resourceTypes: ["main_frame", "sub_frame"]
332+
},
333+
action: {
334+
type: "redirect",
335+
redirect: {
336+
url: `http://${ONION_DOMAIN_PORT}/html`
337+
}
338+
}
339+
}],
340+
removeRuleIds: [KAGI_HTML_SLASH_REDIRECT, ONION_HTML_SLASH_REDIRECT]
341+
};
342+
343+
await browser.declarativeNetRequest.updateDynamicRules(rules);
344+
}
345+
346+
async function unsetHTMLIndexRedirector() {
347+
if (VERBOSE) {
348+
debug_log("unsetHTMLIndexRedirector");
349+
}
350+
await chrome.declarativeNetRequest.updateDynamicRules({
351+
addRules: [],
352+
removeRuleIds: [KAGI_HTML_SLASH_REDIRECT, ONION_HTML_SLASH_REDIRECT]
353+
});
354+
}
355+
306356
async function setAuthorizationHeader(endpoint, token_tuple) {
307357
if (VERBOSE) {
308358
debug_log(`[${endpoint}] ${token_tuple[0].substring(0, 32)}`)
@@ -376,6 +426,8 @@ export {
376426
setAntiFingerprintingRules,
377427
unsetAntiFingerprintingRules,
378428
setNoTokensRedirect,
429+
setHTMLIndexRedirector,
430+
unsetHTMLIndexRedirector,
379431
setAuthorizationHeader,
380432
unsetAuthorizationHeader,
381433
setLocaRedirectorHeader,

src/scripts/icon.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
async function update_extension_icon(enabled) {
22
if (typeof enabled === "undefined") {
3-
const { _enabled } = await browser.storage.local.get({ 'enabled': false })
4-
enabled = _enabled;
3+
enabled = (await browser.storage.local.get({ 'enabled': false }))['enabled'];
54
}
65
const path = enabled ? "enabled" : "disabled";
76
await chrome.action.setIcon({

src/scripts/toggle.js

+41-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ import {
1212
unsetAntiFingerprintingRules,
1313
setLocaRedirectorHeader,
1414
unsetLocaRedirectorHeader,
15-
selfRemovingUnsetRefererHeadersListener
15+
selfRemovingUnsetRefererHeadersListener,
16+
setHTMLIndexRedirector,
17+
unsetHTMLIndexRedirector
1618
} from './headers.js'
1719

1820
import {
@@ -53,7 +55,7 @@ async function checkingDoubleSpendListener(details) {
5355
const url = new URL(details.url);
5456
const scheme_domain_port = url.origin;
5557
const pathname = url.pathname; // comes with a leading /
56-
const endpoint = (pathname == "/") ? `${scheme_domain_port}${pathname}|` : `${scheme_domain_port}${pathname}`;
58+
const endpoint = (pathname == "/" || pathname.endsWith('/html')) ? `${scheme_domain_port}${pathname}|` : `${scheme_domain_port}${pathname}`;
5759
if (VERBOSE) {
5860
debug_log(`checkingDoubleSpendListener: ${details.statusCode} ${endpoint}`)
5961
}
@@ -62,7 +64,42 @@ async function checkingDoubleSpendListener(details) {
6264
if (VERBOSE) {
6365
debug_log(`> loading a new token for ${endpoint}`)
6466
}
67+
// a token was double spent, load the next one
6568
await setPPHeaders(endpoint);
69+
/*
70+
* The status at this line is:
71+
* - an error page is shown (or no results displayed in case of a /socket/ request failing)
72+
* - a new token is loaded for the endpoint that last failed
73+
* This may be a one-off error, eg due to an update in header rules, or a race condition.
74+
* If that's the case, we would rather reload the page now that a new token was set.
75+
* If the error has happened repeatedly though, this is not a good strategy,
76+
* as it may single out that there is a failing user making repeated queries, and also
77+
* loop infinitely.
78+
* Our approach here is the following:
79+
* - load the time of the last recorded double-spend
80+
* - if very recent, then this is a repeated failure, don't reload;
81+
* just show the error telling the user to check the documentation and possibly report the failure
82+
* - if the last error is not very recent, then automatically reload the page
83+
*/
84+
const now = (new Date()).getTime(); // unix time in milliseconds
85+
const { last_double_spend } = await browser.storage.local.get({ 'last_double_spend' : 0 });
86+
const gap = now - last_double_spend;
87+
await browser.storage.local.set({ 'last_double_spend' : now }); // update the last_double_spend information
88+
if (gap > 60 * 1000) { // if the last seen doublespend was more than 1 minute ago
89+
// likely one-off double-spend, reload the current page
90+
if (VERBOSE) {
91+
debug_log("checkingDoubleSpendListener: one-off double-spend, reloading");
92+
}
93+
const active_tabs_cur_window = await chrome.tabs.query({active: true, currentWindow: true});
94+
const cur_tab = active_tabs_cur_window[0];
95+
if (cur_tab && cur_tab.id && cur_tab.id == details.tabId) {
96+
await browser.tabs.reload(details.tabId, { bypassCache: true });
97+
}
98+
} else {
99+
if (VERBOSE) {
100+
debug_log("checkingDoubleSpendListener: repeated double-spend, not reloading");
101+
}
102+
}
66103
} else if (details.statusCode == 403) {
67104
// let the user know that their tokens are stale
68105
// realistically, this should only happen to devs debugging against staging
@@ -96,6 +133,7 @@ async function setEnabled() {
96133
await setRefererRules();
97134
await setAntiFingerprintingRules();
98135
await setLocaRedirectorHeader();
136+
await setHTMLIndexRedirector();
99137
for (let i = 0; i < REDEMPTION_ENDPOINTS.length; i++) {
100138
let endpoint = REDEMPTION_ENDPOINTS[i];
101139
await setPPHeaders(endpoint);
@@ -123,6 +161,7 @@ async function setDisabled() {
123161
}
124162
await unsetAntiFingerprintingRules();
125163
await unsetLocaRedirectorHeader();
164+
await unsetHTMLIndexRedirector();
126165
for (let i = 0; i < REDEMPTION_ENDPOINTS.length; i++) {
127166
let endpoint = REDEMPTION_ENDPOINTS[i];
128167
await unsetPPHeaders(endpoint);

0 commit comments

Comments
 (0)