package eu.hgross.blaubot.websocket;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import javax.net.ssl.SSLException;
import eu.hgross.blaubot.core.IBlaubotAdapter;
import eu.hgross.blaubot.core.IBlaubotConnection;
import eu.hgross.blaubot.core.IBlaubotDevice;
import eu.hgross.blaubot.core.acceptor.ConnectionMetaDataDTO;
import eu.hgross.blaubot.core.acceptor.IBlaubotConnectionListener;
import eu.hgross.blaubot.core.acceptor.IBlaubotIncomingConnectionListener;
import eu.hgross.blaubot.core.acceptor.discovery.IBlaubotBeaconStore;
import eu.hgross.blaubot.core.connector.IBlaubotConnector;
import eu.hgross.blaubot.core.statemachine.BlaubotAdapterHelper;
import eu.hgross.blaubot.util.Log;
import io.netty.bootstrap.Bootstrap;
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.NioSocketChannel;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
public class BlaubotWebsocketConnector implements IBlaubotConnector {
private static final List<String> acceptedConnectorTypes = Arrays.asList(WebsocketConnectionMetaDataDTO.ACCEPTOR_TYPE);
private static final String LOG_TAG = "BlaubotWebsocketConnector";
private final IBlaubotAdapter adapter;
private final IBlaubotDevice ownDevice;
private final AtomicReference<IBlaubotIncomingConnectionListener> incomingConnectionListener;
private IBlaubotBeaconStore beaconStore;
public BlaubotWebsocketConnector(IBlaubotAdapter adapter, IBlaubotDevice ownDevice) {
this.adapter = adapter;
this.ownDevice = ownDevice;
this.incomingConnectionListener = new AtomicReference<>();
}
@Override
public IBlaubotAdapter getAdapter() {
return adapter;
}
@Override
public void setBeaconStore(IBlaubotBeaconStore beaconStore) {
this.beaconStore = beaconStore;
}
@Override
public void setIncomingConnectionListener(IBlaubotIncomingConnectionListener acceptorConnectorListener) {
this.incomingConnectionListener.set(acceptorConnectorListener);
}
@Override
public IBlaubotConnection connectToBlaubotDevice(IBlaubotDevice blaubotDevice) {
// check if we can get connection meta data for this device
List<ConnectionMetaDataDTO> connectionMetaDataList = beaconStore.getLastKnownConnectionMetaData(blaubotDevice.getUniqueDeviceID());
List<ConnectionMetaDataDTO> filteredMetaDataList = BlaubotAdapterHelper.filterBySupportedAcceptorTypes(connectionMetaDataList, getSupportedAcceptorTypes());
// validate
if(filteredMetaDataList.isEmpty()) {
if (Log.logWarningMessages()) {
Log.w(LOG_TAG, "No meta data to connect to " + blaubotDevice);
}
return null;
}
// connect to the first applyable
WebsocketConnectionMetaDataDTO connectionMetaData = new WebsocketConnectionMetaDataDTO(filteredMetaDataList.get(0));
URI connectUri;
// get the uri and append our unique device id
String encodedUniqueDeviceID = null;
try {
encodedUniqueDeviceID = URLEncoder.encode(ownDevice.getUniqueDeviceID(), "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return null;
}
final String uriStr = connectionMetaData.getUri() + "?" + BlaubotWebsocketAdapter.URI_PARAM_UNIQUEDEVICEID + "=" + encodedUniqueDeviceID;
try {
connectUri = new URI(uriStr);
} catch (URISyntaxException e) {
if (Log.logErrorMessages()) {
Log.e(LOG_TAG, "Wrong URI format " + uriStr, e);
}
return null;
}
// connect to web socket using netty#
final String scheme = connectUri.getScheme();
final String host = connectUri.getHost();
final int port = connectUri.getPort();
if (!"ws".equalsIgnoreCase(scheme) && !"wss".equalsIgnoreCase(scheme)) {
if (Log.logErrorMessages()) {
Log.e(LOG_TAG, "Bad scheme, Only WS(S) is supported -> " + connectUri);
}
return null;
}
final boolean ssl = "wss".equalsIgnoreCase(scheme);
final SslContext sslCtx;
if (ssl) {
try {
sslCtx = SslContext.newClientContext(InsecureTrustManagerFactory.INSTANCE);
} catch (SSLException e) {
if (Log.logErrorMessages()) {
Log.e(LOG_TAG, "Failed to create ssl context", e);
}
return null;
}
} else {
sslCtx = null;
}
final EventLoopGroup group = new NioEventLoopGroup();
try {
final WebsocketClientHandler handler = new WebsocketClientHandler(connectUri, blaubotDevice.getUniqueDeviceID(), incomingConnectionListener);
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Connecting to websocket: " + uriStr);
}
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline p = ch.pipeline();
if (sslCtx != null) {
p.addLast(sslCtx.newHandler(ch.alloc(), host, port));
}
p.addLast(new HttpClientCodec(),
new HttpObjectAggregator(8192),
handler);
}
});
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Awaiting web socket handshake to complete ...");
}
ChannelFuture channelFuture = b.connect(connectUri.getHost(), port);
final AtomicBoolean result = new AtomicBoolean(false);
final CountDownLatch latch = new CountDownLatch(1);
channelFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
result.set(future.isSuccess());
latch.countDown();
}
});
latch.await();
if(result.get()) {
BlaubotWebsocketConnection connection = handler.getConnection();
if(connection == null) {
if (Log.logErrorMessages()) {
Log.d(LOG_TAG, "Connection could not be established.");
}
return null;
}
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Websocket connected.");
}
// shutdown the group, if the connection is lost
connection.addConnectionListener(new IBlaubotConnectionListener() {
@Override
public void onConnectionClosed(IBlaubotConnection connection) {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Websocket connection closed, shutting down netty");
}
group.shutdownGracefully();
}
});
return connection;
} else {
if (Log.logErrorMessages()) {
Log.e(LOG_TAG, "Could not connect to web socket");
}
group.shutdownGracefully();
return null;
}
} catch (InterruptedException e) {
e.printStackTrace();
group.shutdownGracefully();
return null;
}
}
@Override
public List<String> getSupportedAcceptorTypes() {
return acceptedConnectorTypes;
}
}