aboutsummaryrefslogtreecommitdiff
path: root/ext
diff options
context:
space:
mode:
authorDarius Jahandarie <djahandarie@gmail.com>2023-11-02 13:05:33 +0000
committerGitHub <noreply@github.com>2023-11-02 13:05:33 +0000
commit092d1beac6c1af8eae8615d541434513e875b4a5 (patch)
tree528d984e6f4b3f39e4502a6cf125f8fc30b86c6c /ext
parent55897b2b29e88ffd0c9140d03b9e74c4a94d98bd (diff)
parentbbefd8a07ba71d7fe5e9c707ddb06e99bfd2a502 (diff)
Merge pull request #293 from praschke/permissions-fixups
Permissions fixups
Diffstat (limited to 'ext')
-rw-r--r--ext/js/background/request-builder.js294
-rw-r--r--ext/js/extension/environment.js16
-rw-r--r--ext/permissions.html47
-rw-r--r--ext/settings.html2
4 files changed, 96 insertions, 263 deletions
diff --git a/ext/js/background/request-builder.js b/ext/js/background/request-builder.js
index 663e242b..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();
}
@@ -42,6 +41,7 @@ class RequestBuilder {
async prepare() {
try {
await this._clearDynamicRules();
+ await this._clearSessionRules();
} catch (e) {
// NOP
}
@@ -54,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);
}
/**
@@ -125,145 +160,56 @@ 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);
+ async _clearSessionRules() {
+ const rules = await this._getSessionRules();
- 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);
- }
- }
+ if (rules.length === 0) { return; }
- _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;
+ const removeRuleIds = [];
+ for (const {id} of rules) {
+ removeRuleIds.push(id);
}
- eventListeners.push({target, callback});
- }
- _removeWebRequestEventListeners(eventListeners) {
- for (const {target, callback} of eventListeners) {
- try {
- target.removeListener(callback);
- } catch (e) {
- console.log(e);
- }
- }
+ await this._updateSessionRules({removeRuleIds});
}
- _getMatchURL(url) {
- const url2 = new URL(url);
- return `${url2.protocol}//${url2.host}${url2.pathname}${url2.search}`.replace(/\*/g, '%2a');
+ _getSessionRules() {
+ return new Promise((resolve, reject) => {
+ chrome.declarativeNetRequest.getSessionRules((result) => {
+ const e = chrome.runtime.lastError;
+ if (e) {
+ reject(new Error(e.message));
+ } else {
+ resolve(result);
+ }
+ });
+ });
}
- _getOriginURL(url) {
- const url2 = new URL(url);
- return `${url2.protocol}//${url2.host}`;
+ _updateSessionRules(options) {
+ return new Promise((resolve, reject) => {
+ chrome.declarativeNetRequest.updateSessionRules(options, () => {
+ const e = chrome.runtime.lastError;
+ if (e) {
+ reject(new Error(e.message));
+ } else {
+ resolve();
+ }
+ });
+ });
}
- _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 _tryUpdateSessionRules(options) {
+ try {
+ await this._updateSessionRules(options);
+ return true;
+ } catch (e) {
+ return false;
}
}
async _clearDynamicRules() {
- if (!isObject(chrome.declarativeNetRequest)) { return; }
-
- const rules = this._getDynamicRules();
+ const rules = await this._getDynamicRules();
if (rules.length === 0) { return; }
@@ -275,53 +221,6 @@ class RequestBuilder {
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._updateDynamicRules({addRules});
- try {
- return await this._fetchInternal(url, init, null);
- } finally {
- await this._tryUpdateDynamicRules({removeRuleIds: [id]});
- }
- } finally {
- this._ruleIds.delete(id);
- }
- }
-
_getDynamicRules() {
return new Promise((resolve, reject) => {
chrome.declarativeNetRequest.getDynamicRules((result) => {
@@ -348,15 +247,6 @@ class RequestBuilder {
});
}
- async _tryUpdateDynamicRules(options) {
- try {
- await this._updateDynamicRules(options);
- return true;
- } catch (e) {
- return false;
- }
- }
-
_getNewRuleId() {
let id = 1;
while (this._ruleIds.has(id)) {
@@ -367,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));
}
@@ -380,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/js/extension/environment.js b/ext/js/extension/environment.js
index ec1e8612..ad5a19ae 100644
--- a/ext/js/extension/environment.js
+++ b/ext/js/extension/environment.js
@@ -31,8 +31,9 @@ class Environment {
}
async _loadEnvironmentInfo() {
- const browser = await this._getBrowser();
const os = await this._getOperatingSystem();
+ const browser = await this._getBrowser(os);
+
return {
browser,
platform: {os}
@@ -64,7 +65,7 @@ class Environment {
});
}
- async _getBrowser() {
+ async _getBrowser(os) {
try {
if (chrome.runtime.getURL('/').startsWith('ms-browser-extension://')) {
return 'edge-legacy';
@@ -76,17 +77,12 @@ class Environment {
// NOP
}
if (typeof browser !== 'undefined') {
- try {
- const info = await browser.runtime.getBrowserInfo();
- if (info.name === 'Fennec') {
- return 'firefox-mobile';
- }
- } catch (e) {
- // NOP
- }
if (this._isSafari()) {
return 'safari';
}
+ if (os === 'android') {
+ return 'firefox-mobile';
+ }
return 'firefox';
} else {
return 'chrome';
diff --git a/ext/permissions.html b/ext/permissions.html
index 9ede7d27..376a9de5 100644
--- a/ext/permissions.html
+++ b/ext/permissions.html
@@ -49,33 +49,6 @@
</div></div>
<div class="settings-item"><div class="settings-item-inner">
<div class="settings-item-left">
- <div class="settings-item-label"><code>webRequest</code></div>
- <div class="settings-item-description">
- <p>
- Yomitan uses this permission to collect audio or create Anki notes using
- <a href="https://ankiweb.net/shared/info/2055492159" target="_blank" rel="noopener noreferrer">AnkiConnect</a>.
- It is also required to surface error information from failed requests.
- </p>
- </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>webRequestBlocking</code></div>
- <div class="settings-item-description">
- <p>
- Yomitan uses this permission 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-left">
<div class="settings-item-label"><code>declarativeNetRequest</code></div>
<div class="settings-item-description">
<p>
@@ -89,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>
@@ -121,9 +94,9 @@
<label class="toggle"><input type="checkbox" class="permissions-toggle" data-required-permissions="clipboardRead"><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" data-hide-for-browser="firefox-mobile"><div class="settings-item-inner">
<div class="settings-item-left">
- <div class="settings-item-label"><code>nativeMessaging</code> <span class="light" data-show-for-browser="chrome edge">(optional)</span></div>
+ <div class="settings-item-label"><code>nativeMessaging</code> <span class="light">(optional)</span></div>
<div class="settings-item-description">
Yomitan has the ability to communicate with an optional native messaging component in order to support
parsing large blocks of Japanese text using
@@ -135,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>
diff --git a/ext/settings.html b/ext/settings.html
index f1001f90..8d5f0a76 100644
--- a/ext/settings.html
+++ b/ext/settings.html
@@ -1372,7 +1372,7 @@
</p>
</div>
</div>
- <div class="settings-item advanced-only">
+ <div class="settings-item advanced-only" data-hide-for-browser="firefox-mobile">
<div class="settings-item-inner">
<div class="settings-item-left">
<div class="settings-item-invalid-indicator"></div>