package eu.hgross.blaubot.websocket;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import eu.hgross.blaubot.core.IBlaubotAdapter;
import eu.hgross.blaubot.core.acceptor.ConnectionMetaDataDTO;
import eu.hgross.blaubot.core.acceptor.IBlaubotConnectionAcceptor;
import eu.hgross.blaubot.core.acceptor.IBlaubotIncomingConnectionListener;
import eu.hgross.blaubot.core.acceptor.IBlaubotListeningStateListener;
import eu.hgross.blaubot.core.acceptor.discovery.IBlaubotBeaconStore;
import eu.hgross.blaubot.util.Log;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.util.SelfSignedCertificate;
/**
* Acceptor using netty for communication over websockets
*/
public class BlaubotWebsocketAcceptor implements IBlaubotConnectionAcceptor {
/**
* Max milliseconds to bind to the given acceptorPort
*/
private static final long MAX_TIME_TO_BIND = 10000;
public static final String LOG_TAG = "BlaubotWebsocketAcceptor";
protected static boolean SSL = false;
private final IBlaubotAdapter adapter;
private final int acceptorPort;
private final String hostAddress;
private IBlaubotListeningStateListener listeningStateListener;
/**
* Is handed to the websocket handler
*/
private AtomicReference<IBlaubotIncomingConnectionListener> incomingConnectionListener;
/**
* The current netty channel on which the websocket is working
*/
private Channel currentChannel;
private final Object startStopMonitor = new Object();
public BlaubotWebsocketAcceptor(IBlaubotAdapter adapter, String hostAddress, int acceptorPort) {
this.adapter = adapter;
this.acceptorPort = acceptorPort;
this.hostAddress = hostAddress;
this.incomingConnectionListener = new AtomicReference<>();
}
@Override
public void setBeaconStore(IBlaubotBeaconStore beaconStore) {
// not used
}
@Override
public IBlaubotAdapter getAdapter() {
return adapter;
}
@Override
public void startListening() {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Starting netty websocket server.");
}
synchronized (startStopMonitor) {
// Configure SSL.
SslContext sslCtx;
if (SSL) {
try {
SelfSignedCertificate ssc = new SelfSignedCertificate();
sslCtx = SslContext.newServerContext(ssc.certificate(), ssc.privateKey());
} catch (Exception e) {
sslCtx = null;
e.printStackTrace();
}
} else {
sslCtx = null;
}
final EventLoopGroup bossGroup = new NioEventLoopGroup(1);
final EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new WebSocketServerInitializer(sslCtx));
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Creating websocket server. Binding to port " + acceptorPort);
}
final CountDownLatch latch = new CountDownLatch(1);
bootstrap.bind(acceptorPort).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Bind " + (future.isSuccess() ? "succeeded" : "failed"));
}
if (future.isSuccess()) {
currentChannel = future.channel();
} else {
currentChannel = null;
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
if (listeningStateListener != null) {
listeningStateListener.onListeningStarted(BlaubotWebsocketAcceptor.this);
}
latch.countDown();
}
});
boolean timedOut = !latch.await(MAX_TIME_TO_BIND, TimeUnit.MILLISECONDS);
if (timedOut) {
if (Log.logDebugMessages()) {
Log.e(LOG_TAG, "Failed to start listening");
}
}
} catch (InterruptedException e) {
e.printStackTrace();
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Netty websocket server should now be started.");
}
}
@Override
public void stopListening() {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Stopping netty websocket server ...");
}
synchronized (startStopMonitor) {
if(this.currentChannel != null) {
final ChannelFuture closeFuture = this.currentChannel.closeFuture();
closeFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (listeningStateListener != null) {
listeningStateListener.onListeningStopped(BlaubotWebsocketAcceptor.this);
}
}
});
this.currentChannel.close();
try {
closeFuture.sync();
} catch (InterruptedException e) {
e.printStackTrace();
}
this.currentChannel = null;
}
}
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Netty websocket server should now be stopped ...");
}
}
@Override
public boolean isStarted() {
synchronized (startStopMonitor) {
return currentChannel != null;
}
}
@Override
public void setListeningStateListener(IBlaubotListeningStateListener stateListener) {
this.listeningStateListener = stateListener;
}
@Override
public void setAcceptorListener(IBlaubotIncomingConnectionListener acceptorListener) {
this.incomingConnectionListener.set(acceptorListener);
}
@Override
public ConnectionMetaDataDTO getConnectionMetaData() {
return new WebsocketConnectionMetaDataDTO(hostAddress, BlaubotWebsocketAdapter.WEBSOCKET_PATH, acceptorPort);
}
/**
* Configures the pipeline for the netty channel
*/
public class WebSocketServerInitializer extends ChannelInitializer<SocketChannel> {
private final SslContext sslCtx;
public WebSocketServerInitializer(SslContext sslCtx) {
this.sslCtx = sslCtx;
}
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
if (sslCtx != null) {
pipeline.addLast(sslCtx.newHandler(ch.alloc()));
}
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new HttpObjectAggregator(65536));
pipeline.addLast(new WebsocketServerHandler(incomingConnectionListener));
}
}
}