Search code examples
node.jswebsocketpcapwinpcap

Node.js - Decoded WinPcap websocket frame is cut off


I'm trying to listen to websocket traffic using the cap module with Node.js (4.5.0) on Windows 7. I am able to parse/decode the payload that is being captured but part of it is cut off when the payload is over a certain length.

I'm using this example to decode the websocket frame (wsDecoded function) and I think the problem is that the response I'm getting from the cap module is returning the entire response in a single buffer, but this example (and some of the other websocket library's I've looked at) seem to expect to process multiple frames when the payload is over a certain size.

I've tried taking the buffer from and breaking it apart into smaller pieces but when I do that the payload just looks like random garbage.

var Cap = require('cap').Cap;
var decoders = require('cap').decoders;
var PROTOCOL = decoders.PROTOCOL;

var c = new Cap();
var device = Cap.findDevice(LOCAL_IP);
var filter = 'port 8088';
var bufSize = 10 * 1024 * 1024;
var buffer = new Buffer(65535);

var linkType = c.open(device, filter, bufSize, buffer);

c.setMinBytes && c.setMinBytes(0);

c.on('packet', function(nbytes, trunc) {
  if (linkType === 'ETHERNET') {
    var ret = decoders.Ethernet(buffer);

    if (ret.info.type === PROTOCOL.ETHERNET.IPV4) {
      ret = decoders.IPV4(buffer, ret.offset);

      if (ret.info.protocol === PROTOCOL.IP.TCP) {
        var datalen = ret.info.totallen - ret.hdrlen;
        ret = decoders.TCP(buffer, ret.offset);
        datalen -= ret.hdrlen;

        var payload = wsDecoded(buffer, ret.offset, ret.offset + datalen);
        console.log(payload.toString());
      }
    }
  }
});

function wsDecoded (data, start, end) {
  var message = data.slice(start, end);
  var FIN = (message[0] & 0x80);
  var RSV1 = (message[0] & 0x40);
  var RSV2 = (message[0] & 0x20);
  var RSV3 = (message[0] & 0x10);
  var Opcode = message[0] & 0x0F;
  var mask = (message[1] & 0x80);
  var length = (message[1] & 0x7F);

  var nextByte = 2;
  if (length === 126) {
    // length = next 2 bytes
    nextByte += 2;
  } else if (length === 127){
    // length = next 8 bytes
    nextByte += 8;
  }

  var maskingKey = null;
  if (mask){
    maskingKey = message.slice(nextByte, nextByte + 4);
    nextByte += 4;
  }

  var payload = message.slice(nextByte, nextByte + length);

  if (maskingKey){
    for (var i = 0; i < payload.length; i++){
     payload[i] = payload[i] ^ maskingKey[i % 4];
    }
  }

  return payload;
}

Example result that is not cut off:

{"Command":"FoldCards","Table":"Ring Game #02","Type":"R","Ghost":"No"}

Example result that is cut off:

{"Command":"Buttons","Table":"Ring Game   #02","Type":"R","Button1":"","Button2":"Ready","Button3":"","Preflop":"No","Call":0,"M

Payload from on('packet') callback when using buffer.toString('binary', ret.offset, datalen). This is very close to what I need but it does not unmask the payload and leaves some random characters in (which I think are the start and stop of the frame but not 100% sure about that)

?'{"Command":"TablesSitting","Tables":[]}?'{"Command":"TablesWaiting","Tables":[]}?~☺({"Command":"RingGameLobby","Clear":"Yes","Count":2,"ID":["Ring Game #01","Ring Game #02"],"Game":["NL Hold'em","NL Hold'em"],"GameIndex":[2,2],"Seats":[10,10],"StakesLo":[10,10],"StakesHi":[20,20],"BuyinMin":[400,400],"BuyinMax":[2000,2000],"Players":[0,0],"Waiting":[0,0],"Password":["No","No"]}?~☺{"Command":"TournamentLobby","Clear":"Yes","Count":0,"ID":[],"SnG":[],"Shootout":[],"Game":[],"GameIndex":[],"Buyin":[],"EntryFee":[],"Rebuy":[],"TS":[],"PreReg":[],"Reg":[],"Max":[],"Starts":[],"StartMin":[],"StartTime":[],"Running":[],"Tables":[],"Password":[]}?~ ?{"Command":"Logins","Clear":"Yes","Total":1,"Player":["harrythree"],"Name":[""],"Location":["Greenville"],"Login":["2016-08-27 13:26:45"]}

Solution

  • You are getting truncated message becasue you didn't read payloadLength correctly. Take a look:

    var length = (message[1] & 0x7F);
    
    var nextByte = 2;
    if (length === 126) {
        // in this case next 2 bytes store lengths. you have to read ones from the buffer
        nextByte += 2;
    } else if (length === 127){
        // in this case next 8 bytes store lengths. you have to read ones from the buffer
        nextByte += 8;
    }
    

    When message size is more than 125 bytes, you have to read correct payloadLength by using next 2 or 8 bytes like RFC says. In your case, you just skip these bytes, so that your "length" is always <= 127 and as result the following line produces truncated payload data:

    var payload = message.slice(nextByte, nextByte + length);
                                                       ^
                                                  always <= 127
    

    I slightly modified your code to debug the problem. This is an example of the implementation I am talking about:

    var Cap = require('cap').Cap;
    var decoders = require('cap').decoders;
    var PROTOCOL = decoders.PROTOCOL;
    
    var c = new Cap();
    var device = Cap.findDevice('127.0.0.1');
    var filter = 'port 3001';
    var bufSize = 10 * 1024 * 1024;
    var buffer = new Buffer(bufSize);
    
    var linkType = c.open(device, filter, bufSize, buffer);
    
    c.setMinBytes && c.setMinBytes(0);
    
    c.on('packet', function(nbytes, trunc) {
    
        console.log('************ packet: length ' + nbytes + ' bytes, truncated? '
            + (trunc ? 'yes' : 'no'));
    
        if (linkType === 'ETHERNET') {
            var ret = decoders.Ethernet(buffer);
    
            if (ret.info.type === PROTOCOL.ETHERNET.IPV4) {
                ret = decoders.IPV4(buffer, ret.offset);
    
                if (ret.info.protocol === PROTOCOL.IP.TCP) {
                    var datalen = ret.info.totallen - ret.hdrlen;
                    ret = decoders.TCP(buffer, ret.offset);
                    datalen -= ret.hdrlen;
    
                    console.log('from: ' + ret.info.srcport + ' to ' + ret.info.dstport);
    
                    console.log("Data len: " + datalen);
    
                    var payload = wsDecoded(buffer, ret.offset, ret.offset + datalen);
                    if (payload) {
                        console.log("MSG:" + payload.toString());
                    } else {
                        console.log("Invalid message");
                    }
    
                }
            }
        }
    });
    
    function wsDecoded (data, start, end) {
        var message = data.slice(start, end);
        var index = 0;
        var FIN = (message[index] & 0x80);
        var RSV1 = (message[index] & 0x40);
        var RSV2 = (message[index] & 0x20);
        var RSV3 = (message[index] & 0x10);
        var Opcode = message[index] & 0x0F;
    
        index++;
    
        var masked = (message[index] & 0x80);
        var payloadLength = message[index] & (~0x80);
        index++;
    
    
        if (payloadLength === 126){
            // length = next 2 bytes
            payloadLength = message.readUInt16BE(index);
            index+=2;
        } else if (payloadLength === 127) {
            // read uint64
            throw new Error("Implement me");
        }
    
        console.log("Opcode: " + Opcode + ", fin: " + FIN + ", masked: " + masked);
    
        var maskingKey = null;
        if (masked){
            maskingKey = message.slice(index, index + 4);
            index += 4;
        }
    
        console.log("Payload length: " + payloadLength);
        var payload = message.slice(index, index + payloadLength);
    
        if (payload.length != payloadLength) {
            console.warn("Length mismatch");
            return false;
        }
    
        if (maskingKey){
            for (var i = 0; i < payload.length; i++){
                payload[i] = payload[i] ^ maskingKey[i % 4];
            }
        }
    
        return payload;
    }