aboutsummaryrefslogtreecommitdiff
path: root/bg/ResponseProcessor.js
blob: 7c264a433e9d71f4e77b45f3b2e75baf47204e9b (plain) (blame)
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 =
      request =>  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));
  }

  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 = 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 ||
        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 };