Intercept WebSocket messages

Viewed 3155

With ajax requests it can be done with this code:

let oldXHROpen = window.XMLHttpRequest.prototype.open;
window.lastXhr = '';
window.XMLHttpRequest.prototype.open = function(method, url, async, user, password) {
  this.addEventListener('load', function() {
    window.lastXhr = this.responseText;
  });
  return oldXHROpen.apply(this, arguments);
};

lastXhr variable will hold the last response.

But how can this be achieved for websockets too?

5 Answers

you would need to make this wrapper as soon as possible

For that answer @brunoff you're correct.. and you can always use your functions before server's by puppet window logic, OR you can hijack the data from MessageEvent itself

function listen(fn){
  fn=fn||console.log
  let property=Object.getOwnPropertyDescriptor
  (MessageEvent.prototype,"data")
  const data=property.get
  function lookAtMessage(){ //to replace get function
    let socket=this.currentTarget instanceof WebSocket
    if(!socket){return data.call(this)}
    let msg=data.call(this)
    Object.defineProperty(this,"data",{value:msg}) //anti-loop
    fn({data:msg,socket:this.currentTarget,event:this})
    return msg
  }
  property.get=lookAtMessage
  Object.defineProperty
  (MessageEvent.prototype,"data",property)
}
listen( ({data})=>console.log(data) )

You can try putting in the code and running it in the console on this page and then running their websocket example

To intercept the messages, you will have to spy on the onmessage = fn and addEventListener("message", fn) calls.

To be able to modify the onmessage we have to override the global WebSocket in the first place. The below is intercepting the incoming messages, but in a similar way you can spy on the send method to intercept the outgoing messages (the ones sent by the client to the server).

I tested this on a page using Firebase and it works nicely, but you have to initialize it before the other scripts making sure that the websocket library (it can be socket.io, ws, etc) is using the overridden WebSocket constructor.

Spy the Incoming Messages and modify the data

Eventually you can override the data before calling the real message listener – this becomes handy if you do not have control over the page functionality and want to inject your own data in the message listener.

const OriginalWebsocket = window.WebSocket
const ProxiedWebSocket = function() {
  console.log("Intercepting web socket creation")

  const ws = new OriginalWebsocket(...arguments)

  const originalAddEventListener = ws.addEventListener
  const proxiedAddEventListener = function() {
    if (arguments[0] === "message") {
      const cb = arguments[1]
      arguments[1] = function() {
        // Here you can get the actual data from the incoming messages
        // Here you can even change the data before calling the real message listener
        Object.defineProperty(e, "data", { value: 'your injected data' })
        console.log("intercepted", arguments[0].data)
        return cb.apply(this, arguments)
      }
    }
    return originalAddEventListener.apply(this, arguments)
  }
  ws.addEventListener = proxiedAddEventListener

  Object.defineProperty(ws, "onmessage", {
    set(func) {
      return proxiedAddEventListener.apply(this, [
        "message",
        func,
        false
      ]);
    }
  });
  return ws;
};

window.WebSocket = ProxiedWebSocket;

If you do not need to modify the data, you can follow the second part of the answer.

Spy the Incoming messages without modifying the data

If you want to listen for messages only, without overriding the data, things are simpler:

const OriginalWebsocket = window.WebSocket
const ProxiedWebSocket = function() {
  const ws = new OriginalWebsocket(...arguments)
  ws.addEventListener("message", function (e) {
    // Only intercept
    console.log(e.data)
  })
  return ws;
};
window.WebSocket = ProxiedWebSocket;

Spy the Outgoing Messages

In a very similar way, you can proxy the send method which is used to send data to the server.

const OriginalWebsocket = window.WebSocket
const ProxiedWebSocket = function() {
  const ws = new OriginalWebsocket(...arguments)
  const originalSend = ws.send
  const proxiedSend = function() {
    console.log("Intercepted outgoing ws message", arguments)
    // Eventually change the sent data
    // arguments[0] = ...
    // arguments[1] = ...
    return originalSend.apply(this, arguments)
  }
  ws.send = proxiedSend
  return ws;
};
window.WebSocket = ProxiedWebSocket;

Feel free to ask any questions if anything is unclear.

Intro

The question/bounty/op is specifically asking for a reputable source. Instead of rolling a custom solution, my proposal is that a known proven library should be used - that has been used, audited, forked, and in general used by the community and that is hosted on github.

enter image description here

The second option is to roll your own (though not recommended) and there are many exccelent answers on how to do it involving the addEventListener

wshook

Wshook is a library (hosted on github) that allows to easily intercept and modify WebSocket requests and message events. It has been starred and forked multiple times.

Disclaimer: I don't have any relationship with the specific project.strong text

Example:

wsHook.before = function(data, url, wsObject) {
    console.log("Sending message to " + url + " : " + data);
}

// Make sure your program calls `wsClient.onmessage` event handler somewhere.
wsHook.after = function(messageEvent, url, wsObject) {
    console.log("Received message from " + url + " : " + messageEvent.data);
    return messageEvent;
}

From the documentation, you will find:

wsHook.before - function(data, url, wsObject):

Invoked just before calling the actual WebSocket's send() method.

This method must return data which can be modified as well.

wsHook.after - function(event, url, wsObject):

Invoked just after receiving the MessageEvent from the WebSocket server and before calling the WebSocket's onmessage Event Handler.

Websocket addEventListener

The WebSocket object supports .addEventListener().
Please see: Multiple Handlers for Websocket Javascript

In a solution similar to yours, where the window.XMLHttpRequest was replaced with a wrapped version that feeds window.lastXhr, we replace window.WebSockets with a wrapped version that feeds window.WebSocketMessages with all messages and timestamps received from all websockets created after this script.

window.watchedWebSockets = [];
window.WebSocketMessages = [];

function WebSocketAttachWatcher(websocket) {
    websocket.addEventListener("message", (event)=>window.WebSocketMessages.push([event.data,Date.now()]));
    window.watchedWebSockets.push(websocket);
}

// here we replace WebSocket with a wrapped one, that attach listeners on 
window.WebSocketUnchanged = window.WebSocket;
window.WebSocket = function(...args) {
    const websocket = new window.WebSocketUnchanged(...args);
    WebSocketAttachWatcher(websocket);
    return websocket;
}

Differently from your XMLRequest case, the websocket may already exist. If you need garanties that all websockets would be catched then you would need to make this wrapper as soon as possible. If you just can't, there's an not so good trick to capture already existing websockets once they send a message:

// here we detect existing websockets on send event... not so trustable
window.WebSocketSendUnchanged = window.WebSocketUnchanged.prototype.send;
window.WebSocket.prototype.send = function(...args) {
    console.log("firstsend");
    if (!(this in window.watchedWebSockets))
        WebSocketAttachWatcher(this);
    this.send = window.WebSocketSendUnchanged; // avoid passing here again on next send
    window.WebSocketSendUnchanged.call(this, ...args);
}

It is not so trustable since if they don't send but receive they will stay unnoticed.

if you are using nodejs then you can use socket.io

yarn add socket.io

after installation, you can use the middleware of socket.io

io.use(async (socket, next) => {
  try {
    const user = await fetchUser(socket);
    socket.user = user;
  } catch (e) {
    next(new Error("unknown user"));
  }
});
Related