6

Chrome extensions have the ability to intercept all web requests to specified URLs using chrome.webRequest.onBeforeRequest. This includes not only static asset requests, but requests for AJAX, PJAX, favicons, and everything in between.

Apple provides a few close approximations to this functionality, such as the beforeLoad (handles images, CSS, and JS) and beforeNavigate (handles full page loads) event handlers, but neither catch AJAX requests. I've tried overloading XMLHttpRequest in an attempt to catch AJAX loads to no avail (I might be doing something wrong). Here's a brief example of how I'm doing this:

var originalOpen = window.XMLHttpRequest.prototype.open;
window.XMLHttpRequest.prototype.open = function(method, url, async, username, password) {
    console.log("overriden");
    return originalOpen.apply(this, arguments);
}

How can I catch all web requests (AJAX, CSS, JS, etc.) in a Safari extension?

Dan Loewenherz
  • 10,879
  • 7
  • 50
  • 81
  • I am not an expert on the topic, but where are you executing this code? In an [injected script](https://developer.apple.com/library/safari/documentation/Tools/Conceptual/SafariExtensionGuide/InjectingScripts/InjectingScripts.html)? Then you have the same problem as Chrome content scripts have: they are isolated from the webpage, and are modifying their own copy of `XMLHttpRequest`. – Xan Jan 21 '15 at 13:10
  • Injected scripts are run within the context of the loaded page, I believe. – Dan Loewenherz Jan 21 '15 at 13:12
  • I linked that page specifically for reference. _"...injected scripts and scripts included in the webpage run in isolated worlds, with no access to each other’s functions or data"_ – Xan Jan 21 '15 at 13:13
  • In Chrome, this is defeated by injecting a ` – Xan Jan 21 '15 at 13:13
  • Thanks for that quote. I missed that. Perhaps adding it into the DOM would do it? – Dan Loewenherz Jan 21 '15 at 13:15
  • You can certainly try. – Xan Jan 21 '15 at 13:18
  • I just tried injecting it into the DOM. Even though the elements make it into the page, they don't appear to execute (i.e., console.log messages don't make it through). – Dan Loewenherz Jan 21 '15 at 14:42

1 Answers1

4

Update: You can check entire code flow on my first Safari Extension I've wrote for TimeCamp tracker: https://github.com/qdevro/timecamp.safariextz

I have succeeded to intercept all AJAX calls (actually the responses were interesting for me, because there all the magic happens), but unfortunately I couldn't find (yet) a solution to send it back to my injected script (I still work on this) now fully working - getting the xhr to the injected script:

I've done it like this:

1) on the injected START script, I've added into the DOM another script (the one which does the interception):

    $(document).ready(function(){
      var script = document.createElement('script');
      script.type = 'text/javascript';
      script.src = safari.extension.baseURI + 'path/to/your/script/bellow.js';
      document.getElementsByTagName('head')[0].appendChild(script);
    })

2) the interception code uses this repository as override of the XMLHttpRequest object, that I've tweaked a little bit as well in order to attach the method, url and sent data to it in order to be easily available when the response get's back.

Basically, I've overriden the open() method of the XMLHttpsRequest to attach those values that I might need in my script, and added the sentData in the send() method as well:

    var RealXHROpen = XMLHttpRequest.prototype.open;
    ...
    // Override open method of all XHR requests (inside wire() method
    XMLHttpRequest.prototype.open = function(method, url, async, user, pass) {
      this.method = method;
      this.url = url;

       RealXHROpen.apply(this, arguments);
    }
    ...
    // Override send method of all XHR requests
    XMLHttpRequest.prototype.send = function(sentData) {
       ...
       this.sentData = sentData;
       ...
    }

Then, I've added a callback on the response, which get's a modified XMLHttpRequest object WHEN the data comes back, and cotains everything: url, method, sentData and responseText with the retrieved data:

    AjaxInterceptor.addResponseCallback(function(xhr) {
      console.debug("response",xhr);
      // xhr.method - contains the method used by the call
      // xhr.url - contains the URL was sent to
      // xhr.sentData - contains all the sent data (if any)
      // xhr.responseText - contains the data sent back to the request

      // Send XHR object back to injected script using DOM events
      var event = new CustomEvent("ajaxResponse", {
        detail: xhr
      });

      window.dispatchEvent(event);
    });

    AjaxInterceptor.wire();

For sending the XHR object from the intercept script back to the injected script, I just had to use DOM events like @Xan has suggested (thanks for that):

    window.addEventListener("ajaxResponse", function(evt) {
      console.debug('xhr response', evt.detail);
      // do whatever you want with the XHR details ...
    }, false);

Some extra hints / (workflow) optimisations that I've used in my project:

  • I've cleaned the GET url's and moved all the parameters (? &) into the dataSent property;
  • I've merged this dataSent property if there's the case (in send(data) method)
  • I've added an identifier on request send (timestamp) in order to match it later (see point bellow and get the idea);
  • I've sent a custom event to the script called "ajaxRequest" in order to prepare / optimise load times (I had to request some other data to some external API using CORS - by passing the call / response back and forth to the global.html which is capable of handling CORS), so I didn't had to wait for the original request to come back before sending my API call, but just matching the responses based on timestamp above;
Community
  • 1
  • 1
qdev
  • 1,371
  • 1
  • 15
  • 18
  • @Xan - I see, let me test the full flow when I get back home (where my Mac & code resides), and I will edit the answer to be complete. Thx ! – qdev Mar 02 '15 at 13:56
  • yeap ! DOM events done the missing part - I'll edit the final steps ;) – qdev Mar 02 '15 at 19:31