// --------------------------------------------------------------------------- // jWebSocket - TCP Connector // Copyright (c) 2010 Alexander Schulze, Innotrade GmbH // --------------------------------------------------------------------------- // This program is free software; you can redistribute it and/or modify it // under the terms of the GNU Lesser General Public License as published by the // Free Software Foundation; either version 3 of the License, or (at your // option) any later version. // This program is distributed in the hope that it will be useful, but WITHOUT // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or // FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for // more details. // You should have received a copy of the GNU Lesser General Public License along // with this program; if not, see <http://www.gnu.org/licenses/lgpl.html>. // --------------------------------------------------------------------------- package org.jwebsocket.tcp; import java.io.*; import java.net.InetAddress; import java.net.Socket; import java.net.SocketTimeoutException; import org.apache.log4j.Logger; import org.jwebsocket.api.WebSocketConnector; import org.jwebsocket.api.WebSocketEngine; import org.jwebsocket.api.WebSocketPacket; import org.jwebsocket.async.IOFuture; import org.jwebsocket.config.JWebSocketCommonConstants; import org.jwebsocket.connectors.BaseConnector; import org.jwebsocket.kit.CloseReason; import org.jwebsocket.kit.RawPacket; import org.jwebsocket.kit.WebSocketProtocolHandler; import org.jwebsocket.logging.Logging; /** * Implementation of the jWebSocket TCP socket connector. * * @author aschulze * @author jang */ public class TCPConnector extends BaseConnector { private static Logger mLog = Logging.getLogger(TCPConnector.class); private InputStream mIn = null; private OutputStream mOut = null; private Socket mClientSocket = null; private boolean mIsRunning = false; private CloseReason mCloseReason = CloseReason.TIMEOUT; /** * creates a new TCP connector for the passed engine using the passed client * socket. Usually connectors are instantiated by their engine only, not by * the application. * * @param aEngine * @param aClientSocket */ public TCPConnector(WebSocketEngine aEngine, Socket aClientSocket) { super(aEngine); mClientSocket = aClientSocket; try { mIn = mClientSocket.getInputStream(); mOut = new PrintStream(mClientSocket.getOutputStream(), true, "UTF-8"); } catch (Exception lEx) { mLog.error(lEx.getClass().getSimpleName() + " instantiating " + getClass().getSimpleName() + ": " + lEx.getMessage()); } } @Override public void startConnector() { int lPort = -1; int lTimeout = -1; try { lPort = mClientSocket.getPort(); lTimeout = mClientSocket.getSoTimeout(); } catch (Exception lEx) { } if (mLog.isDebugEnabled()) { mLog.debug("Starting TCP connector on port " + lPort + " with timeout " + (lTimeout > 0 ? lTimeout + "ms" : "infinite") + ""); } ClientProcessor lClientProc = new ClientProcessor(this); Thread lClientThread = new Thread(lClientProc); lClientThread.start(); if (mLog.isInfoEnabled()) { mLog.info("Started TCP connector on port " + lPort + " with timeout " + (lTimeout > 0 ? lTimeout + "ms" : "infinite") + ""); } } @Override public void stopConnector(CloseReason aCloseReason) { if (mLog.isDebugEnabled()) { mLog.debug("Stopping TCP connector (" + aCloseReason.name() + ")..."); } int lPort = mClientSocket.getPort(); mCloseReason = aCloseReason; mIsRunning = false; if (!isHixieDraft()) { // Hybi specs demand that client must be notified with CLOSE control message before disconnect WebSocketPacket lClose = new RawPacket("BYE"); lClose.setFrameType(RawPacket.FRAMETYPE_CLOSE); sendPacket(lClose); } try { mIn.close(); if (mLog.isInfoEnabled()) { mLog.info("Stopped TCP connector (" + aCloseReason.name() + ") on port " + lPort + "."); } } catch (IOException lEx) { if (mLog.isDebugEnabled()) { mLog.info(lEx.getClass().getSimpleName() + " while stopping TCP connector (" + aCloseReason.name() + ") on port " + lPort + ": " + lEx.getMessage()); } } } @Override public void processPacket(WebSocketPacket aDataPacket) { // forward the data packet to the engine getEngine().processPacket(this, aDataPacket); } @Override public synchronized void sendPacket(WebSocketPacket aDataPacket) { try { if (isHixieDraft()) { sendHixie(aDataPacket); } else { sendHybi(aDataPacket); } mOut.flush(); } catch (IOException lEx) { mLog.error(lEx.getClass().getSimpleName() + " sending data packet: " + lEx.getMessage()); } } @Override public IOFuture sendPacketAsync(WebSocketPacket aDataPacket) { throw new UnsupportedOperationException("Underlying connector:" + getClass().getName() + " doesn't support asynchronous send operation"); } private class ClientProcessor implements Runnable { private WebSocketConnector mConnector = null; /** * Creates the new socket listener thread for this connector. * * @param aConnector */ public ClientProcessor(WebSocketConnector aConnector) { mConnector = aConnector; } @Override public void run() { WebSocketEngine lEngine = getEngine(); ByteArrayOutputStream lBuff = new ByteArrayOutputStream(); try { // start client listener loop mIsRunning = true; // call connectorStarted method of engine lEngine.connectorStarted(mConnector); if (isHixieDraft()) { readHixie(lBuff, lEngine); } else { // assume that #02 and #03 are the same regarding packet processing readHybi(lBuff, lEngine); } // call client stopped method of engine // (e.g. to release client from streams) lEngine.connectorStopped(mConnector, mCloseReason); // br.close(); mIn.close(); mOut.close(); mClientSocket.close(); } catch (Exception lEx) { // ignore this exception for now mLog.error("(close) " + lEx.getClass().getSimpleName() + ": " + lEx.getMessage()); } } private void readHixie(ByteArrayOutputStream aBuff, WebSocketEngine aEngine) throws IOException { while (mIsRunning) { try { int lIn = mIn.read(); // start of frame if (lIn == 0x00) { aBuff.reset(); // end of frame } else if (lIn == 0xFF) { RawPacket lPacket = new RawPacket(aBuff.toByteArray()); try { aEngine.processPacket(mConnector, lPacket); } catch (Exception lEx) { mLog.error(lEx.getClass().getSimpleName() + " in processPacket of connector " + mConnector.getClass().getSimpleName() + ": " + lEx.getMessage()); } aBuff.reset(); } else if (lIn < 0) { mCloseReason = CloseReason.CLIENT; mIsRunning = false; // any other byte within or outside a frame } else { aBuff.write(lIn); } } catch (SocketTimeoutException lEx) { mLog.error("(timeout) " + lEx.getClass().getSimpleName() + ": " + lEx.getMessage()); mCloseReason = CloseReason.TIMEOUT; mIsRunning = false; } catch (Exception lEx) { mLog.error("(other) " + lEx.getClass().getSimpleName() + ": " + lEx.getMessage()); mCloseReason = CloseReason.SERVER; mIsRunning = false; } } } /** * One message may consist of one or more (fragmented message) protocol packets. * The spec is currently unclear whether control packets (ping, pong, close) may * be intermingled with fragmented packets of another message. For now I've * decided to not implement such packets 'swapping', and therefore reading fails * miserably if a client sends control packets during fragmented message read. * TODO: follow next spec drafts and add support for control packets inside fragmented message if needed. * <p> * Structure of packets conforms to the following scheme (copied from spec): * </p> * <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 * +-+-+-+-+-------+-+-------------+-------------------------------+ * |M|R|R|R| opcode|R| Payload len | Extended payload length | * |O|S|S|S| (4) |S| (7) | (16/63) | * |R|V|V|V| |V| | (if payload len==126/127) | * |E|1|2|3| |4| | | * +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + * | Extended payload length continued, if payload len == 127 | * + - - - - - - - - - - - - - - - +-------------------------------+ * | | Extension data | * +-------------------------------+ - - - - - - - - - - - - - - - + * : : * +---------------------------------------------------------------+ * : Application data : * +---------------------------------------------------------------+ * </pre> * RSVx bits are ignored (reserved for future use). * TODO: add support for extension data, when extensions will be defined in the specs. * * <p> * Read section 4.2 of the spec for detailed explanation. * </p> */ private void readHybi(ByteArrayOutputStream aBuff, WebSocketEngine aEngine) throws IOException { int lPacketType; // utilize data input stream, because it has convenient methods for reading // signed/unsigned bytes, shorts, ints and longs DataInputStream lDis = new DataInputStream(mIn); while (mIsRunning) { try { // begin normal packet read int lFlags = lDis.read(); // determine fragmentation boolean lFragmented = (0x01 & lFlags) == 0x01; // shift 4 bits to skip the first bit and three RSVx bits int lType = lFlags >> 4; lPacketType = WebSocketProtocolHandler.toRawPacketType(lType); if (lPacketType == -1) { // Could not determine packet type, ignore the packet. // Maybe we need a setting to decide, if such packets should abort the connection? mLog.trace("Dropping packet with unknown type: " + lType); } else { // Ignore first bit. Payload length is next seven bits, unless its value is greater than 125. long lPayloadLen = mIn.read() >> 1; if (lPayloadLen == 126) { // following two bytes are acutal payload length (16-bit unsigned integer) lPayloadLen = lDis.readUnsignedShort(); } else if (lPayloadLen == 127) { // following eight bytes are actual payload length (64-bit unsigned integer) lPayloadLen = lDis.readLong(); } if (lPayloadLen > 0) { // payload length may be extremely long, so we read in loop rather // than construct one byte[] array and fill it with read() method, // because java does not allow longs as array size while (lPayloadLen-- > 0) { aBuff.write(lDis.read()); } } if (!lFragmented) { if (lPacketType == RawPacket.FRAMETYPE_PING) { // As per spec, server must respond to PING with PONG (maybe // this should be handled higher up in the hierarchy?) WebSocketPacket lPong = new RawPacket(aBuff.toByteArray()); lPong.setFrameType(RawPacket.FRAMETYPE_PONG); sendPacket(lPong); } else if (lPacketType == RawPacket.FRAMETYPE_CLOSE) { mCloseReason = CloseReason.CLIENT; mIsRunning = false; // As per spec, server must respond to CLOSE with acknowledgment CLOSE (maybe // this should be handled higher up in the hierarchy?) WebSocketPacket lClose = new RawPacket(aBuff.toByteArray()); lClose.setFrameType(RawPacket.FRAMETYPE_CLOSE); sendPacket(lClose); } // Packet was read, pass it forward. WebSocketPacket lPacket = new RawPacket(aBuff.toByteArray()); lPacket.setFrameType(lPacketType); try { /* Please keep this comment for debug purposes if (mLog.isDebugEnabled()) { mLog.debug("Received packet: '" + lPacket.getUTF8() + "'"); } */ aEngine.processPacket(mConnector, lPacket); } catch (Exception lEx) { mLog.error(lEx.getClass().getSimpleName() + " in processPacket of connector " + mConnector.getClass().getSimpleName() + ": " + lEx.getMessage()); } aBuff.reset(); } } } catch (SocketTimeoutException lEx) { mLog.error("(timeout) " + lEx.getClass().getSimpleName() + ": " + lEx.getMessage()); mCloseReason = CloseReason.TIMEOUT; mIsRunning = false; } catch (Exception lEx) { mLog.error("(other) " + lEx.getClass().getSimpleName() + ": " + lEx.getMessage()); mCloseReason = CloseReason.SERVER; mIsRunning = false; } } } } @Override public String generateUID() { String lUID = mClientSocket.getInetAddress().getHostAddress() + "@" + mClientSocket.getPort(); return lUID; } @Override public int getRemotePort() { return mClientSocket.getPort(); } @Override public InetAddress getRemoteHost() { return mClientSocket.getInetAddress(); } @Override public String toString() { // TODO: Show proper IPV6 if used String lRes = getId() + " (" + getRemoteHost().getHostAddress() + ":" + getRemotePort(); String lUsername = getUsername(); if (lUsername != null) { lRes += ", " + lUsername; } return lRes + ")"; } private void sendHixie(WebSocketPacket aDataPacket) throws IOException { if (aDataPacket.getFrameType() == RawPacket.FRAMETYPE_BINARY) { // each packet is enclosed in 0xFF<length><data> // TODO: for future use! Not yet finally spec'd in IETF drafts! mOut.write(0xFF); byte[] lBA = aDataPacket.getByteArray(); // TODO: implement multi byte length! mOut.write(lBA.length); mOut.write(lBA); } else { // each packet is enclosed in 0x00<data>0xFF mOut.write(0x00); mOut.write(aDataPacket.getByteArray()); mOut.write(0xFF); } } // TODO: implement fragmentation for packet sending private void sendHybi(WebSocketPacket aDataPacket) throws IOException { byte[] lPacket = WebSocketProtocolHandler.toProtocolPacket(aDataPacket); mOut.write(lPacket); } private boolean isHixieDraft() { return JWebSocketCommonConstants.WS_DRAFT_DEFAULT.equals(getHeader().getDraft()); } }