diff options
author | toasted-nutbread <toasted-nutbread@users.noreply.github.com> | 2021-02-14 11:19:54 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-02-14 11:19:54 -0500 |
commit | e419a418f6f03ef0a24330b67e7b76c5e3a7c22d (patch) | |
tree | a4c27bdfabc9280d9f6262d93d5152a58de8bc15 /ext/js/background/request-builder.js | |
parent | 43d1457ebfe23196348649c245dfb942a0f00a1a (diff) |
Move bg/js (#1387)
* Move bg/js/anki.js to js/comm/anki.js
* Move bg/js/mecab.js to js/comm/mecab.js
* Move bg/js/search-main.js to js/display/search-main.js
* Move bg/js/template-patcher.js to js/templates/template-patcher.js
* Move bg/js/template-renderer-frame-api.js to js/templates/template-renderer-frame-api.js
* Move bg/js/template-renderer-frame-main.js to js/templates/template-renderer-frame-main.js
* Move bg/js/template-renderer-proxy.js to js/templates/template-renderer-proxy.js
* Move bg/js/template-renderer.js to js/templates/template-renderer.js
* Move bg/js/media-utility.js to js/media/media-utility.js
* Move bg/js/native-simple-dom-parser.js to js/dom/native-simple-dom-parser.js
* Move bg/js/simple-dom-parser.js to js/dom/simple-dom-parser.js
* Move bg/js/audio-downloader.js to js/media/audio-downloader.js
* Move bg/js/deinflector.js to js/language/deinflector.js
* Move bg/js/backend.js to js/background/backend.js
* Move bg/js/translator.js to js/language/translator.js
* Move bg/js/search-display-controller.js to js/display/search-display-controller.js
* Move bg/js/request-builder.js to js/background/request-builder.js
* Move bg/js/text-source-map.js to js/general/text-source-map.js
* Move bg/js/clipboard-reader.js to js/comm/clipboard-reader.js
* Move bg/js/clipboard-monitor.js to js/comm/clipboard-monitor.js
* Move bg/js/query-parser.js to js/display/query-parser.js
* Move bg/js/profile-conditions.js to js/background/profile-conditions.js
* Move bg/js/dictionary-database.js to js/language/dictionary-database.js
* Move bg/js/dictionary-importer.js to js/language/dictionary-importer.js
* Move bg/js/anki-note-builder.js to js/data/anki-note-builder.js
* Move bg/js/anki-note-data.js to js/data/anki-note-data.js
* Move bg/js/database.js to js/data/database.js
* Move bg/js/json-schema.js to js/data/json-schema.js
* Move bg/js/options.js to js/data/options-util.js
* Move bg/js/background-main.js to js/background/background-main.js
* Move bg/js/permissions-util.js to js/data/permissions-util.js
* Move bg/js/context-main.js to js/pages/action-popup-main.js
* Move bg/js/generic-page-main.js to js/pages/generic-page-main.js
* Move bg/js/info-main.js to js/pages/info-main.js
* Move bg/js/permissions-main.js to js/pages/permissions-main.js
* Move bg/js/welcome-main.js to js/pages/welcome-main.js
Diffstat (limited to 'ext/js/background/request-builder.js')
-rw-r--r-- | ext/js/background/request-builder.js | 266 |
1 files changed, 266 insertions, 0 deletions
diff --git a/ext/js/background/request-builder.js b/ext/js/background/request-builder.js new file mode 100644 index 00000000..dda5825d --- /dev/null +++ b/ext/js/background/request-builder.js @@ -0,0 +1,266 @@ +/* + * Copyright (C) 2020-2021 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +class RequestBuilder { + constructor() { + this._extraHeadersSupported = null; + this._onBeforeSendHeadersExtraInfoSpec = ['blocking', 'requestHeaders', 'extraHeaders']; + this._textEncoder = new TextEncoder(); + this._ruleIds = new Set(); + } + + async prepare() { + try { + await this._clearDynamicRules(); + } catch (e) { + // NOP + } + } + + async fetchAnonymous(url, init) { + if (isObject(chrome.declarativeNetRequest)) { + return await this._fetchAnonymousDeclarative(url, init); + } + const originURL = this._getOriginURL(url); + const modifications = [ + ['cookie', null], + ['origin', {name: 'Origin', value: originURL}] + ]; + return await this._fetchModifyHeaders(url, init, modifications); + } + + // Private + + async _fetchModifyHeaders(url, init, modifications) { + const matchURL = this._getMatchURL(url); + + let done = false; + const callback = (details) => { + if (done || details.url !== url) { return {}; } + done = true; + + const requestHeaders = details.requestHeaders; + this._modifyHeaders(requestHeaders, modifications); + return {requestHeaders}; + }; + const filter = { + urls: [matchURL], + types: ['xmlhttprequest'] + }; + + let needsCleanup = false; + try { + this._onBeforeSendHeadersAddListener(callback, filter); + needsCleanup = true; + } catch (e) { + // NOP + } + + try { + return await fetch(url, init); + } finally { + if (needsCleanup) { + try { + chrome.webRequest.onBeforeSendHeaders.removeListener(callback); + } catch (e) { + // NOP + } + } + } + } + + _onBeforeSendHeadersAddListener(callback, filter) { + const extraInfoSpec = this._onBeforeSendHeadersExtraInfoSpec; + for (let i = 0; i < 2; ++i) { + try { + chrome.webRequest.onBeforeSendHeaders.addListener(callback, filter, extraInfoSpec); + if (this._extraHeadersSupported === null) { + this._extraHeadersSupported = true; + } + 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 webRequest.onBeforeSendHeaders. + if (this._extraHeadersSupported !== null || !`${e.message}`.includes('extraHeaders')) { + throw e; + } + } + + // addListener failed; remove 'extraHeaders' from extraInfoSpec. + this._extraHeadersSupported = false; + const index = extraInfoSpec.indexOf('extraHeaders'); + if (index >= 0) { extraInfoSpec.splice(index, 1); } + } + } + + _getMatchURL(url) { + const url2 = new URL(url); + return `${url2.protocol}//${url2.host}${url2.pathname}`; + } + + _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 _clearDynamicRules() { + if (!isObject(chrome.declarativeNetRequest)) { return; } + + const rules = 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._updateDynamicRules({addRules}); + try { + return await fetch(url, init); + } finally { + await this._tryUpdateDynamicRules({removeRuleIds: [id]}); + } + } finally { + this._ruleIds.delete(id); + } + } + + _getDynamicRules() { + return new Promise((resolve, reject) => { + chrome.declarativeNetRequest.getDynamicRules((result) => { + const e = chrome.runtime.lastError; + if (e) { + reject(new Error(e.message)); + } else { + resolve(result); + } + }); + }); + } + + _updateDynamicRules(options) { + return new Promise((resolve, reject) => { + chrome.declarativeNetRequest.updateDynamicRules(options, () => { + const e = chrome.runtime.lastError; + if (e) { + reject(new Error(e.message)); + } else { + resolve(); + } + }); + }); + } + + async _tryUpdateDynamicRules(options) { + try { + await this._updateDynamicRules(options); + return true; + } catch (e) { + return false; + } + } + + _getNewRuleId() { + let id = 1; + while (this._ruleIds.has(id)) { + const pre = id; + ++id; + if (id === pre) { throw new Error('Could not generate an id'); } + } + return id; + } + + _escapeDnrUrl(url) { + return url.replace(/[|*^]/g, (char) => this._urlEncodeUtf8(char)); + } + + _urlEncodeUtf8(text) { + const array = this._textEncoder.encode(text); + let result = ''; + for (const byte of array) { + result += `%${byte.toString(16).toUpperCase().padStart(2, '0')}`; + } + return result; + } +} |