aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorpraschke <stel@comfy.monster>2023-10-29 21:20:29 +0000
committerpraschke <stel@comfy.monster>2023-10-29 21:38:13 +0000
commite61a69fb9ed8ad1dc94b4695d9b9052f4a533a52 (patch)
tree784a12cb846295ee243953759b7faaad8d310e75
parentba8eec942c60cc8b676408efd99e3fbbb9670c06 (diff)
remove webRequest and webRequestBlocking
firefox was previously unable to use declarativeNetRequest, as some browser state (ExtensionDNRStore) wasn't correctly initialized wrt yomitan's use of the DNR API. this bug manifested as an unexpected error on calls to updateDynamicRules(), specifically after the browser has been restarted. switching to the use of session rules instead of dynamic rules fixes this bug. i have tested audio info requests (custom JSON, JPod Alternate, Jisho) that exhibited the bug after browser restart on version 115 and 118, and the audio plays instead of the request failing. webRequest can now be entirely removed.
-rw-r--r--dev/data/manifest-variants.json25
-rw-r--r--docs/permissions.md7
-rw-r--r--ext/js/background/request-builder.js306
-rw-r--r--ext/permissions.html33
4 files changed, 74 insertions, 297 deletions
diff --git a/dev/data/manifest-variants.json b/dev/data/manifest-variants.json
index 858dba65..4d4ec301 100644
--- a/dev/data/manifest-variants.json
+++ b/dev/data/manifest-variants.json
@@ -249,31 +249,6 @@
]
},
{
- "action": "add",
- "path": [
- "permissions"
- ],
- "items": [
- "webRequest"
- ]
- },
- {
- "action": "add",
- "path": [
- "permissions"
- ],
- "items": [
- "webRequestBlocking"
- ]
- },
- {
- "action": "remove",
- "path": [
- "permissions"
- ],
- "item": "declarativeNetRequest"
- },
- {
"action": "remove",
"path": [
"permissions"
diff --git a/docs/permissions.md b/docs/permissions.md
index 3fdf72d8..8ab3bba5 100644
--- a/docs/permissions.md
+++ b/docs/permissions.md
@@ -9,12 +9,7 @@
`unlimitedStorage` is used to help prevent web browsers from unexpectedly
deleting dictionary data.
-* `webRequest` and `webRequestBlocking` _(Firefox only)_ <br>
- Yomichan uses these permissions to ensure certain requests have valid and secure headers.
- This sometimes involves removing or changing the `Origin` request header,
- as this can be used to fingerprint browser configuration.
-
-* `declarativeNetRequest` _(Chrome only)_ <br>
+* `declarativeNetRequest` <br>
Yomichan uses this permission to ensure certain requests have valid and secure headers.
This sometimes involves removing or changing the `Origin` request header,
as this can be used to fingerprint browser configuration.
diff --git a/ext/js/background/request-builder.js b/ext/js/background/request-builder.js
index 32c4a788..7ee89539 100644
--- a/ext/js/background/request-builder.js
+++ b/ext/js/background/request-builder.js
@@ -31,7 +31,6 @@ class RequestBuilder {
* Creates a new instance.
*/
constructor() {
- this._onBeforeSendHeadersExtraInfoSpec = ['blocking', 'requestHeaders', 'extraHeaders'];
this._textEncoder = new TextEncoder();
this._ruleIds = new Set();
}
@@ -55,15 +54,50 @@ class RequestBuilder {
* @returns {Promise<Response>} The response of the `fetch` call.
*/
async fetchAnonymous(url, init) {
- if (isObject(chrome.declarativeNetRequest)) {
- return await this._fetchAnonymousDeclarative(url, init);
+ const id = this._getNewRuleId();
+ const originUrl = this._getOriginURL(url);
+ url = encodeURI(decodeURI(url));
+
+ this._ruleIds.add(id);
+ try {
+ const addRules = [{
+ id,
+ priority: 1,
+ condition: {
+ urlFilter: `|${this._escapeDnrUrl(url)}|`,
+ resourceTypes: ['xmlhttprequest']
+ },
+ action: {
+ type: 'modifyHeaders',
+ requestHeaders: [
+ {
+ operation: 'remove',
+ header: 'Cookie'
+ },
+ {
+ operation: 'set',
+ header: 'Origin',
+ value: originUrl
+ }
+ ],
+ responseHeaders: [
+ {
+ operation: 'remove',
+ header: 'Set-Cookie'
+ }
+ ]
+ }
+ }];
+
+ await this._updateSessionRules({addRules});
+ try {
+ return await fetch(url, init);
+ } finally {
+ await this._tryUpdateSessionRules({removeRuleIds: [id]});
+ }
+ } finally {
+ this._ruleIds.delete(id);
}
- const originURL = this._getOriginURL(url);
- const headerModifications = [
- ['cookie', null],
- ['origin', {name: 'Origin', value: originURL}]
- ];
- return await this._fetchInternal(url, init, headerModifications);
}
/**
@@ -126,144 +160,7 @@ class RequestBuilder {
// Private
- async _fetchInternal(url, init, headerModifications) {
- const filter = {
- urls: [this._getMatchURL(url)],
- types: ['xmlhttprequest']
- };
-
- let requestId = null;
- const onBeforeSendHeadersCallback = (details) => {
- if (requestId !== null || details.url !== url) { return {}; }
- ({requestId} = details);
-
- if (headerModifications === null) { return {}; }
-
- const requestHeaders = details.requestHeaders;
- this._modifyHeaders(requestHeaders, headerModifications);
- return {requestHeaders};
- };
-
- let errorDetailsTimer = null;
- let {promise: errorDetailsPromise, resolve: errorDetailsResolve} = deferPromise();
- const onErrorOccurredCallback = (details) => {
- if (errorDetailsResolve === null || details.requestId !== requestId) { return; }
- if (errorDetailsTimer !== null) {
- clearTimeout(errorDetailsTimer);
- errorDetailsTimer = null;
- }
- errorDetailsResolve(details);
- errorDetailsResolve = null;
- };
-
- const eventListeners = [];
- const onBeforeSendHeadersExtraInfoSpec = (headerModifications !== null ? this._onBeforeSendHeadersExtraInfoSpec : []);
- this._addWebRequestEventListener(chrome.webRequest.onBeforeSendHeaders, onBeforeSendHeadersCallback, filter, onBeforeSendHeadersExtraInfoSpec, eventListeners);
- this._addWebRequestEventListener(chrome.webRequest.onErrorOccurred, onErrorOccurredCallback, filter, void 0, eventListeners);
-
- try {
- return await fetch(url, init);
- } catch (e) {
- // onErrorOccurred is not always invoked by this point, so a delay is needed
- if (errorDetailsResolve !== null) {
- errorDetailsTimer = setTimeout(() => {
- errorDetailsTimer = null;
- if (errorDetailsResolve === null) { return; }
- errorDetailsResolve(null);
- errorDetailsResolve = null;
- }, 100);
- }
- const details = await errorDetailsPromise;
- if (details !== null) {
- const data = {details};
- this._assignErrorData(e, data);
- }
- throw e;
- } finally {
- this._removeWebRequestEventListeners(eventListeners);
- }
- }
-
- _addWebRequestEventListener(target, callback, filter, extraInfoSpec, eventListeners) {
- try {
- for (let i = 0; i < 2; ++i) {
- try {
- if (typeof extraInfoSpec === 'undefined') {
- target.addListener(callback, filter);
- } else {
- target.addListener(callback, filter, extraInfoSpec);
- }
- break;
- } catch (e) {
- // Firefox doesn't support the 'extraHeaders' option and will throw the following error:
- // Type error for parameter extraInfoSpec (Error processing 2: Invalid enumeration value "extraHeaders") for [target].
- if (i === 0 && `${e.message}`.includes('extraHeaders') && Array.isArray(extraInfoSpec)) {
- const index = extraInfoSpec.indexOf('extraHeaders');
- if (index >= 0) {
- extraInfoSpec.splice(index, 1);
- continue;
- }
- }
- throw e;
- }
- }
- } catch (e) {
- console.log(e);
- return;
- }
- eventListeners.push({target, callback});
- }
-
- _removeWebRequestEventListeners(eventListeners) {
- for (const {target, callback} of eventListeners) {
- try {
- target.removeListener(callback);
- } catch (e) {
- console.log(e);
- }
- }
- }
-
- _getMatchURL(url) {
- const url2 = new URL(url);
- return `${url2.protocol}//${url2.host}${url2.pathname}${url2.search}`.replace(/\*/g, '%2a');
- }
-
- _getOriginURL(url) {
- const url2 = new URL(url);
- return `${url2.protocol}//${url2.host}`;
- }
-
- _modifyHeaders(headers, modifications) {
- modifications = new Map(modifications);
-
- for (let i = 0, ii = headers.length; i < ii; ++i) {
- const header = headers[i];
- const name = header.name.toLowerCase();
- const modification = modifications.get(name);
- if (typeof modification === 'undefined') { continue; }
-
- modifications.delete(name);
-
- if (modification === null) {
- headers.splice(i, 1);
- --i;
- --ii;
- } else {
- headers[i] = modification;
- }
- }
-
- for (const header of modifications.values()) {
- if (header !== null) {
- headers.push(header);
- }
- }
- }
-
async _clearSessionRules() {
- if (!isObject(chrome.declarativeNetRequest)) { return; }
-
const rules = await this._getSessionRules();
if (rules.length === 0) { return; }
@@ -276,68 +173,6 @@ class RequestBuilder {
await this._updateSessionRules({removeRuleIds});
}
- async _clearDynamicRules() {
- if (!isObject(chrome.declarativeNetRequest)) { return; }
-
- const rules = await this._getDynamicRules();
-
- if (rules.length === 0) { return; }
-
- const removeRuleIds = [];
- for (const {id} of rules) {
- removeRuleIds.push(id);
- }
-
- await this._updateDynamicRules({removeRuleIds});
- }
-
- async _fetchAnonymousDeclarative(url, init) {
- const id = this._getNewRuleId();
- const originUrl = this._getOriginURL(url);
- url = encodeURI(decodeURI(url));
-
- this._ruleIds.add(id);
- try {
- const addRules = [{
- id,
- priority: 1,
- condition: {
- urlFilter: `|${this._escapeDnrUrl(url)}|`,
- resourceTypes: ['xmlhttprequest']
- },
- action: {
- type: 'modifyHeaders',
- requestHeaders: [
- {
- operation: 'remove',
- header: 'Cookie'
- },
- {
- operation: 'set',
- header: 'Origin',
- value: originUrl
- }
- ],
- responseHeaders: [
- {
- operation: 'remove',
- header: 'Set-Cookie'
- }
- ]
- }
- }];
-
- await this._updateSessionRules({addRules});
- try {
- return await fetch(url, init);
- } finally {
- await this._tryUpdateSessionRules({removeRuleIds: [id]});
- }
- } finally {
- this._ruleIds.delete(id);
- }
- }
-
_getSessionRules() {
return new Promise((resolve, reject) => {
chrome.declarativeNetRequest.getSessionRules((result) => {
@@ -364,6 +199,28 @@ class RequestBuilder {
});
}
+ async _tryUpdateSessionRules(options) {
+ try {
+ await this._updateSessionRules(options);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }
+
+ async _clearDynamicRules() {
+ const rules = await this._getDynamicRules();
+
+ if (rules.length === 0) { return; }
+
+ const removeRuleIds = [];
+ for (const {id} of rules) {
+ removeRuleIds.push(id);
+ }
+
+ await this._updateDynamicRules({removeRuleIds});
+ }
+
_getDynamicRules() {
return new Promise((resolve, reject) => {
chrome.declarativeNetRequest.getDynamicRules((result) => {
@@ -390,15 +247,6 @@ class RequestBuilder {
});
}
- async _tryUpdateSessionRules(options) {
- try {
- await this._updateSessionRules(options);
- return true;
- } catch (e) {
- return false;
- }
- }
-
_getNewRuleId() {
let id = 1;
while (this._ruleIds.has(id)) {
@@ -409,6 +257,11 @@ class RequestBuilder {
return id;
}
+ _getOriginURL(url) {
+ const url2 = new URL(url);
+ return `${url2.protocol}//${url2.host}`;
+ }
+
_escapeDnrUrl(url) {
return url.replace(/[|*^]/g, (char) => this._urlEncodeUtf8(char));
}
@@ -422,25 +275,6 @@ class RequestBuilder {
return result;
}
- _assignErrorData(error, data) {
- try {
- error.data = data;
- } catch (e) {
- // On Firefox, assigning DOMException.data can fail in certain contexts.
- // https://bugzilla.mozilla.org/show_bug.cgi?id=1776555
- try {
- Object.defineProperty(error, 'data', {
- configurable: true,
- enumerable: true,
- writable: true,
- value: data
- });
- } catch (e2) {
- // NOP
- }
- }
- }
-
static _joinUint8Arrays(items, totalLength) {
if (items.length === 1) {
const {array, length} = items[0];
diff --git a/ext/permissions.html b/ext/permissions.html
index 4aaef3c1..f6956cd7 100644
--- a/ext/permissions.html
+++ b/ext/permissions.html
@@ -47,22 +47,7 @@
</div>
</div>
</div></div>
- <div class="settings-item" data-show-for-browser="firefox firefox-mobile"><div class="settings-item-inner">
- <div class="settings-item-left">
- <div class="settings-item-label"><code>webRequest</code> and <code>webRequestBlocking</code></div>
- <div class="settings-item-description">
- <p>
- Yomitan uses these permissions to ensure certain requests have valid and secure headers.
- This sometimes involves removing or changing the <code>Origin</code> request header,
- as this can be used to fingerprint browser configuration.
- </p>
- <p>
- Example: <code class="overflow-wrap">Origin: <span class="extension-id-example"></span></code>
- </p>
- </div>
- </div>
- </div></div>
- <div class="settings-item" data-show-for-browser="chrome edge"><div class="settings-item-inner">
+ <div class="settings-item"><div class="settings-item-inner">
<div class="settings-item-left">
<div class="settings-item-label"><code>declarativeNetRequest</code></div>
<div class="settings-item-description">
@@ -77,11 +62,11 @@
</div>
</div>
</div></div>
- <div class="settings-item" data-show-for-manifest-version="3"><div class="settings-item-inner">
+ <div class="settings-item"><div class="settings-item-inner">
<div class="settings-item-left">
<div class="settings-item-label"><code>scripting</code></div>
<div class="settings-item-description">
- Yomitan will sometimes need to inject stylesheets into webpages in order to
+ Yomitan needs to inject content scripts and stylesheets into webpages in order to
properly display the search popup.
</div>
</div>
@@ -123,18 +108,6 @@
<label class="toggle"><input type="checkbox" class="permissions-toggle" data-required-permissions="nativeMessaging"><span class="toggle-body"><span class="toggle-track"></span><span class="toggle-knob"></span></span></label>
</div>
</div></div>
- <div class="settings-item" data-hide-for-manifest-version="3"><div class="settings-item-inner">
- <div class="settings-item-left">
- <div class="settings-item-label"><code>webNavigation</code> <span class="light">(optional)</span></div>
- <div class="settings-item-description">
- Yomitan may require this permission to inject content scripts for certain browsers
- if Google Docs accessibility mode is enabled.
- </div>
- </div>
- <div class="settings-item-right">
- <label class="toggle"><input type="checkbox" class="permissions-toggle" data-required-permissions="webNavigation"><span class="toggle-body"><span class="toggle-track"></span><span class="toggle-knob"></span></span></label>
- </div>
- </div></div>
<div class="settings-item"><div class="settings-item-inner">
<div class="settings-item-left">
<div class="settings-item-label">Allow in private windows <span class="light">(optional)</span></div>