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 further feedback Created Aug. 9, 2016, 12:05 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..dab0b843ac1e4116b6aa6cbe5b041409bd4cb564 100644
--- a/include.preload.js
+++ b/include.preload.js
@@ -17,6 +17,7 @@
var MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
var SELECTOR_GROUP_SIZE = 200;
+var id = Math.random().toString(36).substr(2);
var typeMap = {
"img": "IMAGE",
@@ -349,49 +350,141 @@ function reinjectStyleSheetWhenRemoved(document, style)
return observer;
}
+function runInPage(fn, arg)
+{
+ var script = document.createElement("script");
+ script.type = "application/javascript";
+ script.async = false;
+
+ // include.youtube.js passes this function is passed a RegExp which
Sebastian Noack 2016/08/09 15:09:37 This reads weird. Typo? In include.youtube.js,
kzar 2016/08/09 16:11:40 Done.
+ // JSON.stringify would convert to "{}".
+ if (!(arg instanceof RegExp))
Sebastian Noack 2016/08/09 14:53:30 It seems this wasn't necessary before. So why is i
Sebastian Noack 2016/08/09 15:09:37 Never mind, I got it, (misinterpreted the conditio
+ arg = JSON.stringify(arg);
+
+ script.textContent = "(" + fn + ")(" + arg + ");";
+ document.documentElement.appendChild(script);
+ document.documentElement.removeChild(script);
+}
+
function protectStyleSheet(document, style)
{
- var id = Math.random().toString(36).substr(2)
style.id = id;
- var code = [
- "(function()",
- "{",
- ' var style = document.getElementById("' + id + '") ||',
- ' document.documentElement.shadowRoot.getElementById("' + id + '");',
- ' style.removeAttribute("id");'
- ];
+ var protector = function(id)
+ {
+ var style = document.getElementById(id) ||
+ document.documentElement.shadowRoot.getElementById(id);
+ style.removeAttribute("id");
+
+ var disableables = [style, style.sheet];
+ for (var i = 0; i < disableables.length; i++)
+ Object.defineProperty(disableables[i], "disabled",
+ {value: false, enumerable: true});
- var disableables = ["style", "style.sheet"];
- for (var i = 0; i < disableables.length; i++)
+ ["deleteRule", "removeRule"].forEach(function(method)
+ {
+ var original = CSSStyleSheet.prototype[method];
+ CSSStyleSheet.prototype[method] = function(index)
+ {
+ if (this != style.sheet)
+ original.call(this, index);
+ };
Sebastian Noack 2016/08/09 15:41:15 Bind those functions as well? Otherwise we can sti
kzar 2016/08/09 16:11:40 Done.
+ });
+ };
+
+ runInPage(protector, id);
+}
+
+// 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, preventing blocked WebSocket
+// connections from being opened.
+// [1] - https://bugs.chromium.org/p/chromium/issues/detail?id=129353
+function wrapWebSocket()
+{
+ if (typeof WebSocket == "undefined")
+ return;
+
+ var eventName = "abpws-" + id;
+
+ document.addEventListener(eventName, function(event)
{
- code.push(" Object.defineProperty(" + disableables[i] + ', "disabled", '
- + "{value: false, enumerable: true});");
- }
+ ext.backgroundPage.sendMessage({
+ type: "websocket-request",
+ url: event.detail.url
+ }, function (block)
+ {
+ document.dispatchEvent(
+ new CustomEvent(eventName + "-" + event.detail.url, {detail: block})
+ );
+ });
+ });
- var methods = ["deleteRule", "removeRule"];
- for (var j = 0; j < methods.length; j++)
+ function wrapper(eventName)
Sebastian Noack 2016/08/09 14:53:30 I just noticed that the argument name is the same
Sebastian Noack 2016/08/09 14:53:30 I just noticed a minor inconsistency, while you us
kzar 2016/08/09 16:11:39 Done.
kzar 2016/08/09 16:11:40 I'd rather leave this being called eventName, I th
{
- var method = methods[j];
- if (method in CSSStyleSheet.prototype)
+ // As far as possible we must track everything we use that could be
+ // sabotaged by the website later in order to circumvent us.
+ var RealWebSocket = WebSocket;
+ var closeWebSocket = RealWebSocket.prototype.close;
+ var addEventListener = document.addEventListener.bind(document);
+ var removeEventListener = document.removeEventListener.bind(document);
+ var dispatchEvent = document.dispatchEvent.bind(document);
+ var CustomEvent = window.CustomEvent;
+ var boundCall = Function.prototype.call.bind(Function.prototype.call);
+ var functionToString = Function.prototype.toString;
+ // (Safari 9 considers WebSocket to be an object rather than a function.)
+ var webSocketString = RealWebSocket.toString();
+
+ function checkRequest(url, callback)
{
- var origin = "CSSStyleSheet.prototype." + method;
- code.push(" var " + method + " = " + origin + ";",
- " " + origin + " = function(index)",
- " {",
- " if (this != style.sheet)",
- " " + method + ".call(this, index);",
- " }");
+ var incomingEventName = eventName + "-" + url;
+ function listener(event)
+ {
+ callback(event.detail);
+ removeEventListener(incomingEventName, listener);
+ }
+ addEventListener(incomingEventName, listener);
+
+ dispatchEvent(new CustomEvent(eventName, {
+ detail: {url: url}
+ }));
}
- }
- code.push("})();");
+ function wrappedToString()
+ {
+ if (this === WebSocket)
+ return webSocketString;
+ if (this === wrappedToString)
+ return boundCall(functionToString, functionToString);
+ return boundCall(functionToString, this);
+ };
+ Function.prototype.toString = wrappedToString;
- var script = document.createElement("script");
- script.async = false;
- script.textContent = code.join("\n");
- document.documentElement.appendChild(script);
- document.documentElement.removeChild(script);
+ WebSocket = function(url, protocols)
+ {
+ var websocket = new RealWebSocket(url, protocols);
+
+ checkRequest(websocket.url, function(blocked)
+ {
+ if (blocked)
+ boundCall(closeWebSocket, websocket);
+ });
+
+ return websocket;
+ };
+
+ var properties = Object.getOwnPropertyNames(RealWebSocket);
+ for (var i = 0; i < properties.length; i++)
+ {
+ var name = properties[i];
+ var desc = Object.getOwnPropertyDescriptor(RealWebSocket, name);
+ Object.defineProperty(WebSocket, name, desc);
+ }
+
+ RealWebSocket.prototype.constructor = WebSocket;
+ }
+
+ runInPage(wrapper, eventName);
}
function init(document)
@@ -401,6 +494,8 @@ function init(document)
var observer = null;
var tracer = null;
+ wrapWebSocket();
+
function getPropertyFilters(callback)
{
ext.backgroundPage.sendMessage({
« 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