blob: 7a1e7edbb92f32a01d0653bb106d4641fe875dae [file] [log] [blame]
// ========================================================================
// Copyright 2011-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
//========================================================================
package org.eclipse.jetty.websocket.protocol;
import java.nio.ByteBuffer;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.websocket.api.ProtocolException;
/**
* A Base Frame as seen in <a href="https://tools.ietf.org/html/rfc6455#section-5.2">RFC 6455. Sec 5.2</a>
*
* <pre>
* 0 1 2 3
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
* +-+-+-+-+-------+-+-------------+-------------------------------+
* |F|R|R|R| opcode|M| Payload len | Extended payload length |
* |I|S|S|S| (4) |A| (7) | (16/64) |
* |N|V|V|V| |S| | (if payload len==126/127) |
* | |1|2|3| |K| | |
* +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
* | Extended payload length continued, if payload len == 127 |
* + - - - - - - - - - - - - - - - +-------------------------------+
* | |Masking-key, if MASK set to 1 |
* +-------------------------------+-------------------------------+
* | Masking-key (continued) | Payload Data |
* +-------------------------------- - - - - - - - - - - - - - - - +
* : Payload Data continued ... :
* + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
* | Payload Data continued ... |
* +---------------------------------------------------------------+
* </pre>
*/
public class WebSocketFrame implements Frame
{
/** Maximum size of Control frame, per RFC 6455 */
public static final int MAX_CONTROL_PAYLOAD = 125;
public static WebSocketFrame binary()
{
return new WebSocketFrame(OpCode.BINARY);
}
public static WebSocketFrame binary(byte buf[])
{
return new WebSocketFrame(OpCode.BINARY).setPayload(buf);
}
public static WebSocketFrame ping()
{
return new WebSocketFrame(OpCode.PING);
}
public static WebSocketFrame pong()
{
return new WebSocketFrame(OpCode.PONG);
}
public static WebSocketFrame text()
{
return new WebSocketFrame(OpCode.TEXT);
}
public static WebSocketFrame text(String msg)
{
return new WebSocketFrame(OpCode.TEXT).setPayload(msg);
}
private boolean fin = true;
private boolean rsv1 = false;
private boolean rsv2 = false;
private boolean rsv3 = false;
private OpCode opcode = null;
private boolean masked = false;
private byte mask[];
/**
* The payload data.
* <p>
* It is assumed to always be in FLUSH mode (ready to read) in this object.
*/
private ByteBuffer data;
private int payloadLength = 0;
/** position of start of data within a fresh payload */
private int payloadStart = -1;
private boolean continuation = false;
private int continuationIndex = 0;
/**
* Default constructor
*/
public WebSocketFrame()
{
reset();
}
/**
* Construct form opcode
*/
public WebSocketFrame(OpCode opcode)
{
reset();
this.opcode = opcode;
}
/**
* Copy constructor for the websocket frame.
* <p>
* Note: the underlying payload is merely a {@link ByteBuffer#slice()} of the input frame.
*
* @param copy
* the websocket to copy.
*/
public WebSocketFrame(WebSocketFrame copy)
{
fin = copy.rsv1;
rsv1 = copy.rsv2;
rsv2 = copy.rsv2;
rsv3 = copy.rsv3;
opcode = copy.opcode;
masked = copy.masked;
mask = null;
if (copy.mask != null)
{
mask = new byte[copy.mask.length];
System.arraycopy(copy.mask,0,mask,0,mask.length);
}
payloadLength = copy.payloadLength;
payloadStart = copy.payloadStart;
data = copy.data.slice();
continuationIndex = copy.continuationIndex;
continuation = copy.continuation;
}
public void assertValid()
{
if (opcode.isControlFrame())
{
if (getPayloadLength() > WebSocketFrame.MAX_CONTROL_PAYLOAD)
{
throw new ProtocolException("Desired payload length [" + getPayloadLength() + "] exceeds maximum control payload length ["
+ MAX_CONTROL_PAYLOAD + "]");
}
if (fin == false)
{
throw new ProtocolException("Cannot have FIN==false on Control frames");
}
if (rsv1 == true)
{
throw new ProtocolException("Cannot have RSV1==true on Control frames");
}
if (rsv2 == true)
{
throw new ProtocolException("Cannot have RSV2==true on Control frames");
}
if (rsv3 == true)
{
throw new ProtocolException("Cannot have RSV3==true on Control frames");
}
if (isContinuation())
{
throw new ProtocolException("Control frames cannot be Continuations");
}
}
}
/**
* The number of fragments this frame consists of.
* <p>
* For every {@link OpCode#CONTINUATION} opcode encountered, this increments by one.
* <p>
* Note: Not part of the Base Framing Protocol / header information.
*
* @return the number of continuation fragments encountered.
*/
public int getContinuationIndex()
{
return continuationIndex;
}
@Override
public byte[] getMask()
{
if (!masked)
{
throw new IllegalStateException("Frame is not masked");
}
return mask;
}
@Override
public final OpCode getOpCode()
{
return opcode;
}
/**
* Get the payload ByteBuffer. possible null.
* <p>
*
* @return A {@link ByteBuffer#slice()} of the payload buffer (to prevent modification of the buffer state). Possibly null if no payload present.
* <p>
* Note: this method is exposed via the immutable {@link Frame#getPayload()} method.
*/
@Override
public ByteBuffer getPayload()
{
if (data != null)
{
return data;
}
else
{
return null;
}
}
public String getPayloadAsUTF8()
{
if (data == null)
{
return null;
}
return BufferUtil.toUTF8String(data);
}
@Override
public int getPayloadLength()
{
if (data == null)
{
return 0;
}
return payloadLength;
}
public int getPayloadStart()
{
if (data == null)
{
return -1;
}
return payloadStart;
}
public boolean hasPayload()
{
return ((data != null) && (payloadLength > 0));
}
public boolean isContinuation()
{
return continuation;
}
@Override
public boolean isFin()
{
return fin;
}
public boolean isLastFrame()
{
return fin;
}
@Override
public boolean isMasked()
{
return masked;
}
@Override
public boolean isRsv1()
{
return rsv1;
}
@Override
public boolean isRsv2()
{
return rsv2;
}
@Override
public boolean isRsv3()
{
return rsv3;
}
/**
* Get the position currently within the payload data.
* <p>
* Used by flow control, generator and window sizing.
*
* @return the number of bytes remaining in the payload data that has not yet been written out to Network ByteBuffers.
*/
public int position()
{
if (data == null)
{
return -1;
}
return data.position();
}
/**
* Get the number of bytes remaining to write out to the Network ByteBuffer.
* <p>
* Used by flow control, generator and window sizing.
*
* @return the number of bytes remaining in the payload data that has not yet been written out to Network ByteBuffers.
*/
public int remaining()
{
if (data == null)
{
return 0;
}
return data.remaining();
}
public void reset()
{
fin = true;
rsv1 = false;
rsv2 = false;
rsv3 = false;
opcode = null;
masked = false;
data = null;
payloadLength = 0;
mask = null;
continuationIndex = 0;
continuation = false;
}
public WebSocketFrame setContinuation(boolean continuation)
{
this.continuation = continuation;
return this;
}
public WebSocketFrame setContinuationIndex(int continuationIndex)
{
this.continuationIndex = continuationIndex;
return this;
}
public WebSocketFrame setFin(boolean fin)
{
this.fin = fin;
return this;
}
public WebSocketFrame setMask(byte[] maskingKey)
{
this.mask = maskingKey;
this.masked = (mask != null);
return this;
}
public WebSocketFrame setMasked(boolean mask)
{
this.masked = mask;
return this;
}
public WebSocketFrame setOpCode(OpCode opCode)
{
this.opcode = opCode;
return this;
}
/**
* Set the data and payload length.
*
* @param buf
* the bytebuffer to set
*/
public WebSocketFrame setPayload(byte buf[])
{
if (buf == null)
{
data = null;
return this;
}
if (opcode.isControlFrame())
{
if (buf.length > WebSocketFrame.MAX_CONTROL_PAYLOAD)
{
throw new ProtocolException("Control Payloads can not exceed 125 bytes in length.");
}
}
data = BufferUtil.toBuffer(buf);
payloadStart = data.position();
payloadLength = data.limit();
return this;
}
/**
* Set the data and payload length.
*
* @param buf
* the bytebuffer to set
*/
public WebSocketFrame setPayload(byte buf[], int offset, int len)
{
if (buf == null)
{
data = null;
return this;
}
if (opcode.isControlFrame())
{
if (len > WebSocketFrame.MAX_CONTROL_PAYLOAD)
{
throw new ProtocolException("Control Payloads can not exceed 125 bytes in length.");
}
}
data = BufferUtil.toBuffer(buf,offset,len);
payloadStart = data.position();
payloadLength = data.limit();
return this;
}
/**
* Set the data payload.
* <p>
* The provided buffer will be used as is, no copying of bytes performed.
* <p>
* The provided buffer should be flipped and ready to READ from.
*
* @param buf
* the bytebuffer to set
*/
public WebSocketFrame setPayload(ByteBuffer buf)
{
if (buf == null)
{
data = null;
return this;
}
if (opcode.isControlFrame())
{
if (buf.remaining() > WebSocketFrame.MAX_CONTROL_PAYLOAD)
{
throw new ProtocolException("Control Payloads can not exceed 125 bytes in length.");
}
}
data = buf.slice();
payloadStart = data.position();
payloadLength = data.limit();
return this;
}
public WebSocketFrame setPayload(String str)
{
setPayload(BufferUtil.toBuffer(str,StringUtil.__UTF8_CHARSET));
return this;
}
public WebSocketFrame setRsv1(boolean rsv1)
{
this.rsv1 = rsv1;
return this;
}
public WebSocketFrame setRsv2(boolean rsv2)
{
this.rsv2 = rsv2;
return this;
}
public WebSocketFrame setRsv3(boolean rsv3)
{
this.rsv3 = rsv3;
return this;
}
@Override
public String toString()
{
StringBuilder b = new StringBuilder();
if (opcode != null)
{
b.append(opcode.name());
}
else
{
b.append("NO-OP");
}
b.append('[');
b.append("len=").append(payloadLength);
b.append(",fin=").append(fin);
b.append(",masked=").append(masked);
b.append(",continuation=").append(continuation);
b.append(']');
return b.toString();
}
}