From 7c9fd5e6118a9394b948dab23f6bda790fa31293 Mon Sep 17 00:00:00 2001 From: tophf Date: Wed, 4 Jul 2018 15:03:54 +0300 Subject: [PATCH] spoof USO referrer for their style search API fixes #413 --- content/install-hook-userstyles.js | 33 ++++++++++-- manifest.json | 4 +- popup/search-results.js | 86 ++++++++++++++++++++++++++---- 3 files changed, 109 insertions(+), 14 deletions(-) diff --git a/content/install-hook-userstyles.js b/content/install-hook-userstyles.js index d7560a23..6159e8a2 100644 --- a/content/install-hook-userstyles.js +++ b/content/install-hook-userstyles.js @@ -330,12 +330,13 @@ } })(); -document.documentElement.appendChild(document.createElement('script')).text = '(' + - function () { +// run in page context +document.documentElement.appendChild(document.createElement('script')).text = `(${ + EXTENSION_ORIGIN => { document.currentScript.remove(); // spoof Stylish extension presence in Chrome - if (chrome.app) { + if (window.chrome && chrome.app) { const realImage = window.Image; window.Image = function Image(...args) { return new Proxy(new realImage(...args), { @@ -354,6 +355,29 @@ document.documentElement.appendChild(document.createElement('script')).text = '( }; } + // spoof USO referrer for style search in the popup + if (window !== top && location.pathname === '/') { + window.addEventListener('message', ({data, origin}) => { + if (!data || + !data.xhr || + origin !== EXTENSION_ORIGIN) { + return; + } + const xhr = new XMLHttpRequest(); + xhr.responseType = 'json'; + xhr.onloadend = xhr.onerror = () => { + window.stop(); + top.postMessage({ + id: data.xhr.id, + status: xhr.status, + response: xhr.response, + }, EXTENSION_ORIGIN); + }; + xhr.open('GET', data.xhr.url); + xhr.send(); + }); + } + // USO bug workaround: use the actual style settings in API response let settings; const originalResponseJson = Response.prototype.json; @@ -426,7 +450,8 @@ document.documentElement.appendChild(document.createElement('script')).text = '( return json; }); }; - } + ')()'; + } +})('${chrome.runtime.getURL('').slice(0, -1)}')`; // TODO: remove the following statement when USO pagination is fixed if (location.search.includes('category=')) { diff --git a/manifest.json b/manifest.json index 648e7ffe..aadea1c9 100644 --- a/manifest.json +++ b/manifest.json @@ -14,6 +14,8 @@ "permissions": [ "tabs", "webNavigation", + "webRequest", + "webRequestBlocking", "contextMenus", "storage", "alarms", @@ -60,7 +62,7 @@ { "matches": ["http://userstyles.org/*", "https://userstyles.org/*"], "run_at": "document_start", - "all_frames": false, + "all_frames": true, "js": ["content/install-hook-userstyles.js"] }, { diff --git a/popup/search-results.js b/popup/search-results.js index 5f6a0ab8..e4379614 100755 --- a/popup/search-results.js +++ b/popup/search-results.js @@ -51,6 +51,9 @@ window.addEventListener('showStyles:done', function _() { let searchCurrentPage = 1; let searchExhausted = false; + let searchFrame; + let searchFrameQueue; + const processedResults = []; const unprocessedResults = []; @@ -697,15 +700,7 @@ window.addEventListener('showStyles:done', function _() { return readCache(cacheKey) .then(json => json || - download(searchURL, { - method: 'GET', - headers: { - 'Content-type': 'application/json', - 'Accept': '*/*' - }, - responseType: 'json', - body: null - }).then(writeCache)) + searchInFrame(searchURL).then(writeCache)) .then(json => { searchCurrentPage = json.current_page + 1; searchTotalPages = json.total_pages; @@ -783,5 +778,78 @@ window.addEventListener('showStyles:done', function _() { ignoreChromeError(); } + //endregion + //region USO referrer spoofing via iframe + + function searchInFrame(url) { + return searchFrame ? new Promise((resolve, reject) => { + const id = performance.now(); + const timeout = setTimeout(() => { + searchFrameQueue.get(id).reject(); + searchFrameQueue.delete(id); + }, 10e3); + searchFrameQueue.set(id, {resolve, reject, timeout}); + searchFrame.contentWindow.postMessage({xhr: {id, url}}, '*'); + }) : setupFrame().then(() => searchInFrame(url)); + } + + function setupFrame() { + searchFrame = $create('iframe', {src: BASE_URL}); + searchFrameQueue = new Map(); + + const stripHeaders = info => ({ + responseHeaders: info.responseHeaders.filter(({name}) => !/^X-Frame-Options$/i.test(name)), + }); + chrome.webRequest.onHeadersReceived.addListener(stripHeaders, { + urls: [BASE_URL + '/'], + types: ['sub_frame'], + }, [ + 'blocking', + 'responseHeaders', + ]); + + let frameId; + const stripResources = info => { + if (!frameId && info.url === BASE_URL + '/') { + frameId = info.frameId; + } else if (frameId === info.frameId && info.type !== 'xmlhttprequest') { + return {redirectUrl: 'data:,'}; + } + }; + chrome.webRequest.onBeforeRequest.addListener(stripResources, { + urls: [''], + }, [ + 'blocking', + ]); + setTimeout(() => { + chrome.webRequest.onBeforeRequest.removeListener(stripResources); + }, 10e3); + + window.addEventListener('message', ({data, origin}) => { + if (!data || origin !== BASE_URL) return; + const {resolve, reject, timeout} = searchFrameQueue.get(data.id) || {}; + if (!resolve) return; + chrome.webRequest.onBeforeRequest.removeListener(stripResources); + searchFrameQueue.delete(data.id); + clearTimeout(timeout); + if (data.response && data.status < 400) { + resolve(data.response); + } else { + reject(data.status); + } + }); + + return new Promise((resolve, reject) => { + const done = event => { + chrome.webRequest.onHeadersReceived.removeListener(stripHeaders); + (event.type === 'load' ? resolve : reject)(); + }; + searchFrame.addEventListener('load', done, {once: true}); + searchFrame.addEventListener('error', done, {once: true}); + searchFrame.style.setProperty('display', 'none', 'important'); + document.body.appendChild(searchFrame); + }); + } + //endregion });