blob: 74d50d1d850bb131268d09855521cb4ca82995f0 [file] [log] [blame]
//
// ========================================================================
// Copyright (c) 1995-2015 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.common;
import java.nio.ByteBuffer;
import java.util.List;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.websocket.api.MessageTooLargeException;
import org.eclipse.jetty.websocket.api.ProtocolException;
import org.eclipse.jetty.websocket.api.WebSocketBehavior;
import org.eclipse.jetty.websocket.api.WebSocketException;
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.api.extensions.Extension;
import org.eclipse.jetty.websocket.api.extensions.Frame;
import org.eclipse.jetty.websocket.api.extensions.IncomingFrames;
import org.eclipse.jetty.websocket.common.frames.BinaryFrame;
import org.eclipse.jetty.websocket.common.frames.CloseFrame;
import org.eclipse.jetty.websocket.common.frames.ContinuationFrame;
import org.eclipse.jetty.websocket.common.frames.ControlFrame;
import org.eclipse.jetty.websocket.common.frames.PingFrame;
import org.eclipse.jetty.websocket.common.frames.PongFrame;
import org.eclipse.jetty.websocket.common.frames.TextFrame;
import org.eclipse.jetty.websocket.common.io.payload.DeMaskProcessor;
import org.eclipse.jetty.websocket.common.io.payload.PayloadProcessor;
/**
* Parsing of a frames in WebSocket land.
*/
public class Parser
{
private enum State
{
START,
PAYLOAD_LEN,
PAYLOAD_LEN_BYTES,
MASK,
MASK_BYTES,
PAYLOAD
}
private static final Logger LOG = Log.getLogger(Parser.class);
private final WebSocketPolicy policy;
private final ByteBufferPool bufferPool;
// State specific
private State state = State.START;
private int cursor = 0;
// Frame
private WebSocketFrame frame;
private boolean priorDataFrame;
// payload specific
private ByteBuffer payload;
private int payloadLength;
private PayloadProcessor maskProcessor = new DeMaskProcessor();
// private PayloadProcessor strictnessProcessor;
/**
* Is there an extension using RSV flag?
* <p>
*
* <pre>
* 0100_0000 (0x40) = rsv1
* 0010_0000 (0x20) = rsv2
* 0001_0000 (0x10) = rsv3
* </pre>
*/
private byte flagsInUse=0x00;
private IncomingFrames incomingFramesHandler;
public Parser(WebSocketPolicy wspolicy, ByteBufferPool bufferPool)
{
this.bufferPool = bufferPool;
this.policy = wspolicy;
}
private void assertSanePayloadLength(long len)
{
if (LOG.isDebugEnabled()) {
LOG.debug("{} Payload Length: {} - {}",policy.getBehavior(),len,this);
}
// Since we use ByteBuffer so often, having lengths over Integer.MAX_VALUE is really impossible.
if (len > Integer.MAX_VALUE)
{
// OMG! Sanity Check! DO NOT WANT! Won't anyone think of the memory!
throw new MessageTooLargeException("[int-sane!] cannot handle payload lengths larger than " + Integer.MAX_VALUE);
}
switch (frame.getOpCode())
{
case OpCode.CLOSE:
if (len == 1)
{
throw new ProtocolException("Invalid close frame payload length, [" + payloadLength + "]");
}
// fall thru
case OpCode.PING:
case OpCode.PONG:
if (len > ControlFrame.MAX_CONTROL_PAYLOAD)
{
throw new ProtocolException("Invalid control frame payload length, [" + payloadLength + "] cannot exceed ["
+ ControlFrame.MAX_CONTROL_PAYLOAD + "]");
}
break;
case OpCode.TEXT:
policy.assertValidTextMessageSize((int)len);
break;
case OpCode.BINARY:
policy.assertValidBinaryMessageSize((int)len);
break;
}
}
public void configureFromExtensions(List<? extends Extension> exts)
{
// default
flagsInUse = 0x00;
// configure from list of extensions in use
for (Extension ext : exts)
{
if (ext.isRsv1User())
{
flagsInUse = (byte)(flagsInUse | 0x40);
}
if (ext.isRsv2User())
{
flagsInUse = (byte)(flagsInUse | 0x20);
}
if (ext.isRsv3User())
{
flagsInUse = (byte)(flagsInUse | 0x10);
}
}
}
public IncomingFrames getIncomingFramesHandler()
{
return incomingFramesHandler;
}
public WebSocketPolicy getPolicy()
{
return policy;
}
public boolean isRsv1InUse()
{
return (flagsInUse & 0x40) != 0;
}
public boolean isRsv2InUse()
{
return (flagsInUse & 0x20) != 0;
}
public boolean isRsv3InUse()
{
return (flagsInUse & 0x10) != 0;
}
protected void notifyFrame(final Frame f)
{
if (LOG.isDebugEnabled())
LOG.debug("{} Notify {}",policy.getBehavior(),getIncomingFramesHandler());
if (policy.getBehavior() == WebSocketBehavior.SERVER)
{
/* Parsing on server.
*
* Then you MUST make sure all incoming frames are masked!
*
* Technically, this test is in violation of RFC-6455, Section 5.1
* http://tools.ietf.org/html/rfc6455#section-5.1
*
* But we can't trust the client at this point, so Jetty opts to close
* the connection as a Protocol error.
*/
if (!f.isMasked())
{
throw new ProtocolException("Client MUST mask all frames (RFC-6455: Section 5.1)");
}
}
else if(policy.getBehavior() == WebSocketBehavior.CLIENT)
{
// Required by RFC-6455 / Section 5.1
if (f.isMasked())
{
throw new ProtocolException("Server MUST NOT mask any frames (RFC-6455: Section 5.1)");
}
}
if (incomingFramesHandler == null)
{
return;
}
try
{
incomingFramesHandler.incomingFrame(f);
}
catch (WebSocketException e)
{
notifyWebSocketException(e);
}
catch (Throwable t)
{
LOG.warn(t);
notifyWebSocketException(new WebSocketException(t));
}
}
protected void notifyWebSocketException(WebSocketException e)
{
LOG.warn(e);
if (incomingFramesHandler == null)
{
return;
}
incomingFramesHandler.incomingError(e);
}
public void parse(ByteBuffer buffer) throws WebSocketException
{
if (buffer.remaining() <= 0)
{
return;
}
try
{
// parse through all the frames in the buffer
while (parseFrame(buffer))
{
if (LOG.isDebugEnabled())
LOG.debug("{} Parsed Frame: {}",policy.getBehavior(),frame);
notifyFrame(frame);
if (frame.isDataFrame())
{
priorDataFrame = !frame.isFin();
}
reset();
}
}
catch (WebSocketException e)
{
buffer.position(buffer.limit()); // consume remaining
reset();
// let session know
notifyWebSocketException(e);
// need to throw for proper close behavior in connection
throw e;
}
catch (Throwable t)
{
buffer.position(buffer.limit()); // consume remaining
reset();
// let session know
WebSocketException e = new WebSocketException(t);
notifyWebSocketException(e);
// need to throw for proper close behavior in connection
throw e;
}
}
private void reset()
{
if (frame != null)
frame.reset();
frame = null;
bufferPool.release(payload);
payload = null;
}
/**
* Parse the base framing protocol buffer.
* <p>
* Note the first byte (fin,rsv1,rsv2,rsv3,opcode) are parsed by the {@link Parser#parse(ByteBuffer)} method
* <p>
* Not overridable
*
* @param buffer
* the buffer to parse from.
* @return true if done parsing base framing protocol and ready for parsing of the payload. false if incomplete parsing of base framing protocol.
*/
private boolean parseFrame(ByteBuffer buffer)
{
if (LOG.isDebugEnabled())
{
LOG.debug("{} Parsing {} bytes",policy.getBehavior(),buffer.remaining());
}
while (buffer.hasRemaining())
{
switch (state)
{
case START:
{
// peek at byte
byte b = buffer.get();
boolean fin = ((b & 0x80) != 0);
byte opcode = (byte)(b & 0x0F);
if (!OpCode.isKnown(opcode))
{
throw new ProtocolException("Unknown opcode: " + opcode);
}
if (LOG.isDebugEnabled())
LOG.debug("{} OpCode {}, fin={} rsv={}{}{}",
policy.getBehavior(),
OpCode.name(opcode),
fin,
(((b & 0x40) != 0)?'1':'.'),
(((b & 0x20) != 0)?'1':'.'),
(((b & 0x10) != 0)?'1':'.'));
// base framing flags
switch(opcode)
{
case OpCode.TEXT:
frame = new TextFrame();
// data validation
if (priorDataFrame)
{
throw new ProtocolException("Unexpected " + OpCode.name(opcode) + " frame, was expecting CONTINUATION");
}
break;
case OpCode.BINARY:
frame = new BinaryFrame();
// data validation
if (priorDataFrame)
{
throw new ProtocolException("Unexpected " + OpCode.name(opcode) + " frame, was expecting CONTINUATION");
}
break;
case OpCode.CONTINUATION:
frame = new ContinuationFrame();
// continuation validation
if (!priorDataFrame)
{
throw new ProtocolException("CONTINUATION frame without prior !FIN");
}
// Be careful to use the original opcode
break;
case OpCode.CLOSE:
frame = new CloseFrame();
// control frame validation
if (!fin)
{
throw new ProtocolException("Fragmented Close Frame [" + OpCode.name(opcode) + "]");
}
break;
case OpCode.PING:
frame = new PingFrame();
// control frame validation
if (!fin)
{
throw new ProtocolException("Fragmented Ping Frame [" + OpCode.name(opcode) + "]");
}
break;
case OpCode.PONG:
frame = new PongFrame();
// control frame validation
if (!fin)
{
throw new ProtocolException("Fragmented Pong Frame [" + OpCode.name(opcode) + "]");
}
break;
}
frame.setFin(fin);
// Are any flags set?
if ((b & 0x70) != 0)
{
/*
* RFC 6455 Section 5.2
*
* MUST be 0 unless an extension is negotiated that defines meanings for non-zero values. If a nonzero value is received and none of the
* negotiated extensions defines the meaning of such a nonzero value, the receiving endpoint MUST _Fail the WebSocket Connection_.
*/
if ((b & 0x40) != 0)
{
if (isRsv1InUse())
frame.setRsv1(true);
else
throw new ProtocolException("RSV1 not allowed to be set");
}
if ((b & 0x20) != 0)
{
if (isRsv2InUse())
frame.setRsv2(true);
else
throw new ProtocolException("RSV2 not allowed to be set");
}
if ((b & 0x10) != 0)
{
if (isRsv3InUse())
frame.setRsv3(true);
else
throw new ProtocolException("RSV3 not allowed to be set");
}
}
state = State.PAYLOAD_LEN;
break;
}
case PAYLOAD_LEN:
{
byte b = buffer.get();
frame.setMasked((b & 0x80) != 0);
payloadLength = (byte)(0x7F & b);
if (payloadLength == 127) // 0x7F
{
// length 8 bytes (extended payload length)
payloadLength = 0;
state = State.PAYLOAD_LEN_BYTES;
cursor = 8;
break; // continue onto next state
}
else if (payloadLength == 126) // 0x7E
{
// length 2 bytes (extended payload length)
payloadLength = 0;
state = State.PAYLOAD_LEN_BYTES;
cursor = 2;
break; // continue onto next state
}
assertSanePayloadLength(payloadLength);
if (frame.isMasked())
{
state = State.MASK;
}
else
{
// special case for empty payloads (no more bytes left in buffer)
if (payloadLength == 0)
{
state = State.START;
return true;
}
maskProcessor.reset(frame);
state = State.PAYLOAD;
}
break;
}
case PAYLOAD_LEN_BYTES:
{
byte b = buffer.get();
--cursor;
payloadLength |= (b & 0xFF) << (8 * cursor);
if (cursor == 0)
{
assertSanePayloadLength(payloadLength);
if (frame.isMasked())
{
state = State.MASK;
}
else
{
// special case for empty payloads (no more bytes left in buffer)
if (payloadLength == 0)
{
state = State.START;
return true;
}
maskProcessor.reset(frame);
state = State.PAYLOAD;
}
}
break;
}
case MASK:
{
byte m[] = new byte[4];
frame.setMask(m);
if (buffer.remaining() >= 4)
{
buffer.get(m,0,4);
// special case for empty payloads (no more bytes left in buffer)
if (payloadLength == 0)
{
state = State.START;
return true;
}
maskProcessor.reset(frame);
state = State.PAYLOAD;
}
else
{
state = State.MASK_BYTES;
cursor = 4;
}
break;
}
case MASK_BYTES:
{
byte b = buffer.get();
frame.getMask()[4 - cursor] = b;
--cursor;
if (cursor == 0)
{
// special case for empty payloads (no more bytes left in buffer)
if (payloadLength == 0)
{
state = State.START;
return true;
}
maskProcessor.reset(frame);
state = State.PAYLOAD;
}
break;
}
case PAYLOAD:
{
frame.assertValid();
if (parsePayload(buffer))
{
// special check for close
if (frame.getOpCode() == OpCode.CLOSE)
{
// TODO: yuck. Don't create an object to do validation checks!
new CloseInfo(frame);
}
state = State.START;
// we have a frame!
return true;
}
break;
}
}
}
return false;
}
/**
* Implementation specific parsing of a payload
*
* @param buffer
* the payload buffer
* @return true if payload is done reading, false if incomplete
*/
private boolean parsePayload(ByteBuffer buffer)
{
if (payloadLength == 0)
{
return true;
}
if (buffer.hasRemaining())
{
// Create a small window of the incoming buffer to work with.
// this should only show the payload itself, and not any more
// bytes that could belong to the start of the next frame.
int bytesSoFar = payload == null ? 0 : payload.position();
int bytesExpected = payloadLength - bytesSoFar;
int bytesAvailable = buffer.remaining();
int windowBytes = Math.min(bytesAvailable, bytesExpected);
int limit = buffer.limit();
buffer.limit(buffer.position() + windowBytes);
ByteBuffer window = buffer.slice();
buffer.limit(limit);
buffer.position(buffer.position() + window.remaining());
if (LOG.isDebugEnabled()) {
LOG.debug("{} Window: {}",policy.getBehavior(),BufferUtil.toDetailString(window));
}
maskProcessor.process(window);
if (window.remaining() == payloadLength)
{
// We have the whole content, no need to copy.
frame.setPayload(window);
return true;
}
else
{
if (payload == null)
{
payload = bufferPool.acquire(payloadLength,false);
BufferUtil.clearToFill(payload);
}
// Copy the payload.
payload.put(window);
if (payload.position() == payloadLength)
{
BufferUtil.flipToFlush(payload, 0);
frame.setPayload(payload);
return true;
}
}
}
return false;
}
public void setIncomingFramesHandler(IncomingFrames incoming)
{
this.incomingFramesHandler = incoming;
}
@Override
public String toString()
{
StringBuilder builder = new StringBuilder();
builder.append("Parser@").append(Integer.toHexString(hashCode()));
builder.append("[");
if (incomingFramesHandler == null)
{
builder.append("NO_HANDLER");
}
else
{
builder.append(incomingFramesHandler.getClass().getSimpleName());
}
builder.append(",s=").append(state);
builder.append(",c=").append(cursor);
builder.append(",len=").append(payloadLength);
builder.append(",f=").append(frame);
// builder.append(",p=").append(policy);
builder.append("]");
return builder.toString();
}
}