/****************************************************************************** * * Copyright 2011-2012 Tavendo GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ******************************************************************************/ package de.tavendo.autobahn; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.Socket; import java.net.SocketException; import java.nio.ByteBuffer; import android.os.Handler; import android.os.Message; import android.util.Log; import android.util.Pair; import de.tavendo.autobahn.WebSocketMessage.WebSocketCloseCode; /** * WebSocket reader, the receiving leg of a WebSockets connection. * This runs on it's own background thread and posts messages to master * thread's message queue for there to be consumed by the application. * The only method that needs to be called (from foreground thread) is quit(), * which gracefully shuts down the background receiver thread. */ public class WebSocketReader extends Thread { private static final String TAG = WebSocketReader.class.getCanonicalName(); private static enum ReaderState { STATE_CLOSED, STATE_CONNECTING, STATE_CLOSING, STATE_OPEN } private final Handler mWebSocketConnectionHandler; private final Socket mSocket; private InputStream mInputStream; private final WebSocketOptions mWebSocketOptions; private volatile boolean mStopped = false; private final byte[] mNetworkBuffer; private final ByteBuffer mApplicationBuffer; private NoCopyByteArrayOutputStream mMessagePayload; private ReaderState mState; private boolean mInsideMessage = false; private int mMessageOpcode; private WebSocketFrameHeader mFrameHeader; private Utf8Validator mUTF8Validator = new Utf8Validator(); /** * Create new WebSockets background reader. * * @param master The message handler of master (foreground thread). * @param socket The socket channel created on foreground thread. */ public WebSocketReader(Handler master, Socket socket, WebSocketOptions options, String threadName) { super(threadName); this.mWebSocketConnectionHandler = master; this.mSocket = socket; this.mWebSocketOptions = options; this.mNetworkBuffer = new byte[4096]; this.mApplicationBuffer = ByteBuffer.allocateDirect(options.getMaxFramePayloadSize() + 14); this.mMessagePayload = new NoCopyByteArrayOutputStream(options.getMaxMessagePayloadSize()); this.mFrameHeader = null; this.mState = ReaderState.STATE_CONNECTING; Log.d(TAG, "WebSocket reader created."); } /** * Graceful shutdown of background reader thread (called from master). */ public void quit() { mStopped = true; Log.d(TAG, "quit"); } /** * Notify the master (foreground thread) of WebSockets message received * and unwrapped. * * @param message Message to send to master. */ protected void notify(Object message) { Message msg = mWebSocketConnectionHandler.obtainMessage(); msg.obj = message; mWebSocketConnectionHandler.sendMessage(msg); } /** * Process incoming WebSockets data (after handshake). */ private boolean processData() throws Exception { // outside frame? if (mFrameHeader == null) { // need at least 2 bytes from WS frame header to start processing if (mApplicationBuffer.position() >= 2) { byte b0 = mApplicationBuffer.get(0); boolean fin = (b0 & 0x80) != 0; int rsv = (b0 & 0x70) >> 4; int opcode = b0 & 0x0f; byte b1 = mApplicationBuffer.get(1); boolean masked = (b1 & 0x80) != 0; int payload_len1 = b1 & 0x7f; // now check protocol compliance if (rsv != 0) { throw new WebSocketException("RSV != 0 and no extension negotiated"); } if (masked) { // currently, we don't allow this. need to see whats the final spec. throw new WebSocketException("masked server frame"); } if (opcode > 7) { // control frame if (!fin) { throw new WebSocketException("fragmented control frame"); } if (payload_len1 > 125) { throw new WebSocketException("control frame with payload length > 125 octets"); } if (opcode != 8 && opcode != 9 && opcode != 10) { throw new WebSocketException("control frame using reserved opcode " + opcode); } if (opcode == 8 && payload_len1 == 1) { throw new WebSocketException("received close control frame with payload len 1"); } } else { // message frame if (opcode != 0 && opcode != 1 && opcode != 2) { throw new WebSocketException("data frame using reserved opcode " + opcode); } if (!mInsideMessage && opcode == 0) { throw new WebSocketException("received continuation data frame outside fragmented message"); } if (mInsideMessage && opcode != 0) { throw new WebSocketException("received non-continuation data frame while inside fragmented message"); } } int mask_len = masked ? 4 : 0; int header_len = 0; if (payload_len1 < 126) { header_len = 2 + mask_len; } else if (payload_len1 == 126) { header_len = 2 + 2 + mask_len; } else if (payload_len1 == 127) { header_len = 2 + 8 + mask_len; } else { // should not arrive here throw new Exception("logic error"); } // continue when complete frame header is available if (mApplicationBuffer.position() >= header_len) { // determine frame payload length int i = 2; long payload_len = 0; if (payload_len1 == 126) { payload_len = ((0xff & mApplicationBuffer.get(i)) << 8) | (0xff & mApplicationBuffer.get(i+1)); if (payload_len < 126) { throw new WebSocketException("invalid data frame length (not using minimal length encoding)"); } i += 2; } else if (payload_len1 == 127) { if ((0x80 & mApplicationBuffer.get(i+0)) != 0) { throw new WebSocketException("invalid data frame length (> 2^63)"); } payload_len = ((0xff & mApplicationBuffer.get(i+0)) << 56) | ((0xff & mApplicationBuffer.get(i+1)) << 48) | ((0xff & mApplicationBuffer.get(i+2)) << 40) | ((0xff & mApplicationBuffer.get(i+3)) << 32) | ((0xff & mApplicationBuffer.get(i+4)) << 24) | ((0xff & mApplicationBuffer.get(i+5)) << 16) | ((0xff & mApplicationBuffer.get(i+6)) << 8) | ((0xff & mApplicationBuffer.get(i+7)) ); if (payload_len < 65536) { throw new WebSocketException("invalid data frame length (not using minimal length encoding)"); } i += 8; } else { payload_len = payload_len1; } // immediately bail out on frame too large if (payload_len > mWebSocketOptions.getMaxFramePayloadSize()) { throw new WebSocketException("frame payload too large"); } // save frame header metadata mFrameHeader = new WebSocketFrameHeader(); mFrameHeader.setOpcode(opcode); mFrameHeader.setFin(fin); mFrameHeader.setReserved(rsv); mFrameHeader.setPayloadLength((int) payload_len); mFrameHeader.setHeaderLength(header_len); mFrameHeader.setTotalLen(mFrameHeader.getHeaderLength() + mFrameHeader.getPayloadLength()); if (masked) { byte[] mask = new byte[4]; for (int j = 0; j < 4; ++j) { mask[i] = (byte) (0xff & mApplicationBuffer.get(i + j)); } mFrameHeader.setMask(mask); i += 4; } else { mFrameHeader.setMask(null); } // continue processing when payload empty or completely buffered return mFrameHeader.getPayloadLength() == 0 || mApplicationBuffer.position() >= mFrameHeader.getTotalLength(); } else { // need more data return false; } } else { // need more data return false; } } else { /// \todo refactor this for streaming processing, incl. fail fast on invalid UTF-8 within frame already // within frame // see if we buffered complete frame if (mApplicationBuffer.position() >= mFrameHeader.getTotalLength()) { // cut out frame payload byte[] framePayload = null; int oldPosition = mApplicationBuffer.position(); if (mFrameHeader.getPayloadLength() > 0) { framePayload = new byte[mFrameHeader.getPayloadLength()]; mApplicationBuffer.position(mFrameHeader.getHeaderLength()); mApplicationBuffer.get(framePayload, 0, (int) mFrameHeader.getPayloadLength()); } mApplicationBuffer.position(mFrameHeader.getTotalLength()); mApplicationBuffer.limit(oldPosition); mApplicationBuffer.compact(); if (mFrameHeader.getOpcode() > 7) { // control frame if (mFrameHeader.getOpcode() == 8) { int code = WebSocketCloseCode.RESERVED_NO_STATUS; String reason = null; if (mFrameHeader.getPayloadLength() >= 2) { // parse and check close code code = (framePayload[0] & 0xff) * 256 + (framePayload[1] & 0xff); if (code < 1000 || (code >= 1000 && code <= 2999 && code != 1000 && code != 1001 && code != 1002 && code != 1003 && code != 1007 && code != 1008 && code != 1009 && code != 1010 && code != 1011) || code >= 5000) { throw new WebSocketException("invalid close code " + code); } // parse and check close reason if (mFrameHeader.getPayloadLength() > 2) { byte[] ra = new byte[mFrameHeader.getPayloadLength() - 2]; System.arraycopy(framePayload, 2, ra, 0, mFrameHeader.getPayloadLength() - 2); Utf8Validator val = new Utf8Validator(); val.validate(ra); if (!val.isValid()) { throw new WebSocketException("invalid close reasons (not UTF-8)"); } else { reason = new String(ra, WebSocket.UTF8_ENCODING); } } } onClose(code, reason); } else if (mFrameHeader.getOpcode() == 9) { // dispatch WS ping onPing(framePayload); } else if (mFrameHeader.getOpcode() == 10) { // dispatch WS pong onPong(framePayload); } else { // should not arrive here (handled before) throw new Exception("logic error"); } } else { // message frame if (!mInsideMessage) { // new message started mInsideMessage = true; mMessageOpcode = mFrameHeader.getOpcode(); if (mMessageOpcode == 1 && mWebSocketOptions.getValidateIncomingUtf8()) { mUTF8Validator.reset(); } } if (framePayload != null) { // immediately bail out on message too large if (mMessagePayload.size() + framePayload.length > mWebSocketOptions.getMaxMessagePayloadSize()) { throw new WebSocketException("message payload too large"); } // validate incoming UTF-8 if (mMessageOpcode == 1 && mWebSocketOptions.getValidateIncomingUtf8() && !mUTF8Validator.validate(framePayload)) { throw new WebSocketException("invalid UTF-8 in text message payload"); } // buffer frame payload for message mMessagePayload.write(framePayload); } // on final frame .. if (mFrameHeader.isFin()) { if (mMessageOpcode == 1) { // verify that UTF-8 ends on codepoint if (mWebSocketOptions.getValidateIncomingUtf8() && !mUTF8Validator.isValid()) { throw new WebSocketException("UTF-8 text message payload ended within Unicode code point"); } // deliver text message if (mWebSocketOptions.getReceiveTextMessagesRaw()) { // dispatch WS text message as raw (but validated) UTF-8 onRawTextMessage(mMessagePayload.toByteArray()); } else { // dispatch WS text message as Java String (previously already validated) String s = new String(mMessagePayload.toByteArray(), WebSocket.UTF8_ENCODING); onTextMessage(s); } } else if (mMessageOpcode == 2) { // dispatch WS binary message onBinaryMessage(mMessagePayload.toByteArray()); } else { // should not arrive here (handled before) throw new Exception("logic error"); } // ok, message completed - reset all mInsideMessage = false; mMessagePayload.reset(); } } // reset frame mFrameHeader = null; // reprocess if more data left return mApplicationBuffer.position() > 0; } else { // need more data return false; } } } /** * WebSockets handshake reply from server received, default notifies master. * * @param success Success handshake flag */ protected void onHandshake(boolean success) { notify(new WebSocketMessage.ServerHandshake(success)); } /** * WebSockets close received, default notifies master. */ protected void onClose(int code, String reason) { notify(new WebSocketMessage.Close(code, reason)); } /** * WebSockets ping received, default notifies master. * * @param payload Ping payload or null. */ protected void onPing(byte[] payload) { notify(new WebSocketMessage.Ping(payload)); } /** * WebSockets pong received, default notifies master. * * @param payload Pong payload or null. */ protected void onPong(byte[] payload) { notify(new WebSocketMessage.Pong(payload)); } /** * WebSockets text message received, default notifies master. * This will only be called when the option receiveTextMessagesRaw * HAS NOT been set. * * @param payload Text message payload as Java String decoded * from raw UTF-8 payload or null (empty payload). */ protected void onTextMessage(String payload) { notify(new WebSocketMessage.TextMessage(payload)); } /** * WebSockets text message received, default notifies master. * This will only be called when the option receiveTextMessagesRaw * HAS been set. * * @param payload Text message payload as raw UTF-8 octets or * null (empty payload). */ protected void onRawTextMessage(byte[] payload) { notify(new WebSocketMessage.RawTextMessage(payload)); } /** * WebSockets binary message received, default notifies master. * * @param payload Binary message payload or null (empty payload). */ protected void onBinaryMessage(byte[] payload) { notify(new WebSocketMessage.BinaryMessage(payload)); } /** * Process WebSockets handshake received from server. */ private boolean processHandshake() throws UnsupportedEncodingException { boolean res = false; for (int pos = mApplicationBuffer.position() - 4; pos >= 0; --pos) { if (mApplicationBuffer.get(pos+0) == 0x0d && mApplicationBuffer.get(pos+1) == 0x0a && mApplicationBuffer.get(pos+2) == 0x0d && mApplicationBuffer.get(pos+3) == 0x0a) { /// \todo process & verify handshake from server /// \todo forward subprotocol, if any int oldPosition = mApplicationBuffer.position(); // Check HTTP status code boolean serverError = false; if (mApplicationBuffer.get(0) == 'H' && mApplicationBuffer.get(1) == 'T' && mApplicationBuffer.get(2) == 'T' && mApplicationBuffer.get(3) == 'P') { Pair<Integer, String> status = parseHTTPStatus(); if (status.first >= 300) { // Invalid status code for success connection notify(new WebSocketMessage.ServerError(status.first, status.second)); serverError = true; } } mApplicationBuffer.position(pos + 4); mApplicationBuffer.limit(oldPosition); mApplicationBuffer.compact(); if (!serverError) { // process further when data after HTTP headers left in buffer res = mApplicationBuffer.position() > 0; mState = ReaderState.STATE_OPEN; } else { res = true; mState = ReaderState.STATE_CLOSED; mStopped = true; } onHandshake(!serverError); break; } } return res; } private Pair<Integer, String> parseHTTPStatus() throws UnsupportedEncodingException { int beg, end; // Find first space for (beg = 4; beg < mApplicationBuffer.position(); ++beg) { if (mApplicationBuffer.get(beg) == ' ') break; } // Find second space for (end = beg + 1; end < mApplicationBuffer.position(); ++end) { if (mApplicationBuffer.get(end) == ' ') break; } // Parse status code between them ++beg; int statusCode = 0; for (int i = 0; beg + i < end; ++i) { int digit = (mApplicationBuffer.get(beg + i) - 0x30); statusCode *= 10; statusCode += digit; } // Find end of line to extract error message ++end; int eol; for (eol = end; eol < mApplicationBuffer.position(); ++eol) { if (mApplicationBuffer.get(eol) == 0x0d) break; } int statusMessageLength = eol - end; byte[] statusBuf = new byte[statusMessageLength]; mApplicationBuffer.position(end); mApplicationBuffer.get(statusBuf, 0, statusMessageLength); String statusMessage = new String(statusBuf, WebSocket.UTF8_ENCODING); Log.w(TAG, String.format("Status: %d (%s)", statusCode, statusMessage)); return new Pair<Integer, String>(statusCode, statusMessage); } /** * Consume data buffered in mFrameBuffer. */ private boolean consumeData() throws Exception { switch (mState) { case STATE_OPEN: case STATE_CLOSING: return processData(); case STATE_CLOSED: return false; case STATE_CONNECTING: return processHandshake(); default: return false; } } /** * Run the background reader thread loop. */ @Override public void run() { synchronized (this) { notifyAll(); } InputStream inputStream = null; try { inputStream = mSocket.getInputStream(); } catch (IOException e) { Log.e(TAG, e.getLocalizedMessage()); return; } this.mInputStream = inputStream; Log.d(TAG, "WebSocker reader running."); mApplicationBuffer.clear(); while (!mStopped) { try { int bytesRead = mInputStream.read(mNetworkBuffer); if (bytesRead > 0) { mApplicationBuffer.put(mNetworkBuffer, 0, bytesRead); while (consumeData()) { } } else if (bytesRead == -1) { Log.d(TAG, "run() : ConnectionLost"); notify(new WebSocketMessage.ConnectionLost()); this.mStopped = true; } else { Log.e(TAG, "WebSocketReader read() failed."); } } catch (WebSocketException e) { Log.d(TAG, "run() : WebSocketException (" + e.toString() + ")"); // wrap the exception and notify master notify(new WebSocketMessage.ProtocolViolation(e)); } catch (SocketException e) { Log.d(TAG, "run() : SocketException (" + e.toString() + ")"); // wrap the exception and notify master notify(new WebSocketMessage.ConnectionLost()); } catch (IOException e) { Log.d(TAG, "run() : IOException (" + e.toString() + ")"); notify(new WebSocketMessage.ConnectionLost()); } catch (Exception e) { Log.d(TAG, "run() : Exception (" + e.toString() + ")"); // wrap the exception and notify master notify(new WebSocketMessage.Error(e)); } } Log.d(TAG, "WebSocket reader ended."); } }