| Index: include.preload.js |
| diff --git a/include.preload.js b/include.preload.js |
| index 949d039663f9130843b4d8d0a3ddfcb434522066..af018d16fde5651abfea0f3241cb8658493479cb 100644 |
| --- a/include.preload.js |
| +++ b/include.preload.js |
| @@ -349,9 +349,9 @@ function reinjectStyleSheetWhenRemoved(document, style) |
| return observer; |
| } |
| -function protectStyleSheet(document, style) |
| +function protectStyleSheet(document, style, script) |
| { |
| - var id = Math.random().toString(36).substr(2) |
| + var id = Math.random().toString(36).substr(2); |
| style.id = id; |
| var code = [ |
| @@ -387,11 +387,155 @@ function protectStyleSheet(document, style) |
| code.push("})();"); |
| - var script = document.createElement("script"); |
| - script.async = false; |
| - script.textContent = code.join("\n"); |
| - document.documentElement.appendChild(script); |
| - document.documentElement.removeChild(script); |
| + script.textContent += code.join("\n"); |
| +} |
| + |
| +// Neither Chrome[1] nor Safari allow us to intercept WebSockets, and therefore |
| +// some ad networks are misusing them as a way to serve adverts and circumvent |
| +// us. As a workaround we wrap WebSocket, closing connections that would have |
| +// otherwise been blocked. |
| +// [1] - https://bugs.chromium.org/p/chromium/issues/detail?id=129353 |
| +function wrapWebSocket(script) |
| +{ |
| + if (typeof WebSocket == "undefined") |
| + return; |
| + |
| + var eventName = "abpws-" + Math.random().toString().substr(2); |
|
Sebastian Noack
2016/07/01 17:03:39
Instead of duplicating this logic, perhaps just as
kzar
2016/07/06 16:39:19
Done.
|
| + |
| + document.addEventListener(eventName, function(event) |
| + { |
| + ext.backgroundPage.sendMessage({ |
| + type: "websocket-request", |
| + url: event.detail.url |
| + }, function (block) |
| + { |
| + document.dispatchEvent( |
| + new CustomEvent(eventName + "-" + event.detail.url, {detail: block}) |
| + ); |
| + }); |
| + }); |
| + |
| + function wrapper(eventName) |
| + { |
| + var originalWebSocket = WebSocket; |
| + var readyStates = { |
| + CLOSED: {value: 3, enumerable: true}, |
| + CLOSING: {value: 2, enumerable: true}, |
| + OPEN: {value: 1, enumerable: true}, |
| + CONNECTING: {value: 0, enumerable: true} |
| + }; |
| + |
| + WebSocket = function(url, protocol) |
| + { |
| + var blocked = false; |
| + var websocket; |
|
Sebastian Noack
2016/07/01 17:03:39
Since you access this variable before it might get
kzar
2016/07/06 16:39:19
Done.
|
| + var websocketProperties = { |
|
Sebastian Noack
2016/07/01 17:03:39
It seems you don't need to hard-code that properti
kzar
2016/07/06 16:39:19
Done.
|
| + attributes: { |
| + url: url, |
| + protocol: protocol, |
| + readyState: readyStates.CONNECTING, |
| + bufferedAmount: 0, |
| + extensions: "", |
| + binaryType: "blob", |
| + onopen: null, |
| + onerror: null, |
| + onclose: null, |
| + onmessage: null |
| + }, |
| + methods: ["close", "send", "addEventListener", "removeEventListener"] |
| + }; |
| + var properties = Object.create(null); |
| + var queue = []; |
| + |
| + for (var key in websocketProperties.attributes) |
| + { |
| + properties[key] = function(key, defaultValue) |
| + { |
| + return { |
| + enumerable: true, |
|
Sebastian Noack
2016/07/01 17:03:40
Instead hard-coding metadata like "enumerable" you
kzar
2016/07/06 16:39:19
Done.
|
| + get: function() |
| + { |
| + if (blocked && key == "readyState") |
| + return readyStates.CLOSED; |
| + return websocket ? websocket[key] : defaultValue; |
| + }, |
| + set: function(value) |
| + { |
| + if (websocket) |
| + return websocket[key] = value; |
|
Sebastian Noack
2016/07/01 17:03:40
Why do you return in a setter?
kzar
2016/07/06 16:39:20
Because the return value for an assignment is usua
Sebastian Noack
2016/08/08 16:42:53
An assignment operation returns it's right-hand va
kzar
2016/08/08 18:19:05
Ah, I didn't know that!
|
| + else |
| + { |
| + queue.push(["set", key, value]); |
| + return value; |
| + } |
| + } |
| + }; |
| + }(key, websocketProperties.attributes[key]); |
| + } |
| + for (var i = 0; i < websocketProperties.methods.length; i += 1) |
| + { |
| + var method = websocketProperties.methods[i]; |
| + properties[method] = (function(method) |
| + { |
| + return { |
| + enumerable: true, |
| + value: function() |
| + { |
| + if (websocket) |
| + websocket[method].apply(websocket, arguments); |
| + else |
| + queue.push(["call", method, arguments]); |
| + } |
| + }; |
| + }(method)); |
| + } |
| + Object.defineProperties(this, properties); |
|
Sebastian Noack
2016/07/01 17:03:40
Note that those properties (in Chrome at least) ar
kzar
2016/07/06 16:39:20
Done.
|
| + |
| + function processQueue() |
| + { |
| + for (var i = 0; i < queue.length; i += 1) |
| + { |
| + var action = queue[i][0]; |
| + var key = queue[i][1]; |
| + var value = queue[i][2]; |
| + switch (action) |
| + { |
| + case "set": |
| + websocket[key] = value; |
| + break; |
| + case "call": |
| + websocket[key].apply(websocket, value); |
| + break; |
| + } |
| + } |
| + queue = undefined; |
| + } |
| + |
| + var incomingEventName = eventName + "-" + url; |
| + function listener(event) |
| + { |
| + blocked = event.detail; |
| + if (blocked) |
| + queue = undefined; |
| + else |
| + { |
| + websocket = new originalWebSocket(url, protocol); |
| + processQueue(); |
| + } |
| + |
| + document.removeEventListener(incomingEventName, listener); |
| + } |
| + document.addEventListener(incomingEventName, listener); |
| + |
| + document.dispatchEvent(new CustomEvent(eventName, { |
| + detail: {url: url, protocol: protocol} |
| + })); |
| + }; |
| + WebSocket.prototype = Object.create(window.EventTarget.prototype, readyStates); |
|
Sebastian Noack
2016/07/01 17:03:40
Can't you simply reuse the original parent prototy
kzar
2016/07/06 16:39:20
Done. (Good point as it is different in Safari.)
|
| + Object.defineProperties(WebSocket, readyStates); |
| + } |
| + |
| + script.textContent += "(" + wrapper.toString() + ")(\"" + eventName + "\");"; |
|
Sebastian Noack
2016/07/01 17:03:40
This is inconsistent with how we generate the code
kzar
2016/07/06 16:39:19
Done, however wasn't sure how to test the code abo
|
| } |
| function init(document) |
| @@ -401,6 +545,12 @@ function init(document) |
| var observer = null; |
| var tracer = null; |
| + // Scripts to be injected into the page, executed at the end of initialization |
| + var script = document.createElement("script"); |
| + script.async = false; |
| + |
| + wrapWebSocket(script); |
| + |
| function getPropertyFilters(callback) |
| { |
| ext.backgroundPage.sendMessage({ |
| @@ -448,7 +598,8 @@ function init(document) |
| return; |
| observer = reinjectStyleSheetWhenRemoved(document, style); |
| - protectStyleSheet(document, style); |
| + if (script) |
|
Sebastian Noack
2016/07/01 17:03:39
Sorry, I didn't notice that this a different code
kzar
2016/07/06 16:39:19
Done.
|
| + protectStyleSheet(document, style, script); |
| } |
| // If using shadow DOM, we have to add the ::content pseudo-element |
| @@ -563,6 +714,10 @@ function init(document) |
| } |
| }, true); |
| + document.documentElement.appendChild(script); |
| + document.documentElement.removeChild(script); |
| + script = undefined; |
| + |
| return updateStylesheet; |
| } |