All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.threadly.litesockets.protocols.ws.WebSocketFrameParser Maven / Gradle / Ivy

package org.threadly.litesockets.protocols.ws;

import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.ParseException;
import java.util.Random;

import org.threadly.litesockets.utils.Base64;
import org.threadly.litesockets.utils.MergedByteBuffers;


/**
 * Simple frame parser for websockets.
 * 
 * @author lwahlmeier
 *
 */
public class WebSocketFrameParser {
  public static final String MAGIC_UUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; 
  
  private static final byte[] MAGIC_UUID_BA = MAGIC_UUID.getBytes();
  private static final int UNSIGN_BYTE_MASK = 0xff;
  private static final int UNSIGNED_SHORT_MASK = 0xffff;
  private static final int OPCODE_MASK = 0xf;
  private static final int WS_SMALL_LENGTH_MASK = 0x7f;
  private static final int WS_SHORT_SIZE = 126;
  private static final int WS_LONG_SIZE = 127;
  private static final int WS_SHORT_LENGTH = 2;
  private static final int WS_LONG_LENGTH = 8;
  private static final int MASK_SIZE = 4;
  private static final int MIN_WS_FRAME_SIZE = 2;
  private static final int MAX_WS_FRAME_SIZE = 14;
  private static final int STATIC_FOUR = 4;
  private static final int STATIC_FIVE = 5;
  private static final int STATIC_SIX = 6;
  private static final int STATIC_SEVEN = 7;
  private static final int DEFAULT_SECRET_KEY_SIZE = 20;
  private static final String DEFAULT_SECRET_HASH_ALGO = "SHA-1";
  private static final Random RANDOM = new Random();
  
  
  private WebSocketFrameParser() {}
  
  public static String makeSecretKey() {
    return makeSecretKey(DEFAULT_SECRET_KEY_SIZE);
  }
  
  public static String makeSecretKey(final int size) {
    byte[] ba = new byte[size];
    RANDOM.nextBytes(ba);
    return Base64.encode(ba);
  }

  /**
   * Makes a Sec-WebSocket-Key response string.
   * 
   * @param str base64 string passed in the Sec-WebSocket-Key header.
   * @return a base64 string to set as the Sec-WebSocket-Key response.
   */
  public static String makeKeyResponse(final String str) {
    final MessageDigest md;
    try {
      md = MessageDigest.getInstance(DEFAULT_SECRET_HASH_ALGO);
      md.update(str.getBytes());
      md.update(MAGIC_UUID_BA);
      return Base64.encode(md.digest());
    } catch (NoSuchAlgorithmException e) {
      throw new RuntimeException("HUGE problem we dont support the SHA1 hash cant to websockets!!!!!", e);
    }
  }
  
  public static boolean validateKeyResponse(final String orig, final String response) {
    String correctResponse =  makeKeyResponse(orig);
    return response.equals(correctResponse);
  }

  /**
   * Parses a WebSocket frame out of the provided {@link ByteBuffer}.
   * 
   * @param bb the ByteBuffer containing the WebSocketFrame.
   * @return a {@link WebSocketFrame}.
   * @throws ParseException this is thrown if there is not enough data to make a {@link WebSocketFrame}. 
   */
  public static WebSocketFrame parseWebSocketFrame(final ByteBuffer bb) throws ParseException {
    MergedByteBuffers mbb = new MergedByteBuffers();
    mbb.add(bb);
    int origSize = mbb.remaining();
    WebSocketFrame wsf = parseWebSocketFrame(mbb);
    bb.position(bb.position() + origSize - mbb.remaining());
    return wsf;
  }
  
  /**
   * Parses a WebSocket frame from the passed in {@link MergedByteBuffers}.  Only the data for the frame
   * will be parsed out of the ByteBuffer, the payload will remain.
   * 
   * @param mbb {@link MergedByteBuffers} containing the frame as the first bytes.
   * @return a {@link WebSocketFrame} object to get info about the data in the {@link WebSocketFrame}.
   * @throws ParseException this is thrown if there is not enough data to make a {@link WebSocketFrame}.
   */
  public static WebSocketFrame parseWebSocketFrame(final MergedByteBuffers mbb) throws ParseException {
    final int size = getFrameLength(mbb);
    if(size > 0 && mbb.remaining() >= size) {
      ByteBuffer nbb = mbb.pull(size);
      return new WebSocketFrame(nbb);
    } else {
      throw new ParseException("Not enough data to make a WebSocketFrame", 0);
    }
  }

  /**
   * Gets the length of a the WebSocket frame from a passed in MergedByteBuffers object.
   * This does not modify the MergedByteBuffer.
   * 
   * @param mbb MergedByteBuffer to get the Frame length from.
   * @return size of the frame in bytes, or -1 is there is not enough data to make figure out the frame length.
   */
  public static int getFrameLength(final MergedByteBuffers mbb) {
    final MergedByteBuffers nmbb = mbb.copy();
    return getFrameLength(nmbb.pull(Math.min(nmbb.remaining(), MAX_WS_FRAME_SIZE)));
  }

  /**
   * This will mask or unmask data against provided mask.
   * 
   * @param nbb the {@link ByteBuffer} to apply the mask to.
   * @param mask the mask to apply to the ByteBuffer.
   * @return {@link ByteBuffer} with the provided mask applyed to it.
   */
  public static ByteBuffer doDataMask(final ByteBuffer nbb, final int mask) {
    if(mask == 0) {
      return nbb;
    } else {
      byte[] maskArray = ByteBuffer.allocate(MASK_SIZE).putInt(mask).array();
      ByteBuffer rbb = ByteBuffer.allocate(nbb.remaining());
      while(nbb.remaining()>=MASK_SIZE) {
        rbb.putInt(nbb.getInt()^mask);
      }
      for(int i=0; nbb.remaining() > 0; i++) {
        rbb.put((byte)(nbb.get()^maskArray[i%MASK_SIZE]));
      }
      rbb.flip();
      return rbb;
    }
  }

  /**
   * Gives the total length of the Frame in the provided {@link ByteBuffer}.
   * It will not shift any data in the provided {@link ByteBuffer}.
   * 
   * @param bb the {@link ByteBuffer} to find the frame length on.
   * @return the size of the frame in this {@link ByteBuffer}.
   */
  public static int getFrameLength(final ByteBuffer bb) {
    if(bb.remaining() < MIN_WS_FRAME_SIZE) {
      return -1;
    }

    int size = MIN_WS_FRAME_SIZE + getLengthSize(bb);
    if(hasMask(bb)) {
      size += MASK_SIZE;
    } 
    return size;
  }
  
  /**
   * Creates a {@link WebSocketFrame} object with the provided parameters.
   * 
   * @param size the size of the payload in the WebSocket Frame.
   * @param opCode The opCode to put in this WebSocket.
   * @param mask true if a mask should be added to this frame, false if not.
   * @return a {@link WebSocketFrame} object created with the provided params.
   */
  public static WebSocketFrame makeWebSocketFrame(final int size, byte opCode, final boolean mask) {
    return makeWebSocketFrame(size, true, opCode, mask);
  }
  
  /**
   * Creates a {@link WebSocketFrame} object with the provided parameters.
   * 
   * @param size the size of the payload in the WebSocket Frame.
   * @param isFinished true if we should mark this WebSocket Frame as finished false if not.
   * @param opCode The opCode to put in this WebSocket.
   * @param mask true if a mask should be added to this frame, false if not.
   * @return a {@link WebSocketFrame} object created with the provided params.
   */
  public static WebSocketFrame makeWebSocketFrame(final int size, boolean isFinished, byte opCode, final boolean mask) {

    ByteBuffer nbb;
    int maskExtra = mask ? MASK_SIZE : 0;
    byte bmask = mask ? (byte)1 : (byte)0;
    byte firstByte = opCode;
    if(isFinished) {
      firstByte = (byte)(firstByte | (1<> STATIC_SEVEN == 1;
  }



  /**
   * WebSocketFrame object.  This is allows you to easily get information about the WebSocketFrame data.
   * This object is immutable.
   * 
   * 
   * @author lwahlmeier
   *
   */
  public static class WebSocketFrame {
    private final ByteBuffer bb;
    private final int frameLength;

    protected WebSocketFrame(final ByteBuffer bb) {
      frameLength = getFrameLength(bb);
      if(frameLength < 0 || bb.remaining() < frameLength) {
        throw new IllegalStateException("Not enough data to make a WebSocketFrame");
      }
      this.bb = bb;
    }
    
    public ByteBuffer getRawFrame() {
      return bb.duplicate();
    }

    public boolean isFinished() {
      return ((bb.get(0)&UNSIGN_BYTE_MASK) >> STATIC_SEVEN) == 1;
    }

    public boolean hasRSV1() {
      return ((bb.get(0) >> STATIC_SIX) &0x1) == 1;
    }

    public boolean hasRSV2() {
      return ((bb.get(0) >> STATIC_FIVE) &0x1) == 1;
    }

    public boolean hasRSV3() {
      return ((bb.get(0) >> STATIC_FOUR) &0x1) == 1;
    }

    public int getOpCode() {
      return bb.get(0) & OPCODE_MASK;
    }

    public boolean hasMask() {
      return (bb.get(1) & UNSIGN_BYTE_MASK) >> STATIC_SEVEN == 1;
    }

    public long getPayloadDataLength() {
      byte sl = getSmallLen(bb);
      if(sl < WS_SHORT_SIZE) {
        return sl;
      } else if(sl == WS_SHORT_SIZE) {
        return bb.getShort(2) & UNSIGNED_SHORT_MASK;
      } else {
        return bb.getLong(2);
      }
    }

    public int getMaskValue() {
      if(hasMask()) {
        return bb.getInt(getFrameLength(bb)-MASK_SIZE);
      }
      return 0;
    }

    public byte[] getMaskArray() {
      byte[] ba = new byte[MASK_SIZE];
      if(hasMask()) {
        final int start = getFrameLength(bb)-MASK_SIZE;
        for(int i=0; i




© 2015 - 2025 Weber Informatics LLC | Privacy Policy