package org.java_websocket; 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; import org.java_websocket.drafts.Draft; import org.java_websocket.drafts.Draft.CloseHandshakeType; import org.java_websocket.drafts.Draft.HandshakeState; import org.java_websocket.drafts.Draft_10; import org.java_websocket.drafts.Draft_17; import org.java_websocket.drafts.Draft_75; import org.java_websocket.drafts.Draft_76; import org.java_websocket.exceptions.IncompleteHandshakeException; import org.java_websocket.exceptions.InvalidDataException; import org.java_websocket.exceptions.InvalidHandshakeException; import org.java_websocket.exceptions.WebsocketNotConnectedException; import org.java_websocket.framing.CloseFrame; import org.java_websocket.framing.CloseFrameBuilder; import org.java_websocket.framing.Framedata; import org.java_websocket.framing.Framedata.Opcode; import org.java_websocket.handshake.ClientHandshake; import org.java_websocket.handshake.ClientHandshakeBuilder; import org.java_websocket.handshake.Handshakedata; import org.java_websocket.handshake.ServerHandshake; import org.java_websocket.handshake.ServerHandshakeBuilder; import org.java_websocket.server.WebSocketServer.WebSocketWorker; import org.java_websocket.util.Charsetfunctions; /** * 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 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 * * @param socket * may be unbound */ 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( Charsetfunctions.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 ); Opcode curop = f.getOpcode(); boolean fin = f.isFin(); if( curop == 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 == Opcode.PING ) { wsl.onWebsocketPing( this, f ); continue; } else if( curop == Opcode.PONG ) { wsl.onWebsocketPong( this, f ); continue; } else if( !fin || curop == Opcode.CONTINUOUS ) { if( curop != 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 == Opcode.TEXT ) { try { wsl.onWebsocketMessage( this, Charsetfunctions.stringUtf8( f.getPayloadData() ) ); } catch ( RuntimeException e ) { wsl.onWebsocketError( this, e ); } } else if( curop == 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 ); } /** * Send 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 ) ); } /** * Send 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( 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 ); } // Send 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; } }