From 01937dc9d5215ef96ce756e3ccda51bf29032f58 Mon Sep 17 00:00:00 2001 From: Wojtek Kosior Date: Mon, 10 May 2021 18:07:05 +0200 Subject: initial commit --- background/ResponseProcessor.mjs | 145 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 background/ResponseProcessor.mjs (limited to 'background/ResponseProcessor.mjs') diff --git a/background/ResponseProcessor.mjs b/background/ResponseProcessor.mjs new file mode 100644 index 0000000..85c2655 --- /dev/null +++ b/background/ResponseProcessor.mjs @@ -0,0 +1,145 @@ +/** +* GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript. +* +* Copyright (C) 2018 Giorgio Maone +* +* This file is part of GNU LibreJS. +* +* GNU LibreJS 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. +* +* GNU LibreJS 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 GNU LibreJS. If not, see . +*/ + +/** + An abstraction layer over the StreamFilter API, allowing its clients to process + only the "interesting" HTML and script requests and leaving the other alone +*/ + +import ResponseMetaData from './ResponseMetaData.mjs'; +import browser from '/common/browser.mjs'; + +let listeners = new WeakMap(); +let webRequestEvent = browser.webRequest.onHeadersReceived; + +class ResponseProcessor { + + static install(handler, types = ["main_frame", "sub_frame"]) { + if (listeners.has(handler)) return false; + let listener = + async request => await new ResponseTextFilter(request).process(handler); + listeners.set(handler, listener); + webRequestEvent.addListener( + listener, + {urls: [""], types}, + ["blocking", "responseHeaders"] + ); + return true; + } + + static uninstall(handler) { + let listener = listeners.get(handler); + if (listener) { + webRequestEvent.removeListener(listener); + } + } +} + +Object.assign(ResponseProcessor, { + // control flow values to be returned by handler.pre() callbacks + ACCEPT: {}, + REJECT: {cancel: true}, + CONTINUE: null +}); + +class ResponseTextFilter { + constructor(request) { + this.request = request; + let {type, statusCode} = request; + let md = this.metaData = new ResponseMetaData(request); + this.canProcess = // we want to process html documents and scripts only + (statusCode < 300 || statusCode >= 400) && // skip redirections + !md.disposition && // skip forced downloads + (type === "script" || /\bhtml\b/i.test(md.contentType)); + } + + async process(handler) { + if (!this.canProcess) return ResponseProcessor.ACCEPT; + let {metaData, request} = this; + let response = {request, metaData}; // we keep it around allowing callbacks to store state + if (typeof handler.pre === "function") { + let res = await handler.pre(response); + if (res) return res; + if (handler.post) handler = handler.post; + if (typeof handler !== "function") return ResponseProcessor.ACCEPT; + } + + return ResponseProcessor.ACCEPT; + + let {requestId, responseHeaders} = request; + let filter = browser.webRequest.filterResponseData(requestId); + let buffer = []; + + filter.ondata = event => { + buffer.push(event.data); + }; + + filter.onstop = async event => { + // concatenate chunks + let size = buffer.reduce((sum, chunk, n) => sum + chunk.byteLength, 0) + let allBytes = new Uint8Array(size); + let pos = 0; + for (let chunk of buffer) { + allBytes.set(new Uint8Array(chunk), pos); + pos += chunk.byteLength; + } + buffer = null; // allow garbage collection + if (allBytes.indexOf(0) !== -1) { + console.debug("Warning: zeroes in bytestream, probable cached encoding mismatch.", request); + if (request.type === "script") { + console.debug("It's a script, trying to refetch it."); + response.text = await (await fetch(request.url, {cache: "reload", credentials: "include"})).text(); + } else { + console.debug("It's a %s, trying to decode it as UTF-16.", request.type); + response.text = new TextDecoder("utf-16be").decode(allBytes, {stream: true}); + } + } else { + response.text = metaData.decode(allBytes); + } + let editedText = null; + try { + editedText = await handler(response); + } catch(e) { + console.error(e); + } + if (editedText !== null) { + // we changed the content, let's re-encode + let encoded = new TextEncoder().encode(editedText); + // pre-pending the UTF-8 BOM will force the charset per HTML 5 specs + allBytes = new Uint8Array(encoded.byteLength + 3); + allBytes.set(ResponseMetaData.UTF8BOM, 0); // UTF-8 BOM + allBytes.set(encoded, 3); + } + filter.write(allBytes); + filter.close(); + } + + return ResponseProcessor.ACCEPT; + } +} + +/* The following was originally in Storage.js */ +function url_item(url) { + let queryPos = url.indexOf("?"); + return queryPos === -1 ? url : url.substring(0, queryPos); +} + +export {ResponseProcessor, url_item}; -- cgit v1.2.3