package eu.hgross.blaubot.websocket; import java.net.URI; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import eu.hgross.blaubot.core.BlaubotDevice; import eu.hgross.blaubot.core.acceptor.IBlaubotIncomingConnectionListener; import eu.hgross.blaubot.util.Log; import io.netty.buffer.ByteBuf; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.DefaultHttpHeaders; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame; import io.netty.handler.codec.http.websocketx.PongWebSocketFrame; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker; import io.netty.handler.codec.http.websocketx.WebSocketClientHandshakerFactory; import io.netty.handler.codec.http.websocketx.WebSocketFrame; import io.netty.handler.codec.http.websocketx.WebSocketVersion; import io.netty.util.CharsetUtil; /** * WebSocketClientHandler that manages the BlaubotWebsocketConnection * * After .connect() via the bootstrapper, call getConnection(). */ public class WebsocketClientHandler extends SimpleChannelInboundHandler<Object> { private static final String LOG_TAG = "WebsocketClientHandler"; private final WebSocketClientHandshaker handshaker; private final String remoteDeviceUniqueDeviceId; private final AtomicReference<IBlaubotIncomingConnectionListener> incomingConnectionListenerReference; private ChannelPromise handshakeFuture; private BlaubotWebsocketConnection connection; /** * Creates a new WebSocketClientHandler that manages the BlaubotWebsocketConnection * @param uri The uri to connect with * @param remoteUniqueDeviceId the unique device id of the device we are connecting to * @param listenerReference a reference Object that handles the connection listener */ public WebsocketClientHandler(URI uri, String remoteUniqueDeviceId, AtomicReference<IBlaubotIncomingConnectionListener> listenerReference) { // Connect with V13 (RFC 6455 aka HyBi-17). // other options are V08 or V00. // If V00 is used, ping is not supported and remember to change // HttpResponseDecoder to WebSocketHttpResponseDecoder in the pipeline. this.handshaker = WebSocketClientHandshakerFactory.newHandshaker(uri, WebSocketVersion.V13, null, false, new DefaultHttpHeaders(), BlaubotWebsocketAdapter.MAX_WEBSOCKET_FRAME_SIZE); this.remoteDeviceUniqueDeviceId = remoteUniqueDeviceId; this.incomingConnectionListenerReference = listenerReference; } @Override public void handlerAdded(ChannelHandlerContext ctx) { handshakeFuture = ctx.newPromise(); handshakeFuture.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) throws Exception { Channel channel = future.channel(); BlaubotDevice remoteDevice = new BlaubotDevice(remoteDeviceUniqueDeviceId); connection = new BlaubotWebsocketConnection(remoteDevice, channel); final IBlaubotIncomingConnectionListener connectionListener = incomingConnectionListenerReference.get(); if (connectionListener != null) { connectionListener.onConnectionEstablished(connection); } } }); } @Override public void channelActive(ChannelHandlerContext ctx) { handshaker.handshake(ctx.channel()); } @Override public void channelInactive(ChannelHandlerContext ctx) { if (Log.logDebugMessages()) { Log.d(LOG_TAG, "WebSocket Client disconnected!"); } } @Override public void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception { Channel ch = ctx.channel(); if (!handshaker.isHandshakeComplete()) { handshaker.finishHandshake(ch, (FullHttpResponse) msg); if (Log.logDebugMessages()) { Log.d(LOG_TAG, "WebSocket Client connected!"); } handshakeFuture.setSuccess(); return; } if (msg instanceof FullHttpResponse) { FullHttpResponse response = (FullHttpResponse) msg; throw new IllegalStateException( "Unexpected FullHttpResponse (getStatus=" + response.getStatus() + ", content=" + response.content().toString(CharsetUtil.UTF_8) + ')'); } WebSocketFrame frame = (WebSocketFrame) msg; if (frame instanceof BinaryWebSocketFrame) { BinaryWebSocketFrame binaryWebSocketFrame = (BinaryWebSocketFrame) frame; ByteBuf content = binaryWebSocketFrame.content(); // write to the connection connection.writeMockDataToInputStream(content); } else if (frame instanceof TextWebSocketFrame) { TextWebSocketFrame textFrame = (TextWebSocketFrame) frame; if (Log.logDebugMessages()) { Log.d(LOG_TAG, "WebSocket Client received message: " + textFrame.text()); } } else if (frame instanceof PongWebSocketFrame) { if (Log.logDebugMessages()) { Log.d(LOG_TAG, "WebSocket Client received pong"); } } else if (frame instanceof CloseWebSocketFrame) { if (Log.logDebugMessages()) { Log.d(LOG_TAG, "WebSocket Client received closing"); } ch.close(); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); if (!handshakeFuture.isDone()) { handshakeFuture.setFailure(cause); } ctx.close(); } /** * Blocks until the handshake is done. * If done, the blaubot connection is returned * * @return the blaubot connection if the connection was successful or null, if not * @throws InterruptedException if interrupted while waiting for the handshake */ public synchronized BlaubotWebsocketConnection getConnection() throws InterruptedException { if (this.connection != null) { return connection; } final CountDownLatch latch = new CountDownLatch(1); final AtomicBoolean result = new AtomicBoolean(false); handshakeFuture.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) throws Exception { result.set(future.isSuccess()); latch.countDown(); } }); latch.await(); if (result.get()) { return this.connection; } return null; } }