/******************************************************************************
*
* 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 org.magnum.soda.transport.wamp;
import java.io.IOException;
import java.net.SocketException;
import java.nio.channels.SocketChannel;
import java.util.Random;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Base64;
import android.util.Log;
/**
* WebSocket writer, the sending leg of a WebSockets connection.
* This is run on it's background thread with it's own message loop.
* The only method that needs to be called (from foreground thread) is forward(),
* which is used to forward a WebSockets message to this object (running on
* background thread) so that it can be formatted and sent out on the
* underlying TCP socket.
*/
public class WebSocketWriter extends Handler {
private static final boolean DEBUG = true;
private static final String TAG = WebSocketWriter.class.getName();
/// Random number generator for handshake key and frame mask generation.
private final Random mRng = new Random();
/// Connection master.
private final Handler mMaster;
/// Message looper this object is running on.
private final Looper mLooper;
/// The NIO socket channel created on foreground thread.
private final SocketChannel mSocket;
/// WebSockets options.
private final WebSocketOptions mOptions;
/// The send buffer that holds data to send on socket.
private final ByteBufferOutputStream mBuffer;
/**
* Create new WebSockets background writer.
*
* @param looper The message looper of the background thread on which
* this object is running.
* @param master The message handler of master (foreground thread).
* @param socket The socket channel created on foreground thread.
* @param options WebSockets connection options.
*/
public WebSocketWriter(Looper looper, Handler master, SocketChannel socket, WebSocketOptions options) {
super(looper);
mLooper = looper;
mMaster = master;
mSocket = socket;
mOptions = options;
mBuffer = new ByteBufferOutputStream(options.getMaxFramePayloadSize() + 14, 4*64*1024);
if (DEBUG) Log.d(TAG, "created");
}
/**
* Call this from the foreground (UI) thread to make the writer
* (running on background thread) send a WebSocket message on the
* underlying TCP.
*
* @param message Message to send to WebSockets writer. An instance of the message
* classes inside WebSocketMessage or another type which then needs
* to be handled within processAppMessage() (in a class derived from
* this class).
*/
public void forward(Object message) {
Message msg = obtainMessage();
msg.obj = message;
sendMessage(msg);
}
/**
* Notify the master (foreground thread).
*
* @param message Message to send to master.
*/
private void notify(Object message) {
Message msg = mMaster.obtainMessage();
msg.obj = message;
mMaster.sendMessage(msg);
}
/**
* Create new key for WebSockets handshake.
*
* @return WebSockets handshake key (Base64 encoded).
*/
private String newHandshakeKey() {
final byte[] ba = new byte[16];
mRng.nextBytes(ba);
return Base64.encodeToString(ba, Base64.NO_WRAP);
}
/**
* Create new (random) frame mask.
*
* @return Frame mask (4 octets).
*/
private byte[] newFrameMask() {
final byte[] ba = new byte[4];
mRng.nextBytes(ba);
return ba;
}
/**
* Send WebSocket client handshake.
*/
private void sendClientHandshake(WebSocketMessage.ClientHandshake message) throws IOException {
// write HTTP header with handshake
String path;
if (message.mQuery != null) {
path = message.mPath + "?" + message.mQuery;
} else {
path = message.mPath;
}
mBuffer.write("GET " + path + " HTTP/1.1");
mBuffer.crlf();
mBuffer.write("Host: " + message.mHost);
mBuffer.crlf();
mBuffer.write("Upgrade: WebSocket");
mBuffer.crlf();
mBuffer.write("Connection: Upgrade");
mBuffer.crlf();
mBuffer.write("Sec-WebSocket-Key: " + newHandshakeKey());
mBuffer.crlf();
if (message.mOrigin != null && !message.mOrigin.equals("")) {
mBuffer.write("Origin: " + message.mOrigin);
mBuffer.crlf();
}
if (message.mSubprotocols != null && message.mSubprotocols.length > 0) {
mBuffer.write("Sec-WebSocket-Protocol: ");
for (int i = 0; i < message.mSubprotocols.length; ++i) {
mBuffer.write(message.mSubprotocols[i]);
mBuffer.write(", ");
}
mBuffer.crlf();
}
mBuffer.write("Sec-WebSocket-Version: 13");
mBuffer.crlf();
mBuffer.crlf();
}
/**
* Send WebSockets close.
*/
private void sendClose(WebSocketMessage.Close message) throws IOException, WebSocketException {
if (message.mCode > 0) {
byte[] payload = null;
if (message.mReason != null && !message.mReason.equals("")) {
byte[] pReason = message.mReason.getBytes("UTF-8");
payload = new byte[2 + pReason.length];
for (int i = 0; i < pReason.length; ++i) {
payload[i + 2] = pReason[i];
}
} else {
payload = new byte[2];
}
if (payload != null && payload.length > 125) {
throw new WebSocketException("close payload exceeds 125 octets");
}
payload[0] = (byte)((message.mCode >> 8) & 0xff);
payload[1] = (byte)(message.mCode & 0xff);
sendFrame(8, true, payload);
} else {
sendFrame(8, true, null);
}
}
/**
* Send WebSockets ping.
*/
private void sendPing(WebSocketMessage.Ping message) throws IOException, WebSocketException {
if (message.mPayload != null && message.mPayload.length > 125) {
throw new WebSocketException("ping payload exceeds 125 octets");
}
sendFrame(9, true, message.mPayload);
}
/**
* Send WebSockets pong. Normally, unsolicited Pongs are not used,
* but Pongs are only send in response to a Ping from the peer.
*/
private void sendPong(WebSocketMessage.Pong message) throws IOException, WebSocketException {
if (message.mPayload != null && message.mPayload.length > 125) {
throw new WebSocketException("pong payload exceeds 125 octets");
}
sendFrame(10, true, message.mPayload);
}
/**
* Send WebSockets binary message.
*/
private void sendBinaryMessage(WebSocketMessage.BinaryMessage message) throws IOException, WebSocketException {
if (message.mPayload.length > mOptions.getMaxMessagePayloadSize()) {
throw new WebSocketException("message payload exceeds payload limit");
}
sendFrame(2, true, message.mPayload);
}
/**
* Send WebSockets text message.
*/
private void sendTextMessage(WebSocketMessage.TextMessage message) throws IOException, WebSocketException {
byte[] payload = message.mPayload.getBytes("UTF-8");
if (payload.length > mOptions.getMaxMessagePayloadSize()) {
throw new WebSocketException("message payload exceeds payload limit");
}
sendFrame(1, true, payload);
}
/**
* Send WebSockets binary message.
*/
private void sendRawTextMessage(WebSocketMessage.RawTextMessage message) throws IOException, WebSocketException {
if (message.mPayload.length > mOptions.getMaxMessagePayloadSize()) {
throw new WebSocketException("message payload exceeds payload limit");
}
sendFrame(1, true, message.mPayload);
}
/**
* Sends a WebSockets frame. Only need to use this method in derived classes which implement
* more message types in processAppMessage(). You need to know what you are doing!
*
* @param opcode The WebSocket frame opcode.
* @param fin FIN flag for WebSocket frame.
* @param payload Frame payload or null.
*/
protected void sendFrame(int opcode, boolean fin, byte[] payload) throws IOException {
if (payload != null) {
sendFrame(opcode, fin, payload, 0, payload.length);
} else {
sendFrame(opcode, fin, null, 0, 0);
}
}
/**
* Sends a WebSockets frame. Only need to use this method in derived classes which implement
* more message types in processAppMessage(). You need to know what you are doing!
*
* @param opcode The WebSocket frame opcode.
* @param fin FIN flag for WebSocket frame.
* @param payload Frame payload or null.
* @param offset Offset within payload of the chunk to send.
* @param length Length of the chunk within payload to send.
*/
protected void sendFrame(int opcode, boolean fin, byte[] payload, int offset, int length) throws IOException {
// first octet
byte b0 = 0;
if (fin) {
b0 |= (byte) (1 << 7);
}
b0 |= (byte) opcode;
mBuffer.write(b0);
// second octet
byte b1 = 0;
if (mOptions.getMaskClientFrames()) {
b1 = (byte) (1 << 7);
}
long len = length;
// extended payload length
if (len <= 125) {
b1 |= (byte) len;
mBuffer.write(b1);
} else if (len <= 0xffff) {
b1 |= (byte) (126 & 0xff);
mBuffer.write(b1);
mBuffer.write(new byte[] {(byte)((len >> 8) & 0xff),
(byte)(len & 0xff)});
} else {
b1 |= (byte) (127 & 0xff);
mBuffer.write(b1);
mBuffer.write(new byte[] {(byte)((len >> 56) & 0xff),
(byte)((len >> 48) & 0xff),
(byte)((len >> 40) & 0xff),
(byte)((len >> 32) & 0xff),
(byte)((len >> 24) & 0xff),
(byte)((len >> 16) & 0xff),
(byte)((len >> 8) & 0xff),
(byte)(len & 0xff)});
}
byte mask[] = null;
if (mOptions.getMaskClientFrames()) {
// a mask is always needed, even without payload
mask = newFrameMask();
mBuffer.write(mask[0]);
mBuffer.write(mask[1]);
mBuffer.write(mask[2]);
mBuffer.write(mask[3]);
}
if (len > 0) {
if (mOptions.getMaskClientFrames()) {
/// \todo optimize masking
/// \todo masking within buffer of output stream
for (int i = 0; i < len; ++i) {
payload[i + offset] ^= mask[i % 4];
}
}
mBuffer.write(payload, offset, length);
}
}
/**
* Process message received from foreground thread. This is called from
* the message looper set up for the background thread running this writer.
*
* @param msg Message from thread message queue.
*/
@Override
public void handleMessage(Message msg) {
try {
// clear send buffer
mBuffer.clear();
// process message from master
processMessage(msg.obj);
// send out buffered data
mBuffer.flip();
while (mBuffer.remaining() > 0) {
// this can block on socket write
@SuppressWarnings("unused")
int written = mSocket.write(mBuffer.getBuffer());
}
} catch (SocketException e) {
if (DEBUG) Log.d(TAG, "run() : SocketException (" + e.toString() + ")");
// wrap the exception and notify master
notify(new WebSocketMessage.ConnectionLost());
} catch (Exception e) {
if (DEBUG) e.printStackTrace();
// wrap the exception and notify master
notify(new WebSocketMessage.Error(e));
}
}
/**
* Process WebSockets or control message from master. Normally,
* there should be no reason to override this. If you do, you
* need to know what you are doing.
*
* @param msg An instance of the message types within WebSocketMessage
* or a message that is handled in processAppMessage().
*/
protected void processMessage(Object msg) throws IOException, WebSocketException {
if (msg instanceof WebSocketMessage.TextMessage) {
sendTextMessage((WebSocketMessage.TextMessage) msg);
} else if (msg instanceof WebSocketMessage.RawTextMessage) {
sendRawTextMessage((WebSocketMessage.RawTextMessage) msg);
} else if (msg instanceof WebSocketMessage.BinaryMessage) {
sendBinaryMessage((WebSocketMessage.BinaryMessage) msg);
} else if (msg instanceof WebSocketMessage.Ping) {
sendPing((WebSocketMessage.Ping) msg);
} else if (msg instanceof WebSocketMessage.Pong) {
sendPong((WebSocketMessage.Pong) msg);
} else if (msg instanceof WebSocketMessage.Close) {
sendClose((WebSocketMessage.Close) msg);
} else if (msg instanceof WebSocketMessage.ClientHandshake) {
sendClientHandshake((WebSocketMessage.ClientHandshake) msg);
} else if (msg instanceof WebSocketMessage.Quit) {
mLooper.quit();
if (DEBUG) Log.d(TAG, "ended");
return;
} else {
// call hook which may be overridden in derived class to process
// messages we don't understand in this class
processAppMessage(msg);
}
}
/**
* Process message other than plain WebSockets or control message.
* This is intended to be overridden in derived classes.
*
* @param msg Message from foreground thread to process.
*/
protected void processAppMessage(Object msg) throws WebSocketException, IOException {
throw new WebSocketException("unknown message received by WebSocketWriter");
}
}