1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
|
/**
* GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
*
* Copyright (C) 2018 Giorgio Maone <giorgio@maone.net>
*
* 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 <http://www.gnu.org/licenses/>.
*/
/**
An abstraction layer over the StreamFilter API, allowing its clients to process
only the "interesting" HTML and script requests and leaving the other alone
*/
let {ResponseMetaData} = require("./ResponseMetaData");
let listeners = new WeakMap();
let webRequestEvent = browser.webRequest.onHeadersReceived;
class ResponseProcessor {
static install(handler, types = ["main_frame", "sub_frame", "script"]) {
if (listeners.has(handler)) return false;
let listener =
async request => await new ResponseTextFilter(request).process(handler);
listeners.set(handler, listener);
webRequestEvent.addListener(
listener,
{urls: ["<all_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") 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 => {
let decoder = metaData.createDecoder();
let params = {stream: true};
response.text = buffer.map(
chunk => decoder.decode(chunk, params))
.join('');
let editedText = null;
try {
editedText = await handler(response);
} catch(e) {
console.error(e);
}
if (metaData.forcedUTF8 && request.type !== "script" ||
editedText !== null && response.text !== editedText) {
// if we changed the charset, the text or both, let's re-encode
filter.write(new TextEncoder().encode(editedText));
} else {
// ... otherwise pass all the raw bytes through
for (let chunk of buffer) filter.write(chunk);
}
filter.disconnect();
}
return metaData.forceUTF8() ? {responseHeaders} : ResponseProcessor.ACCEPT;;
}
}
module.exports = { ResponseProcessor };
|