package jxm.JWS;
import jxm.CHRF;
import jxm.JWS.EX.IncompleteHandshakeException;
import jxm.JWS.EX.InvalidDataException;
import jxm.JWS.EX.WebsocketNotConnectedException;
import jxm.JWS.FR.CloseFrame;
import jxm.JWS.FR.CloseFrameBuilder;
import jxm.JWS.FR.Framedata;
import jxm.JWS.DF.*;
import jxm.JWS.DF.Draft.CloseHandshakeType;
import jxm.JWS.DF.Draft.HandshakeState;
import jxm.JWS.EX.InvalidHandshakeException;
import jxm.JWS.HS.*;
import jxm.JWS.WebSocketServer.WebSocketWorker;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.ByteChannel;
import java.nio.channels.NotYetConnectedException;
import java.nio.channels.SelectionKey;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
/**
* Represents one end (client or server) of a single WebSocketImpl connection.
* Takes care of the "handshake" phase, then allows for easy sending of
* text frames, and receiving frames through an event-based model.
*/
public class WebSocketImpl implements WebSocket {
public static int RCVBUF = 16384;
public static/*final*/ boolean DEBUG = false; // must be final in the future in order to take advantage of VM optimization
public static final List<Draft> defaultdraftlist = new ArrayList<Draft>(4);
static {
defaultdraftlist.add(new Draft_17());
defaultdraftlist.add(new Draft_10());
defaultdraftlist.add(new Draft_76());
defaultdraftlist.add(new Draft_75());
}
public SelectionKey key;
/**
* the possibly wrapped channel object whose selection is controlled by {@link #key}
*/
public ByteChannel channel;
/**
* Queue of buffers that need to be sent to the client.
*/
public final BlockingQueue<ByteBuffer> outQueue;
/**
* Queue of buffers that need to be processed
*/
public final BlockingQueue<ByteBuffer> inQueue;
/**
* Helper variable meant to store the thread which ( exclusively ) triggers this objects decode method.
*/
public volatile WebSocketWorker workerThread; // TODO reset worker?
/**
* When true no further frames may be submitted to be sent
*/
private volatile boolean flushandclosestate = false;
private READYSTATE readystate = READYSTATE.NOT_YET_CONNECTED;
/**
* The listener to notify of WebSocket events.
*/
private final WebSocketListener wsl;
private List<Draft> knownDrafts;
private Draft draft = null;
private Role role;
private Framedata.Opcode current_continuous_frame_opcode = null;
/**
* the bytes of an incomplete received handshake
*/
private ByteBuffer tmpHandshakeBytes = ByteBuffer.allocate(0);
/**
* stores the handshake sent by this websocket ( Role.CLIENT only )
*/
private ClientHandshake handshakerequest = null;
private String closemessage = null;
private Integer closecode = null;
private Boolean closedremotely = null;
private String resourceDescriptor = null;
/**
* crates a websocket with server role
*/
public WebSocketImpl(WebSocketListener listener, List<Draft> drafts) {
this(listener, (Draft) null);
this.role = Role.SERVER;
// draft.copyInstance will be called when the draft is first needed
if (drafts == null || drafts.isEmpty()) {
knownDrafts = defaultdraftlist;
} else {
knownDrafts = drafts;
}
}
/**
* crates a websocket with client role
*
*/
public WebSocketImpl(WebSocketListener listener, Draft draft) {
if (listener == null || (draft == null && role == Role.SERVER))// socket can be null because we want do be able to create the object without already having a bound channel
throw new IllegalArgumentException("parameters must not be null");
this.outQueue = new LinkedBlockingQueue<ByteBuffer>();
inQueue = new LinkedBlockingQueue<ByteBuffer>();
this.wsl = listener;
this.role = Role.CLIENT;
if (draft != null)
this.draft = draft.copyInstance();
}
@Deprecated
public WebSocketImpl(WebSocketListener listener, Draft draft, Socket socket) {
this(listener, draft);
}
@Deprecated
public WebSocketImpl(WebSocketListener listener, List<Draft> drafts, Socket socket) {
this(listener, drafts);
}
/**
*
*/
public void decode(ByteBuffer socketBuffer) {
assert (socketBuffer.hasRemaining());
if (DEBUG)
System.out.println("process(" + socketBuffer.remaining() + "): {" + (socketBuffer.remaining() > 1000 ? "too big to display" : new String(socketBuffer.array(), socketBuffer.position(), socketBuffer.remaining())) + "}");
if (readystate != READYSTATE.NOT_YET_CONNECTED) {
decodeFrames(socketBuffer);
;
} else {
if (decodeHandshake(socketBuffer)) {
assert (tmpHandshakeBytes.hasRemaining() != socketBuffer.hasRemaining() || !socketBuffer.hasRemaining()); // the buffers will never have remaining bytes at the same time
if (socketBuffer.hasRemaining()) {
decodeFrames(socketBuffer);
} else if (tmpHandshakeBytes.hasRemaining()) {
decodeFrames(tmpHandshakeBytes);
}
}
}
assert (isClosing() || isFlushAndClose() || !socketBuffer.hasRemaining());
}
/**
* Returns whether the handshake phase has is completed.
* In case of a broken handshake this will be never the case.
*/
private boolean decodeHandshake(ByteBuffer socketBufferNew) {
ByteBuffer socketBuffer;
if (tmpHandshakeBytes.capacity() == 0) {
socketBuffer = socketBufferNew;
} else {
if (tmpHandshakeBytes.remaining() < socketBufferNew.remaining()) {
ByteBuffer buf = ByteBuffer.allocate(tmpHandshakeBytes.capacity() + socketBufferNew.remaining());
tmpHandshakeBytes.flip();
buf.put(tmpHandshakeBytes);
tmpHandshakeBytes = buf;
}
tmpHandshakeBytes.put(socketBufferNew);
tmpHandshakeBytes.flip();
socketBuffer = tmpHandshakeBytes;
}
socketBuffer.mark();
try {
if (draft == null) {
HandshakeState isflashedgecase = isFlashEdgeCase(socketBuffer);
if (isflashedgecase == HandshakeState.MATCHED) {
try {
write(ByteBuffer.wrap(CHRF.utf8Bytes(wsl.getFlashPolicy(this))));
close(CloseFrame.FLASHPOLICY, "");
} catch (InvalidDataException e) {
close(CloseFrame.ABNORMAL_CLOSE, "remote peer closed connection before flashpolicy could be transmitted", true);
}
return false;
}
}
HandshakeState handshakestate = null;
try {
if (role == Role.SERVER) {
if (draft == null) {
for (Draft d : knownDrafts) {
d = d.copyInstance();
try {
d.setParseMode(role);
socketBuffer.reset();
Handshakedata tmphandshake = d.translateHandshake(socketBuffer);
if (tmphandshake instanceof ClientHandshake == false) {
flushAndClose(CloseFrame.PROTOCOL_ERROR, "wrong http function", false);
return false;
}
ClientHandshake handshake = (ClientHandshake) tmphandshake;
handshakestate = d.acceptHandshakeAsServer(handshake);
if (handshakestate == HandshakeState.MATCHED) {
resourceDescriptor = handshake.getResourceDescriptor();
ServerHandshakeBuilder response;
try {
response = wsl.onWebsocketHandshakeReceivedAsServer(this, d, handshake);
} catch (InvalidDataException e) {
flushAndClose(e.getCloseCode(), e.getMessage(), false);
return false;
} catch (RuntimeException e) {
wsl.onWebsocketError(this, e);
flushAndClose(CloseFrame.NEVER_CONNECTED, e.getMessage(), false);
return false;
}
write(d.createHandshake(d.postProcessHandshakeResponseAsServer(handshake, response), role));
draft = d;
open(handshake);
return true;
}
} catch (InvalidHandshakeException e) {
// go on with an other draft
}
}
if (draft == null) {
close(CloseFrame.PROTOCOL_ERROR, "no draft matches");
}
return false;
} else {
// special case for multiple step handshakes
Handshakedata tmphandshake = draft.translateHandshake(socketBuffer);
if (tmphandshake instanceof ClientHandshake == false) {
flushAndClose(CloseFrame.PROTOCOL_ERROR, "wrong http function", false);
return false;
}
ClientHandshake handshake = (ClientHandshake) tmphandshake;
handshakestate = draft.acceptHandshakeAsServer(handshake);
if (handshakestate == HandshakeState.MATCHED) {
open(handshake);
return true;
} else {
close(CloseFrame.PROTOCOL_ERROR, "the handshake did finaly not match");
}
return false;
}
} else if (role == Role.CLIENT) {
draft.setParseMode(role);
Handshakedata tmphandshake = draft.translateHandshake(socketBuffer);
if (tmphandshake instanceof ServerHandshake == false) {
flushAndClose(CloseFrame.PROTOCOL_ERROR, "wrong http function", false);
return false;
}
ServerHandshake handshake = (ServerHandshake) tmphandshake;
handshakestate = draft.acceptHandshakeAsClient(handshakerequest, handshake);
if (handshakestate == HandshakeState.MATCHED) {
try {
wsl.onWebsocketHandshakeReceivedAsClient(this, handshakerequest, handshake);
} catch (InvalidDataException e) {
flushAndClose(e.getCloseCode(), e.getMessage(), false);
return false;
} catch (RuntimeException e) {
wsl.onWebsocketError(this, e);
flushAndClose(CloseFrame.NEVER_CONNECTED, e.getMessage(), false);
return false;
}
open(handshake);
return true;
} else {
close(CloseFrame.PROTOCOL_ERROR, "draft " + draft + " refuses handshake");
}
}
} catch (InvalidHandshakeException e) {
close(e);
}
} catch (IncompleteHandshakeException e) {
if (tmpHandshakeBytes.capacity() == 0) {
socketBuffer.reset();
int newsize = e.getPreferedSize();
if (newsize == 0) {
newsize = socketBuffer.capacity() + 16;
} else {
assert (e.getPreferedSize() >= socketBuffer.remaining());
}
tmpHandshakeBytes = ByteBuffer.allocate(newsize);
tmpHandshakeBytes.put(socketBufferNew);
// tmpHandshakeBytes.flip();
} else {
tmpHandshakeBytes.position(tmpHandshakeBytes.limit());
tmpHandshakeBytes.limit(tmpHandshakeBytes.capacity());
}
}
return false;
}
private void decodeFrames(ByteBuffer socketBuffer) {
List<Framedata> frames;
try {
frames = draft.translateFrame(socketBuffer);
for (Framedata f : frames) {
if (DEBUG)
System.out.println("matched frame: " + f);
Framedata.Opcode curop = f.getOpcode();
boolean fin = f.isFin();
if (curop == Framedata.Opcode.CLOSING) {
int code = CloseFrame.NOCODE;
String reason = "";
if (f instanceof CloseFrame) {
CloseFrame cf = (CloseFrame) f;
code = cf.getCloseCode();
reason = cf.getMessage();
}
if (readystate == READYSTATE.CLOSING) {
// complete the close handshake by disconnecting
closeConnection(code, reason, true);
} else {
// echo close handshake
if (draft.getCloseHandshakeType() == CloseHandshakeType.TWOWAY)
close(code, reason, true);
else
flushAndClose(code, reason, false);
}
continue;
} else if (curop == Framedata.Opcode.PING) {
wsl.onWebsocketPing(this, f);
continue;
} else if (curop == Framedata.Opcode.PONG) {
wsl.onWebsocketPong(this, f);
continue;
} else if (!fin || curop == Framedata.Opcode.CONTINUOUS) {
if (curop != Framedata.Opcode.CONTINUOUS) {
if (current_continuous_frame_opcode != null)
throw new InvalidDataException(CloseFrame.PROTOCOL_ERROR, "Previous continuous frame sequence not completed.");
current_continuous_frame_opcode = curop;
} else if (fin) {
if (current_continuous_frame_opcode == null)
throw new InvalidDataException(CloseFrame.PROTOCOL_ERROR, "Continuous frame sequence was not started.");
current_continuous_frame_opcode = null;
} else if (current_continuous_frame_opcode == null) {
throw new InvalidDataException(CloseFrame.PROTOCOL_ERROR, "Continuous frame sequence was not started.");
}
try {
wsl.onWebsocketMessageFragment(this, f);
} catch (RuntimeException e) {
wsl.onWebsocketError(this, e);
}
} else if (current_continuous_frame_opcode != null) {
throw new InvalidDataException(CloseFrame.PROTOCOL_ERROR, "Continuous frame sequence not completed.");
} else if (curop == Framedata.Opcode.TEXT) {
try {
wsl.onWebsocketMessage(this, CHRF.stringUtf8(f.getPayloadData()));
} catch (RuntimeException e) {
wsl.onWebsocketError(this, e);
}
} else if (curop == Framedata.Opcode.BINARY) {
try {
wsl.onWebsocketMessage(this, f.getPayloadData());
} catch (RuntimeException e) {
wsl.onWebsocketError(this, e);
}
} else {
throw new InvalidDataException(CloseFrame.PROTOCOL_ERROR, "non control or continious frame expected");
}
}
} catch (InvalidDataException e1) {
wsl.onWebsocketError(this, e1);
close(e1);
return;
}
}
private void close(int code, String message, boolean remote) {
if (readystate != READYSTATE.CLOSING && readystate != READYSTATE.CLOSED) {
if (readystate == READYSTATE.OPEN) {
if (code == CloseFrame.ABNORMAL_CLOSE) {
assert (remote == false);
readystate = READYSTATE.CLOSING;
flushAndClose(code, message, false);
return;
}
if (draft.getCloseHandshakeType() != CloseHandshakeType.NONE) {
try {
if (!remote) {
try {
wsl.onWebsocketCloseInitiated(this, code, message);
} catch (RuntimeException e) {
wsl.onWebsocketError(this, e);
}
}
sendFrame(new CloseFrameBuilder(code, message));
} catch (InvalidDataException e) {
wsl.onWebsocketError(this, e);
flushAndClose(CloseFrame.ABNORMAL_CLOSE, "generated frame is invalid", false);
}
}
flushAndClose(code, message, remote);
} else if (code == CloseFrame.FLASHPOLICY) {
assert (remote);
flushAndClose(CloseFrame.FLASHPOLICY, message, true);
} else {
flushAndClose(CloseFrame.NEVER_CONNECTED, message, false);
}
if (code == CloseFrame.PROTOCOL_ERROR)// this endpoint found a PROTOCOL_ERROR
flushAndClose(code, message, remote);
readystate = READYSTATE.CLOSING;
tmpHandshakeBytes = null;
return;
}
}
@Override
public void close(int code, String message) {
close(code, message, false);
}
/**
* @param remote Indicates who "generated" <code>code</code>.<br>
* <code>true</code> means that this endpoint received the <code>code</code> from the other endpoint.<br>
* false means this endpoint decided to send the given code,<br>
* <code>remote</code> may also be true if this endpoint started the closing handshake since the other endpoint may not simply echo the <code>code</code> but close the connection the same time this endpoint does do but with an other <code>code</code>. <br>
*/
protected synchronized void closeConnection(int code, String message, boolean remote) {
if (readystate == READYSTATE.CLOSED) {
return;
}
if (key != null) {
// key.attach( null ); //see issue #114
key.cancel();
}
if (channel != null) {
try {
channel.close();
} catch (IOException e) {
wsl.onWebsocketError(this, e);
}
}
try {
this.wsl.onWebsocketClose(this, code, message, remote);
} catch (RuntimeException e) {
wsl.onWebsocketError(this, e);
}
if (draft != null)
draft.reset();
handshakerequest = null;
readystate = READYSTATE.CLOSED;
this.outQueue.clear();
}
protected void closeConnection(int code, boolean remote) {
closeConnection(code, "", remote);
}
public void closeConnection() {
if (closedremotely == null) {
throw new IllegalStateException("this method must be used in conjuction with flushAndClose");
}
closeConnection(closecode, closemessage, closedremotely);
}
public void closeConnection(int code, String message) {
closeConnection(code, message, false);
}
protected synchronized void flushAndClose(int code, String message, boolean remote) {
if (flushandclosestate) {
return;
}
closecode = code;
closemessage = message;
closedremotely = remote;
flushandclosestate = true;
wsl.onWriteDemand(this); // ensures that all outgoing frames are flushed before closing the connection
try {
wsl.onWebsocketClosing(this, code, message, remote);
} catch (RuntimeException e) {
wsl.onWebsocketError(this, e);
}
if (draft != null)
draft.reset();
handshakerequest = null;
}
public void eot() {
if (getReadyState() == READYSTATE.NOT_YET_CONNECTED) {
closeConnection(CloseFrame.NEVER_CONNECTED, true);
} else if (flushandclosestate) {
closeConnection(closecode, closemessage, closedremotely);
} else if (draft.getCloseHandshakeType() == CloseHandshakeType.NONE) {
closeConnection(CloseFrame.NORMAL, true);
} else if (draft.getCloseHandshakeType() == CloseHandshakeType.ONEWAY) {
if (role == Role.SERVER)
closeConnection(CloseFrame.ABNORMAL_CLOSE, true);
else
closeConnection(CloseFrame.NORMAL, true);
} else {
closeConnection(CloseFrame.ABNORMAL_CLOSE, true);
}
}
@Override
public void close(int code) {
close(code, "", false);
}
public void close(InvalidDataException e) {
close(e.getCloseCode(), e.getMessage(), false);
}
/**
* Call Text data to the other end.
*
* @throws IllegalArgumentException
* @throws NotYetConnectedException
*/
@Override
public void send(String text) throws WebsocketNotConnectedException {
if (text == null)
throw new IllegalArgumentException("Cannot send 'null' data to a WebSocketImpl.");
send(draft.createFrames(text, role == Role.CLIENT));
}
/**
* Call Binary data (plain bytes) to the other end.
*
* @throws IllegalArgumentException
* @throws NotYetConnectedException
*/
@Override
public void send(ByteBuffer bytes) throws IllegalArgumentException, WebsocketNotConnectedException {
if (bytes == null)
throw new IllegalArgumentException("Cannot send 'null' data to a WebSocketImpl.");
send(draft.createFrames(bytes, role == Role.CLIENT));
}
@Override
public void send(byte[] bytes) throws IllegalArgumentException, WebsocketNotConnectedException {
send(ByteBuffer.wrap(bytes));
}
private void send(Collection<Framedata> frames) {
if (!isOpen())
throw new WebsocketNotConnectedException();
for (Framedata f : frames) {
sendFrame(f);
}
}
@Override
public void sendFragmentedFrame(Framedata.Opcode op, ByteBuffer buffer, boolean fin) {
send(draft.continuousFrame(op, buffer, fin));
}
@Override
public void sendFrame(Framedata framedata) {
if (DEBUG)
System.out.println("send frame: " + framedata);
write(draft.createBinaryFrame(framedata));
}
@Override
public boolean hasBufferedData() {
return !this.outQueue.isEmpty();
}
private HandshakeState isFlashEdgeCase(ByteBuffer request) throws IncompleteHandshakeException {
request.mark();
if (request.limit() > Draft.FLASH_POLICY_REQUEST.length) {
return HandshakeState.NOT_MATCHED;
} else if (request.limit() < Draft.FLASH_POLICY_REQUEST.length) {
throw new IncompleteHandshakeException(Draft.FLASH_POLICY_REQUEST.length);
} else {
for (int flash_policy_index = 0; request.hasRemaining(); flash_policy_index++) {
if (Draft.FLASH_POLICY_REQUEST[flash_policy_index] != request.get()) {
request.reset();
return HandshakeState.NOT_MATCHED;
}
}
return HandshakeState.MATCHED;
}
}
public void startHandshake(ClientHandshakeBuilder handshakedata) throws InvalidHandshakeException {
assert (readystate != READYSTATE.CONNECTING) : "shall only be called once";
// Store the Handshake Request we are about to send
this.handshakerequest = draft.postProcessHandshakeRequestAsClient(handshakedata);
resourceDescriptor = handshakedata.getResourceDescriptor();
assert (resourceDescriptor != null);
// Notify Listener
try {
wsl.onWebsocketHandshakeSentAsClient(this, this.handshakerequest);
} catch (InvalidDataException e) {
// Stop if the client code throws an exception
throw new InvalidHandshakeException("Handshake data rejected by client.");
} catch (RuntimeException e) {
wsl.onWebsocketError(this, e);
throw new InvalidHandshakeException("rejected because of" + e);
}
// Call
write(draft.createHandshake(this.handshakerequest, role));
}
private void write(ByteBuffer buf) {
if (DEBUG)
System.out.println("write(" + buf.remaining() + "): {" + (buf.remaining() > 1000 ? "too big to display" : new String(buf.array())) + "}");
outQueue.add(buf);
/*try {
outQueue.put( buf );
} catch ( InterruptedException e ) {
write( buf );
Thread.currentThread().interrupt(); // keep the interrupted status
e.printStackTrace();
}*/
wsl.onWriteDemand(this);
}
private void write(List<ByteBuffer> bufs) {
for (ByteBuffer b : bufs) {
write(b);
}
}
private void open(Handshakedata d) {
if (DEBUG)
System.out.println("open using draft: " + draft.getClass().getSimpleName());
readystate = READYSTATE.OPEN;
try {
wsl.onWebsocketOpen(this, d);
} catch (RuntimeException e) {
wsl.onWebsocketError(this, e);
}
}
@Override
public boolean isConnecting() {
assert (flushandclosestate ? readystate == READYSTATE.CONNECTING : true);
return readystate == READYSTATE.CONNECTING; // ifflushandclosestate
}
@Override
public boolean isOpen() {
assert (readystate == READYSTATE.OPEN ? !flushandclosestate : true);
return readystate == READYSTATE.OPEN;
}
@Override
public boolean isClosing() {
return readystate == READYSTATE.CLOSING;
}
@Override
public boolean isFlushAndClose() {
return flushandclosestate;
}
@Override
public boolean isClosed() {
return readystate == READYSTATE.CLOSED;
}
@Override
public READYSTATE getReadyState() {
return readystate;
}
@Override
public int hashCode() {
return super.hashCode();
}
@Override
public String toString() {
return super.toString(); // its nice to be able to set breakpoints here
}
@Override
public InetSocketAddress getRemoteSocketAddress() {
return wsl.getRemoteSocketAddress(this);
}
@Override
public InetSocketAddress getLocalSocketAddress() {
return wsl.getLocalSocketAddress(this);
}
@Override
public Draft getDraft() {
return draft;
}
@Override
public void close() {
close(CloseFrame.NORMAL);
}
@Override
public String getResourceDescriptor() {
return resourceDescriptor;
}
}