Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Unified Diff: include.preload.js

Issue 29347034: Issue 1727 - Prevent circumvention via WebSocket (Closed)
Patch Set: Addressed feedback Created June 29, 2016, 1:38 p.m.
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « no previous file | lib/requestBlocker.js » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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;
}
« no previous file with comments | « no previous file | lib/requestBlocker.js » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld