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

Side by Side Diff: include.preload.js

Issue 29347034: Issue 1727 - Prevent circumvention via WebSocket (Closed)
Patch Set: Use Proxy, intercept events Created July 12, 2016, 11:19 a.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | lib/requestBlocker.js » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 /* 1 /*
2 * This file is part of Adblock Plus <https://adblockplus.org/>, 2 * This file is part of Adblock Plus <https://adblockplus.org/>,
3 * Copyright (C) 2006-2016 Eyeo GmbH 3 * Copyright (C) 2006-2016 Eyeo GmbH
4 * 4 *
5 * Adblock Plus is free software: you can redistribute it and/or modify 5 * Adblock Plus is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License version 3 as 6 * it under the terms of the GNU General Public License version 3 as
7 * published by the Free Software Foundation. 7 * published by the Free Software Foundation.
8 * 8 *
9 * Adblock Plus is distributed in the hope that it will be useful, 9 * Adblock Plus is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details. 12 * GNU General Public License for more details.
13 * 13 *
14 * You should have received a copy of the GNU General Public License 14 * You should have received a copy of the GNU General Public License
15 * along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. 15 * along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
16 */ 16 */
17 17
18 var MutationObserver = window.MutationObserver || window.WebKitMutationObserver; 18 var MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
19 var SELECTOR_GROUP_SIZE = 200; 19 var SELECTOR_GROUP_SIZE = 200;
20 var id = Math.random().toString(36).substr(2);
20 21
21 var typeMap = { 22 var typeMap = {
22 "img": "IMAGE", 23 "img": "IMAGE",
23 "input": "IMAGE", 24 "input": "IMAGE",
24 "picture": "IMAGE", 25 "picture": "IMAGE",
25 "audio": "MEDIA", 26 "audio": "MEDIA",
26 "video": "MEDIA", 27 "video": "MEDIA",
27 "frame": "SUBDOCUMENT", 28 "frame": "SUBDOCUMENT",
28 "iframe": "SUBDOCUMENT", 29 "iframe": "SUBDOCUMENT",
29 "object": "OBJECT", 30 "object": "OBJECT",
(...skipping 312 matching lines...) Expand 10 before | Expand all | Expand 10 after
342 var observer = new MutationObserver(function() 343 var observer = new MutationObserver(function()
343 { 344 {
344 if (style.parentNode != parentNode) 345 if (style.parentNode != parentNode)
345 parentNode.appendChild(style); 346 parentNode.appendChild(style);
346 }); 347 });
347 348
348 observer.observe(parentNode, {childList: true}); 349 observer.observe(parentNode, {childList: true});
349 return observer; 350 return observer;
350 } 351 }
351 352
353 function injectJS(f)
354 {
355 var args = JSON.stringify(Array.prototype.slice.call(arguments, 1));
356 args = args.substring(1, args.length - 1);
357 var codeString = "(" + f.toString() + ")(" + args + ");";
358
359 var script = document.createElement("script");
360 script.async = false;
361 script.textContent = codeString;
362 document.documentElement.appendChild(script);
363 document.documentElement.removeChild(script);
364 }
365
352 function protectStyleSheet(document, style) 366 function protectStyleSheet(document, style)
353 { 367 {
354 var id = Math.random().toString(36).substr(2)
355 style.id = id; 368 style.id = id;
356 369
357 var code = [ 370 var protector = function(id)
358 "(function()",
359 "{",
360 ' var style = document.getElementById("' + id + '") ||',
361 ' document.documentElement.shadowRoot.getElementById("' + id + '");',
362 ' style.removeAttribute("id");'
363 ];
364
365 var disableables = ["style", "style.sheet"];
366 for (var i = 0; i < disableables.length; i++)
367 { 371 {
368 code.push(" Object.defineProperty(" + disableables[i] + ', "disabled", ' 372 var style = document.getElementById(id) ||
369 + "{value: false, enumerable: true});") ; 373 document.documentElement.shadowRoot.getElementById(id);
374 style.removeAttribute("id");
375
376 var i;
377 var disableables = [style, style.sheet];
378 for (i = 0; i < disableables.length; i += 1)
379 Object.defineProperty(disableables[i], "disabled",
380 {value: false, enumerable: true});
381
382 var methods = ["deleteRule", "removeRule"];
383 for (i = 0; i < methods.length; i += 1)
384 {
385 if (methods[i] in CSSStyleSheet.prototype)
386 {
387 (function(method)
388 {
389 var original = CSSStyleSheet.prototype[method];
390 CSSStyleSheet.prototype[method] = function(index)
391 {
392 if (this != style.sheet)
393 original.call(this, index);
394 };
395 }(methods[i]));
396 }
397 }
398 };
399
400 injectJS(protector, id);
401 }
402
403 // Neither Chrome[1] nor Safari allow us to intercept WebSockets, and therefore
404 // some ad networks are misusing them as a way to serve adverts and circumvent
405 // us. As a workaround we wrap WebSocket, preventing blocked WebSocket
406 // connections from being opened. We go to some lengths to avoid breaking code
407 // using WebSockets, circumvention and as far as possible detection.
408 // [1] - https://bugs.chromium.org/p/chromium/issues/detail?id=129353
409 function wrapWebSocket()
410 {
411 if (typeof WebSocket == "undefined" ||
412 typeof WeakMap == "undefined" ||
413 typeof Proxy == "undefined")
414 return;
415
416 var eventName = "abpws-" + id;
417
418 document.addEventListener(eventName, function(event)
419 {
420 ext.backgroundPage.sendMessage({
421 type: "websocket-request",
422 url: event.detail.url
423 }, function (block)
424 {
425 document.dispatchEvent(
426 new CustomEvent(eventName + "-" + event.detail.url, {detail: block})
427 );
428 });
429 });
430
431 function wrapper(eventName)
432 {
433 var RealWebSocket = WebSocket;
434
435 function checkRequest(url, protocols, callback)
436 {
437 var incomingEventName = eventName + "-" + url;
438 function listener(event)
439 {
440 callback(event.detail);
441 document.removeEventListener(incomingEventName, listener);
442 }
443 document.addEventListener(incomingEventName, listener);
444
445 document.dispatchEvent(new CustomEvent(eventName, {
446 detail: {url: url, protocols: protocols}
447 }));
448 }
449
450 // We need to store state for our wrapped WebSocket instances, that webpages
451 // can't access. We use a WeakMap to avoid leaking memory in the case that
452 // all other references to a WebSocket instance have been deleted.
453 var instanceStorage = new WeakMap();
454
455 var eventNames = ["close", "open", "message", "error"];
456 var eventAttrNames = ["onclose", "onopen", "onmessage", "onerror"];
457
458 function addRemoveEventListener(storage, key, type, listener)
kzar 2016/07/12 11:34:32 Should we care about the useCapture parameter for
459 {
460 if (typeof listener == "object")
461 listener = listener.handleEvent;
462
463 if (!(eventNames.indexOf(type) > -1 && typeof listener == "function"))
464 return;
465
466 var listeners = storage.listeners[type];
467 var listenerIndex = listeners.indexOf(listener);
468
469 if (key == "addEventListener")
470 {
471 if (listenerIndex == -1)
472 listeners.push(listener);
473 }
474 else if (listenerIndex > -1)
475 listeners.splice(listenerIndex, 1);
476 }
477
478 // We check if a WebSocket should be blocked before actually creating it. As
479 // this is done asynchonously we must queue up any actions (method calls and
480 // assignments) that happen in the mean time.
481 // Once we have a result, we create the WebSocket (if allowed) and perform
482 // the queued actions.
483 function processQueue(storage)
kzar 2016/07/12 11:34:33 Queuing up assignments and method calls seems rath
484 {
485 for (var i = 0; i < storage.queue.length; i += 1)
486 {
487 var action = storage.queue[i][0];
488 var key = storage.queue[i][1];
489 var value = storage.queue[i][2];
490
491 if (action == "set")
492 storage.websocket[key] = value;
493 else if (action == "call")
494 storage.websocket[key].apply(storage.websocket, value);
495 }
496 }
497
498 var defaults = {
499 readyState: RealWebSocket.CONNECTING,
500 bufferedAmount: 0,
501 extensions: "",
502 binaryType: "blob"
503 };
504
505 // We cannot dispatch WebSocket events directly to their listeners since
506 // event.target would give a way back to the original WebSocket constructor.
507 // Instead we must listen for events ourselves and pass them on, taking care
508 // to spoof the event target and isTrusted flag.
509 function wrappedEventListener(name, me)
510 {
511 var storage = instanceStorage.get(me);
512 return function(event)
513 {
514 var eventProxy = new Proxy(event, {
515 get: function(target, key)
516 {
517 if (key == "isTrusted" && "isTrusted" in target)
518 return true;
519 if (key == "target" || key == "srcElement" || key == "currentTarget" )
520 return me;
521 return target[key];
522 }
523 });
524
525 var listeners = storage.listeners[name];
526 for (var i = 0; i < listeners.length; i += 1)
527 listeners[i].call(me, eventProxy);
528 var listener = storage.listeners["on" + name];
529 if (typeof listener == "function")
530 listener.call(me, eventProxy);
531 };
532 }
533
534 WebSocket = function(url, protocols)
kzar 2016/07/12 11:34:33 So far I don't intercept WebSocket.toString() so i
lainverse 2016/07/12 12:22:20 If we going all the way to avoid detection then we
535 {
536 var me = this;
537 var storage = {
538 url: url,
539 protocol: protocols || "",
540 queue: [],
541 websocket: null,
542 blocked: false,
543 listeners: {
544 onclose: null,
545 onopen: null,
546 onmessage: null,
547 onerror: null,
548 close: [],
549 open: [],
550 message: [],
551 error: []
552 }
553 };
554 instanceStorage.set(me, storage);
555
556 checkRequest(url, protocols, function(blocked)
557 {
558 if (blocked)
559 {
560 storage.blocked = true;
561 wrappedEventListener("error", me)(new Error("error"));
562 }
563 else
564 {
565 storage.websocket = new RealWebSocket(url, protocols);
566 for (var i = 0; i < eventNames.length; i += 1)
kzar 2016/07/12 11:34:32 If a website creates a WebSocket (which manages to
567 storage.websocket.addEventListener(
568 eventNames[i],
569 wrappedEventListener(eventNames[i], me)
570 );
571 processQueue(storage);
572 }
573 delete storage.queue;
574 });
575 };
576 Object.defineProperties(WebSocket, {
577 CONNECTING: {value: 0, enumerable: true},
578 OPEN: {value: 1, enumerable: true},
579 CLOSING: {value: 2, enumerable: true},
580 CLOSED: {value: 3, enumerable: true}
581 });
582 WebSocket.prototype = new Proxy(RealWebSocket.prototype,{
583 set: function(target, key, value, me)
584 {
585 var storage = instanceStorage.get(me);
586
587 if (!storage)
588 target[key] = value;
589 else if (eventAttrNames.indexOf(key) > -1)
590 storage.listeners[key] = value;
591 else if (storage.websocket)
592 storage.websocket[key] = value;
593 else if (!storage.blocked)
594 storage.queue.push(["set", key, value]);
595
596 return true;
597 },
598 get: function(target, key, me)
599 {
600 if (key == "__proto__" && me != WebSocket.prototype)
601 return WebSocket.prototype;
602
603 if (key == "constructor")
604 return WebSocket;
605
606 var storage = instanceStorage.get(me);
607 if (!storage)
608 return target[key];
609
610 if (key == "addEventListener" || key == "removeEventListener")
611 return function()
612 {
613 if (arguments.length > 1)
614 addRemoveEventListener(storage, key, arguments[0], arguments[1]);
615 };
616
617 if (eventAttrNames.indexOf(key) > -1)
618 return storage.listeners[key];
619
620 var desc = Object.getOwnPropertyDescriptor(target, key);
621 if (desc && typeof desc.value == "function")
622 return function()
623 {
624 if (storage.websocket)
625 storage.websocket[key].apply(storage.websocket, arguments);
626 else if (!storage.blocked)
627 storage.queue.push(["call", key, arguments]);
628 };
629
630 if (storage.websocket)
631 return storage.websocket[key];
632 if (key == "url" || key == "protocol")
633 return storage[key];
634 if (storage.blocked && key == "readyState")
635 return WebSocket.CLOSED;
636 if (key in defaults)
637 return defaults[key];
638 return undefined;
639 }
640 });
370 } 641 }
371 642
372 var methods = ["deleteRule", "removeRule"]; 643 injectJS(wrapper, eventName);
373 for (var j = 0; j < methods.length; j++)
374 {
375 var method = methods[j];
376 if (method in CSSStyleSheet.prototype)
377 {
378 var origin = "CSSStyleSheet.prototype." + method;
379 code.push(" var " + method + " = " + origin + ";",
380 " " + origin + " = function(index)",
381 " {",
382 " if (this != style.sheet)",
383 " " + method + ".call(this, index);",
384 " }");
385 }
386 }
387
388 code.push("})();");
389
390 var script = document.createElement("script");
391 script.async = false;
392 script.textContent = code.join("\n");
393 document.documentElement.appendChild(script);
394 document.documentElement.removeChild(script);
395 } 644 }
396 645
397 function init(document) 646 function init(document)
398 { 647 {
399 var shadow = null; 648 var shadow = null;
400 var style = null; 649 var style = null;
401 var observer = null; 650 var observer = null;
402 var tracer = null; 651 var tracer = null;
403 652
653 wrapWebSocket();
654
404 function getPropertyFilters(callback) 655 function getPropertyFilters(callback)
405 { 656 {
406 ext.backgroundPage.sendMessage({ 657 ext.backgroundPage.sendMessage({
407 type: "filters.get", 658 type: "filters.get",
408 what: "cssproperties" 659 what: "cssproperties"
409 }, callback); 660 }, callback);
410 } 661 }
411 var propertyFilters = new CSSPropertyFilters(window, getPropertyFilters, 662 var propertyFilters = new CSSPropertyFilters(window, getPropertyFilters,
412 addElemHideSelectors); 663 addElemHideSelectors);
413 664
(...skipping 150 matching lines...) Expand 10 before | Expand all | Expand 10 after
564 }, true); 815 }, true);
565 816
566 return updateStylesheet; 817 return updateStylesheet;
567 } 818 }
568 819
569 if (document instanceof HTMLDocument) 820 if (document instanceof HTMLDocument)
570 { 821 {
571 checkSitekey(); 822 checkSitekey();
572 window.updateStylesheet = init(document); 823 window.updateStylesheet = init(document);
573 } 824 }
OLDNEW
« 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