Search code examples
javascriptgoogle-chrome-extensionwebsocketfirefox-addon-webextensionsintercept

How can an extension listen to a WebSocket? (and what if the WebSocket is within an iframe?)


I am writing a Firefox extension that needs to listen to communication between browser and server. For HTTP communication, I use the webRequest library in a background script. But according to this Mozilla bug report, I cannot use webRequest to intercept WebSocket messages. How can my extension listen in on WebSocket communication?

Update:

I've injected my WebSocket wrapper, but it's never called! The WebSocket I am trying to listen to is inside an iframe. Does that matter?


Solution

  • The only way to listen to WebSocket communication is to inject a WebSocket wrapper into the site's code and have it relay the messages to you. You're code should look something like this:

    manifest.json

    {
      ...
      "content_scripts": [
        {
          "matches": [
            "<website-url>",    
          ],
          "js": ["content/syringe.js"]
        }
      ],
      "web_accessible_resources": ["lib/socket-sniffer.js"]
    }
    

    content/syringe.js

    var s = document.createElement('script');
    s.src = browser.runtime.getURL('lib/socket-sniffer.js');
    s.onload = function() {
        this.remove();
    };
    

    lib/socket-sniffer.js

    (function() {
      var OrigWebSocket = window.WebSocket;
      var callWebSocket = OrigWebSocket.apply.bind(OrigWebSocket);
      var wsAddListener = OrigWebSocket.prototype.addEventListener;
      wsAddListener = wsAddListener.call.bind(wsAddListener);
      window.WebSocket = function WebSocket(url, protocols) {
        var ws;
        if (!(this instanceof WebSocket)) {
          // Called without 'new' (browsers will throw an error).
          ws = callWebSocket(this, arguments);
        } else if (arguments.length === 1) {
          ws = new OrigWebSocket(url);
        } else if (arguments.length >= 2) {
          ws = new OrigWebSocket(url, protocols);
        } else { // No arguments (browsers will throw an error)
          ws = new OrigWebSocket();
        }
    
        wsAddListener(ws, 'message', function(event) {
          console.log("Received:", event);
        });
        return ws;
      }.bind();
      window.WebSocket.prototype = OrigWebSocket.prototype;
      window.WebSocket.prototype.constructor = window.WebSocket;
    
      var wsSend = OrigWebSocket.prototype.send;
      wsSend = wsSend.apply.bind(wsSend);
      OrigWebSocket.prototype.send = function(data) {
        console.log("Sent:", data);
        return wsSend(this, arguments);
      };
    })();
    
    (document.head || document.documentElement).appendChild(s);
    

    All credit for the above code obviously goes to the posters linked above. I have only copied tt here for convenience' sake.

    Update:

    Yes, it does matter that the WebSocket is within an iframe! By default, extensions are only loaded in the top-most frame. For it to load into the iframe, you must add "all_frames": true to your manifest.json:

    manifest.json

    {
      ...
      "content_scripts": [
        {
          "matches": [
            "<website-url>",    
          ],
          "all_frames": true,      
          "js": ["content/syringe.js"]
        }
      ],
      "web_accessible_resources": ["lib/socket-sniffer.js"]
    }
    

    If you'd like to read more, here's the documentation for all_frames.