Initial implementation of UDP session channels. todo: closing them when
inactive (via timeouts, since udp doesn't have sessions). Can now have 3 types of connections, TCP, UDP, and TCP+UDP
This commit is contained in:
parent
803f9c5fdb
commit
f4e94f2562
|
@ -35,10 +35,7 @@ import dorkbox.util.OS;
|
|||
import dorkbox.util.exceptions.SecurityException;
|
||||
import io.netty.bootstrap.Bootstrap;
|
||||
import io.netty.buffer.PooledByteBufAllocator;
|
||||
import io.netty.channel.ChannelOption;
|
||||
import io.netty.channel.DefaultEventLoopGroup;
|
||||
import io.netty.channel.EventLoopGroup;
|
||||
import io.netty.channel.WriteBufferWaterMark;
|
||||
import io.netty.channel.*;
|
||||
import io.netty.channel.epoll.EpollDatagramChannel;
|
||||
import io.netty.channel.epoll.EpollEventLoopGroup;
|
||||
import io.netty.channel.epoll.EpollSocketChannel;
|
||||
|
@ -181,8 +178,7 @@ class Client<C extends Connection> extends EndPointClient implements Connection
|
|||
.option(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(WRITE_BUFF_LOW, WRITE_BUFF_HIGH))
|
||||
.remoteAddress(config.host, config.tcpPort)
|
||||
.handler(new RegistrationRemoteHandlerClientTCP(threadName,
|
||||
registrationWrapper,
|
||||
serializationManager));
|
||||
registrationWrapper));
|
||||
|
||||
// android screws up on this!!
|
||||
tcpBootstrap.option(ChannelOption.TCP_NODELAY, !OS.isAndroid())
|
||||
|
@ -210,14 +206,16 @@ class Client<C extends Connection> extends EndPointClient implements Connection
|
|||
udpBootstrap.channel(NioDatagramChannel.class);
|
||||
}
|
||||
|
||||
|
||||
udpBootstrap.group(boss)
|
||||
.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
|
||||
// Netty4 has a default of 2048 bytes as upper limit for datagram packets.
|
||||
.option(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(EndPoint.udpMaxSize))
|
||||
.option(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(WRITE_BUFF_LOW, WRITE_BUFF_HIGH))
|
||||
.localAddress(new InetSocketAddress(0)) // bind to wildcard
|
||||
.remoteAddress(new InetSocketAddress(config.host, config.udpPort))
|
||||
.handler(new RegistrationRemoteHandlerClientUDP(threadName,
|
||||
registrationWrapper,
|
||||
serializationManager));
|
||||
registrationWrapper));
|
||||
|
||||
// Enable to READ and WRITE MULTICAST data (ie, 192.168.1.0)
|
||||
// in order to WRITE: write as normal, just make sure it ends in .255
|
||||
|
|
|
@ -28,24 +28,14 @@ import dorkbox.util.NamedThreadFactory;
|
|||
import dorkbox.util.OS;
|
||||
import dorkbox.util.Property;
|
||||
import dorkbox.util.exceptions.SecurityException;
|
||||
import io.netty.bootstrap.Bootstrap;
|
||||
import io.netty.bootstrap.ServerBootstrap;
|
||||
import io.netty.buffer.PooledByteBufAllocator;
|
||||
import io.netty.channel.*;
|
||||
import io.netty.channel.epoll.EpollDatagramChannel;
|
||||
import io.netty.channel.epoll.EpollEventLoopGroup;
|
||||
import io.netty.channel.epoll.EpollServerSocketChannel;
|
||||
import io.netty.channel.kqueue.KQueueDatagramChannel;
|
||||
import io.netty.channel.kqueue.KQueueEventLoopGroup;
|
||||
import io.netty.channel.kqueue.KQueueServerSocketChannel;
|
||||
import io.netty.channel.local.LocalAddress;
|
||||
import io.netty.channel.local.LocalServerChannel;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import io.netty.channel.oio.OioEventLoopGroup;
|
||||
import io.netty.channel.socket.nio.NioDatagramChannel;
|
||||
import io.netty.channel.socket.nio.NioServerDatagramChannel;
|
||||
import io.netty.channel.socket.nio.NioServerSocketChannel;
|
||||
import io.netty.channel.socket.oio.OioDatagramChannel;
|
||||
import io.netty.channel.socket.oio.OioServerSocketChannel;
|
||||
|
||||
/**
|
||||
* The server can only be accessed in an ASYNC manner. This means that the server can only be used in RESPONSE to events. If you access the
|
||||
|
@ -74,7 +64,7 @@ class Server<C extends Connection> extends EndPointServer {
|
|||
|
||||
private final ServerBootstrap localBootstrap;
|
||||
private final ServerBootstrap tcpBootstrap;
|
||||
private final Bootstrap udpBootstrap;
|
||||
private final ServerBootstrap udpBootstrap;
|
||||
|
||||
private final int tcpPort;
|
||||
private final int udpPort;
|
||||
|
@ -135,7 +125,7 @@ class Server<C extends Connection> extends EndPointServer {
|
|||
}
|
||||
|
||||
if (udpPort > 0) {
|
||||
udpBootstrap = new Bootstrap();
|
||||
udpBootstrap = new ServerBootstrap();
|
||||
}
|
||||
else {
|
||||
udpBootstrap = null;
|
||||
|
@ -148,25 +138,25 @@ class Server<C extends Connection> extends EndPointServer {
|
|||
final EventLoopGroup boss;
|
||||
final EventLoopGroup worker;
|
||||
|
||||
if (OS.isAndroid()) {
|
||||
// android ONLY supports OIO (not NIO)
|
||||
boss = new OioEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName + "-boss", threadGroup));
|
||||
worker = new OioEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName, threadGroup));
|
||||
}
|
||||
else if (OS.isLinux() && NativeLibrary.isAvailable()) {
|
||||
// JNI network stack is MUCH faster (but only on linux)
|
||||
boss = new EpollEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName + "-boss", threadGroup));
|
||||
worker = new EpollEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName, threadGroup));
|
||||
}
|
||||
else if (OS.isMacOsX() && NativeLibrary.isAvailable()) {
|
||||
// KQueue network stack is MUCH faster (but only on macosx)
|
||||
boss = new KQueueEventLoopGroup(EndPoint.DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName + "-boss", threadGroup));
|
||||
worker = new KQueueEventLoopGroup(EndPoint.DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName, threadGroup));
|
||||
}
|
||||
else {
|
||||
// if (OS.isAndroid()) {
|
||||
// // android ONLY supports OIO (not NIO)
|
||||
// boss = new OioEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName + "-boss", threadGroup));
|
||||
// worker = new OioEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName, threadGroup));
|
||||
// }
|
||||
// else if (OS.isLinux() && NativeLibrary.isAvailable()) {
|
||||
// // JNI network stack is MUCH faster (but only on linux)
|
||||
// boss = new EpollEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName + "-boss", threadGroup));
|
||||
// worker = new EpollEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName, threadGroup));
|
||||
// }
|
||||
// else if (OS.isMacOsX() && NativeLibrary.isAvailable()) {
|
||||
// // KQueue network stack is MUCH faster (but only on macosx)
|
||||
// boss = new KQueueEventLoopGroup(EndPoint.DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName + "-boss", threadGroup));
|
||||
// worker = new KQueueEventLoopGroup(EndPoint.DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName, threadGroup));
|
||||
// }
|
||||
// else {
|
||||
boss = new NioEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName + "-boss", threadGroup));
|
||||
worker = new NioEventLoopGroup(DEFAULT_THREAD_POOL_SIZE, new NamedThreadFactory(threadName, threadGroup));
|
||||
}
|
||||
// }
|
||||
|
||||
manageForShutdown(boss);
|
||||
manageForShutdown(worker);
|
||||
|
@ -195,21 +185,21 @@ class Server<C extends Connection> extends EndPointServer {
|
|||
}
|
||||
|
||||
if (tcpBootstrap != null) {
|
||||
if (OS.isAndroid()) {
|
||||
// android ONLY supports OIO (not NIO)
|
||||
tcpBootstrap.channel(OioServerSocketChannel.class);
|
||||
}
|
||||
else if (OS.isLinux() && NativeLibrary.isAvailable()) {
|
||||
// JNI network stack is MUCH faster (but only on linux)
|
||||
tcpBootstrap.channel(EpollServerSocketChannel.class);
|
||||
}
|
||||
else if (OS.isMacOsX() && NativeLibrary.isAvailable()) {
|
||||
// KQueue network stack is MUCH faster (but only on macosx)
|
||||
tcpBootstrap.channel(KQueueServerSocketChannel.class);
|
||||
}
|
||||
else {
|
||||
// if (OS.isAndroid()) {
|
||||
// // android ONLY supports OIO (not NIO)
|
||||
// tcpBootstrap.channel(OioServerSocketChannel.class);
|
||||
// }
|
||||
// else if (OS.isLinux() && NativeLibrary.isAvailable()) {
|
||||
// // JNI network stack is MUCH faster (but only on linux)
|
||||
// tcpBootstrap.channel(EpollServerSocketChannel.class);
|
||||
// }
|
||||
// else if (OS.isMacOsX() && NativeLibrary.isAvailable()) {
|
||||
// // KQueue network stack is MUCH faster (but only on macosx)
|
||||
// tcpBootstrap.channel(KQueueServerSocketChannel.class);
|
||||
// }
|
||||
// else {
|
||||
tcpBootstrap.channel(NioServerSocketChannel.class);
|
||||
}
|
||||
// }
|
||||
|
||||
// TODO: If we use netty for an HTTP server,
|
||||
// Beside the usual ChannelOptions the Native Transport allows to enable TCP_CORK which may come in handy if you implement a HTTP Server.
|
||||
|
@ -217,15 +207,15 @@ class Server<C extends Connection> extends EndPointServer {
|
|||
tcpBootstrap.group(boss, worker)
|
||||
.option(ChannelOption.SO_BACKLOG, backlogConnectionCount)
|
||||
.option(ChannelOption.SO_REUSEADDR, true)
|
||||
.option(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(WRITE_BUFF_LOW, WRITE_BUFF_HIGH))
|
||||
|
||||
.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
|
||||
.childOption(ChannelOption.SO_KEEPALIVE, true)
|
||||
.option(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(WRITE_BUFF_LOW, WRITE_BUFF_HIGH))
|
||||
.childHandler(new RegistrationRemoteHandlerServerTCP(threadName,
|
||||
registrationWrapper,
|
||||
serializationManager));
|
||||
registrationWrapper));
|
||||
|
||||
// have to check options.host for null. we don't bind to 0.0.0.0, we bind to "null" to get the "any" address!
|
||||
if (hostName.equals("0.0.0.0")) {
|
||||
if (!hostName.equals("0.0.0.0")) {
|
||||
tcpBootstrap.localAddress(hostName, tcpPort);
|
||||
}
|
||||
else {
|
||||
|
@ -240,33 +230,49 @@ class Server<C extends Connection> extends EndPointServer {
|
|||
|
||||
|
||||
if (udpBootstrap != null) {
|
||||
if (OS.isAndroid()) {
|
||||
// android ONLY supports OIO (not NIO)
|
||||
udpBootstrap.channel(OioDatagramChannel.class);
|
||||
}
|
||||
else if (OS.isLinux() && NativeLibrary.isAvailable()) {
|
||||
// JNI network stack is MUCH faster (but only on linux)
|
||||
udpBootstrap.channel(EpollDatagramChannel.class);
|
||||
}
|
||||
else if (OS.isMacOsX() && NativeLibrary.isAvailable()) {
|
||||
// KQueue network stack is MUCH faster (but only on macosx)
|
||||
udpBootstrap.channel(KQueueDatagramChannel.class);
|
||||
}
|
||||
else {
|
||||
// windows
|
||||
udpBootstrap.channel(NioDatagramChannel.class);
|
||||
}
|
||||
// if (OS.isAndroid()) {
|
||||
// // android ONLY supports OIO (not NIO)
|
||||
// udpBootstrap.channel(OioDatagramChannel.class);
|
||||
// }
|
||||
// else if (OS.isLinux() && NativeLibrary.isAvailable()) {
|
||||
// // JNI network stack is MUCH faster (but only on linux)
|
||||
// udpBootstrap.channel(EpollDatagramChannel.class);
|
||||
// }
|
||||
// else if (OS.isMacOsX() && NativeLibrary.isAvailable()) {
|
||||
// // KQueue network stack is MUCH faster (but only on macosx)
|
||||
// udpBootstrap.channel(KQueueDatagramChannel.class);
|
||||
// }
|
||||
// else {
|
||||
// windows and linux/mac that are incompatible with the native implementations
|
||||
// udpBootstrap.channel(NioDatagramChannel.class);
|
||||
// }
|
||||
udpBootstrap.channel(NioServerDatagramChannel.class);
|
||||
|
||||
|
||||
// Netty4 has a default of 2048 bytes as upper limit for datagram packets, we want this to be whatever we specify
|
||||
FixedRecvByteBufAllocator recvByteBufAllocator = new FixedRecvByteBufAllocator(EndPoint.udpMaxSize);
|
||||
udpBootstrap.group(worker)
|
||||
.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
|
||||
.option(ChannelOption.RCVBUF_ALLOCATOR, recvByteBufAllocator)
|
||||
.option(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(WRITE_BUFF_LOW, WRITE_BUFF_HIGH))
|
||||
|
||||
// not binding to specific address, since it's driven by TCP, and that can be bound to a specific address
|
||||
.localAddress(udpPort) // if you bind to a specific interface, Linux will be unable to receive broadcast packets!
|
||||
.handler(new RegistrationRemoteHandlerServerUDP(threadName,
|
||||
registrationWrapper,
|
||||
serializationManager));
|
||||
// a non-root user can't receive a broadcast packet on *nix if the socket is bound on non-wildcard address.
|
||||
// TODO: move broadcast to it's own handler, and have UDP server be able to be bound to a specific IP
|
||||
// OF NOTE: At the end in my case I decided to bind to .255 broadcast address on Linux systems. (to receive broadcast packets)
|
||||
.localAddress(udpPort) // if you bind to a specific interface, Linux will be unable to receive broadcast packets! see: http://developerweb.net/viewtopic.php?id=5722
|
||||
|
||||
.childHandler(new RegistrationRemoteHandlerServerUDP(threadName,
|
||||
registrationWrapper));
|
||||
|
||||
|
||||
|
||||
// // have to check options.host for null. we don't bind to 0.0.0.0, we bind to "null" to get the "any" address!
|
||||
// if (hostName.equals("0.0.0.0")) {
|
||||
// udpBootstrap.localAddress(hostName, tcpPort);
|
||||
// }
|
||||
// else {
|
||||
// udpBootstrap.localAddress(udpPort);
|
||||
// }
|
||||
|
||||
// Enable to READ from MULTICAST data (ie, 192.168.1.0)
|
||||
// in order to WRITE: write as normal, just make sure it ends in .255
|
||||
|
@ -319,13 +325,11 @@ class Server<C extends Connection> extends EndPointServer {
|
|||
future = localBootstrap.bind();
|
||||
future.await();
|
||||
} catch (InterruptedException e) {
|
||||
String errorMessage = stopWithErrorMessage(logger, "Could not bind to LOCAL address on the server.", e);
|
||||
throw new IllegalArgumentException(errorMessage);
|
||||
throw new IllegalArgumentException("Could not bind to LOCAL address '" + localChannelName + "' on the server.", e);
|
||||
}
|
||||
|
||||
if (!future.isSuccess()) {
|
||||
String errorMessage = stopWithErrorMessage(logger, "Could not bind to LOCAL address on the server.", future.cause());
|
||||
throw new IllegalArgumentException(errorMessage);
|
||||
throw new IllegalArgumentException("Could not bind to LOCAL address '" + localChannelName + "' on the server.", future.cause());
|
||||
}
|
||||
|
||||
logger.info("Listening on LOCAL address: '{}'", localChannelName);
|
||||
|
@ -340,19 +344,13 @@ class Server<C extends Connection> extends EndPointServer {
|
|||
future = tcpBootstrap.bind();
|
||||
future.await();
|
||||
} catch (Exception e) {
|
||||
String errorMessage = stopWithErrorMessage(logger,
|
||||
"Could not bind to address " + hostName + " TCP port " + tcpPort +
|
||||
" on the server.",
|
||||
e);
|
||||
throw new IllegalArgumentException(errorMessage);
|
||||
stop();
|
||||
throw new IllegalArgumentException("Could not bind to address " + hostName + " TCP port " + tcpPort + " on the server.", e);
|
||||
}
|
||||
|
||||
if (!future.isSuccess()) {
|
||||
String errorMessage = stopWithErrorMessage(logger,
|
||||
"Could not bind to address " + hostName + " TCP port " + tcpPort +
|
||||
" on the server.",
|
||||
future.cause());
|
||||
throw new IllegalArgumentException(errorMessage);
|
||||
stop();
|
||||
throw new IllegalArgumentException("Could not bind to address " + hostName + " TCP port " + tcpPort + " on the server.", future.cause());
|
||||
}
|
||||
|
||||
logger.info("Listening on address {} at TCP port: {}", hostName, tcpPort);
|
||||
|
@ -366,19 +364,12 @@ class Server<C extends Connection> extends EndPointServer {
|
|||
future = udpBootstrap.bind();
|
||||
future.await();
|
||||
} catch (Exception e) {
|
||||
String errorMessage = stopWithErrorMessage(logger,
|
||||
"Could not bind to address " + hostName + " UDP port " + udpPort +
|
||||
" on the server.",
|
||||
e);
|
||||
throw new IllegalArgumentException(errorMessage);
|
||||
throw new IllegalArgumentException("Could not bind to address " + hostName + " UDP port " + udpPort + " on the server.", e);
|
||||
}
|
||||
|
||||
if (!future.isSuccess()) {
|
||||
String errorMessage = stopWithErrorMessage(logger,
|
||||
"Could not bind to address " + hostName + " UDP port " + udpPort +
|
||||
" on the server.",
|
||||
throw new IllegalArgumentException("Could not bind to address " + hostName + " UDP port " + udpPort + " on the server.",
|
||||
future.cause());
|
||||
throw new IllegalArgumentException(errorMessage);
|
||||
}
|
||||
|
||||
logger.info("Listening on address {} at UDP port: {}", hostName, udpPort);
|
||||
|
|
|
@ -51,6 +51,7 @@ import io.netty.channel.ChannelInboundHandlerAdapter;
|
|||
import io.netty.channel.epoll.EpollDatagramChannel;
|
||||
import io.netty.channel.epoll.EpollSocketChannel;
|
||||
import io.netty.channel.local.LocalChannel;
|
||||
import io.netty.channel.socket.nio.DatagramSessionChannel;
|
||||
import io.netty.channel.socket.nio.NioDatagramChannel;
|
||||
import io.netty.channel.socket.nio.NioSocketChannel;
|
||||
import io.netty.handler.timeout.IdleState;
|
||||
|
@ -73,7 +74,7 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn
|
|||
|
||||
public static
|
||||
boolean isUdp(Class<? extends Channel> channelClass) {
|
||||
return channelClass == NioDatagramChannel.class || channelClass == EpollDatagramChannel.class;
|
||||
return channelClass == NioDatagramChannel.class || channelClass == EpollDatagramChannel.class || channelClass == DatagramSessionChannel.class;
|
||||
}
|
||||
|
||||
public static
|
||||
|
@ -261,7 +262,7 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn
|
|||
@Override
|
||||
public
|
||||
String idAsHex() {
|
||||
return Integer.toHexString(this.channelWrapper.id());
|
||||
return Integer.toHexString(id());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -287,8 +288,16 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn
|
|||
pingFuture2.cancel();
|
||||
}
|
||||
|
||||
Promise<PingTuple<? extends Connection>> newPromise = this.channelWrapper.getEventLoop()
|
||||
Promise<PingTuple<? extends Connection>> newPromise;
|
||||
if (this.channelWrapper.udp() != null) {
|
||||
newPromise = this.channelWrapper.udp()
|
||||
.newPromise();
|
||||
}
|
||||
else {
|
||||
newPromise = this.channelWrapper.tcp()
|
||||
.newPromise();
|
||||
}
|
||||
|
||||
this.pingFuture = new PingFuture(newPromise);
|
||||
|
||||
PingMessage ping = new PingMessage();
|
||||
|
@ -305,11 +314,15 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn
|
|||
public final
|
||||
void ping0(PingMessage ping) {
|
||||
if (this.channelWrapper.udp() != null) {
|
||||
UDP(ping).flush();
|
||||
UDP(ping);
|
||||
}
|
||||
else if (this.channelWrapper.tcp() != null) {
|
||||
TCP(ping);
|
||||
}
|
||||
else {
|
||||
TCP(ping).flush();
|
||||
self(ping);
|
||||
}
|
||||
flush();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -337,8 +350,8 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn
|
|||
|
||||
@Override
|
||||
public
|
||||
void channelWritabilityChanged(final ChannelHandlerContext ctx) throws Exception {
|
||||
super.channelWritabilityChanged(ctx);
|
||||
void channelWritabilityChanged(final ChannelHandlerContext context) throws Exception {
|
||||
super.channelWritabilityChanged(context);
|
||||
|
||||
// needed to place back-pressure when writing too much data to the connection
|
||||
if (writeSignalNeeded.getAndSet(false)) {
|
||||
|
@ -354,7 +367,7 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn
|
|||
*
|
||||
* This blocks until we are writable again
|
||||
*/
|
||||
private
|
||||
final
|
||||
void controlBackPressure(ConnectionPoint c) {
|
||||
while (!closeInProgress.get() && !c.isWritable()) {
|
||||
needsLock.set(true);
|
||||
|
@ -389,86 +402,30 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn
|
|||
@Override
|
||||
public final
|
||||
void self(Object message) {
|
||||
Logger logger2 = this.logger;
|
||||
if (logger2.isTraceEnabled()) {
|
||||
logger2.trace("Sending LOCAL {}", message);
|
||||
}
|
||||
logger.trace("Sending LOCAL {}", message);
|
||||
this.sessionManager.onMessage(this, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the object over the network using TCP. (LOCAL channels do not care if its TCP or UDP)
|
||||
*/
|
||||
final
|
||||
ConnectionPoint TCP_backpressure(Object message) {
|
||||
Logger logger2 = this.logger;
|
||||
if (!this.closeInProgress.get()) {
|
||||
if (logger2.isTraceEnabled()) {
|
||||
logger2.trace("Sending TCP {}", message);
|
||||
}
|
||||
ConnectionPointWriter tcp = this.channelWrapper.tcp();
|
||||
// needed to place back-pressure when writing too much data to the connection. Will create deadlocks if called from
|
||||
// INSIDE the event loop
|
||||
controlBackPressure(tcp);
|
||||
|
||||
tcp.write(message);
|
||||
return tcp;
|
||||
}
|
||||
else {
|
||||
if (logger2.isDebugEnabled()) {
|
||||
logger2.debug("writing TCP while closed: {}", message);
|
||||
}
|
||||
// we have to return something, otherwise dependent code will throw a null pointer exception
|
||||
return ChannelNull.get();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the object over the network using TCP. (LOCAL channels do not care if its TCP or UDP)
|
||||
*/
|
||||
@Override
|
||||
public final
|
||||
ConnectionPoint TCP(Object message) {
|
||||
Logger logger2 = this.logger;
|
||||
if (!this.closeInProgress.get()) {
|
||||
if (logger2.isTraceEnabled()) {
|
||||
logger2.trace("Sending TCP {}", message);
|
||||
}
|
||||
ConnectionPointWriter tcp = this.channelWrapper.tcp();
|
||||
if (!closeInProgress.get()) {
|
||||
logger.trace("Sending TCP {}", message);
|
||||
|
||||
ConnectionPoint tcp = this.channelWrapper.tcp();
|
||||
try {
|
||||
tcp.write(message);
|
||||
} catch (Exception e) {
|
||||
logger.error("Unable to write TCP object {}", message.getClass());
|
||||
}
|
||||
return tcp;
|
||||
}
|
||||
else {
|
||||
if (logger2.isDebugEnabled()) {
|
||||
logger2.debug("writing TCP while closed: {}", message);
|
||||
}
|
||||
// we have to return something, otherwise dependent code will throw a null pointer exception
|
||||
return ChannelNull.get();
|
||||
}
|
||||
}
|
||||
logger.debug("writing TCP while closed: {}", message);
|
||||
|
||||
/**
|
||||
* Sends the object over the network using UDP (LOCAL channels do not care if its TCP or UDP)
|
||||
*/
|
||||
final
|
||||
ConnectionPoint UDP_backpressure(Object message) {
|
||||
Logger logger2 = this.logger;
|
||||
if (!this.closeInProgress.get()) {
|
||||
if (logger2.isTraceEnabled()) {
|
||||
logger2.trace("Sending UDP {}", message);
|
||||
}
|
||||
ConnectionPointWriter udp = this.channelWrapper.udp();
|
||||
// needed to place back-pressure when writing too much data to the connection. Will create deadlocks if called from
|
||||
// INSIDE the event loop
|
||||
controlBackPressure(udp);
|
||||
|
||||
udp.write(message);
|
||||
return udp;
|
||||
}
|
||||
else {
|
||||
if (logger2.isDebugEnabled()) {
|
||||
logger2.debug("writing UDP while closed: {}", message);
|
||||
}
|
||||
// we have to return something, otherwise dependent code will throw a null pointer exception
|
||||
return ChannelNull.get();
|
||||
}
|
||||
|
@ -480,19 +437,19 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn
|
|||
@Override
|
||||
public
|
||||
ConnectionPoint UDP(Object message) {
|
||||
Logger logger2 = this.logger;
|
||||
if (!this.closeInProgress.get()) {
|
||||
if (logger2.isTraceEnabled()) {
|
||||
logger2.trace("Sending UDP {}", message);
|
||||
}
|
||||
ConnectionPointWriter udp = this.channelWrapper.udp();
|
||||
if (!closeInProgress.get()) {
|
||||
logger.trace("Sending UDP {}", message);
|
||||
|
||||
ConnectionPoint udp = this.channelWrapper.udp();
|
||||
try {
|
||||
udp.write(message);
|
||||
} catch (Exception e) {
|
||||
logger.error("Unable to write TCP object {}", message.getClass());
|
||||
}
|
||||
return udp;
|
||||
}
|
||||
else {
|
||||
if (logger2.isDebugEnabled()) {
|
||||
logger2.debug("writing UDP while closed: {}", message);
|
||||
}
|
||||
logger.debug("writing UDP while closed: {}", message);
|
||||
// we have to return something, otherwise dependent code will throw a null pointer exception
|
||||
return ChannelNull.get();
|
||||
}
|
||||
|
@ -539,6 +496,7 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn
|
|||
// } else
|
||||
if (event instanceof IdleStateEvent) {
|
||||
if (((IdleStateEvent) event).state() == IdleState.ALL_IDLE) {
|
||||
// will auto-flush if necessary
|
||||
this.sessionManager.onIdle(this);
|
||||
}
|
||||
}
|
||||
|
@ -559,6 +517,7 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn
|
|||
// delay close until it's finished.
|
||||
this.messageInProgress.set(true);
|
||||
|
||||
// will auto-flush if necessary
|
||||
this.sessionManager.onMessage(this, object);
|
||||
|
||||
this.messageInProgress.set(false);
|
||||
|
@ -626,6 +585,7 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn
|
|||
if (isTCP || isLocal) {
|
||||
// this is because channelInactive can ONLY happen when netty shuts down the channel.
|
||||
// and connection.close() can be called by the user.
|
||||
// will auto-flush if necessary
|
||||
this.sessionManager.onDisconnected(this);
|
||||
|
||||
// close TCP/UDP together!
|
||||
|
@ -977,7 +937,8 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn
|
|||
// have to wait for the object to be created + ID to be assigned on the remote system BEFORE we can create the proxy instance here.
|
||||
|
||||
// this means we are creating a NEW object on the server, bound access to only this connection
|
||||
TCP(message).flush();
|
||||
TCP(message);
|
||||
flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1003,7 +964,8 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn
|
|||
// have to wait for the object to be created + ID to be assigned on the remote system BEFORE we can create the proxy instance here.
|
||||
|
||||
// this means we are creating a NEW object on the server, bound access to only this connection
|
||||
TCP(message).flush();
|
||||
TCP(message);
|
||||
flush();
|
||||
}
|
||||
|
||||
|
||||
|
@ -1048,11 +1010,8 @@ class ConnectionImpl extends ChannelInboundHandlerAdapter implements ICryptoConn
|
|||
*/
|
||||
public
|
||||
void removeRmiListeners(final int objectID, final Listener listener) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* For network connections, the interface class kryo ID == implementation class kryo ID, so they switch automatically.
|
||||
* For local connections, we have to switch it appropriately in the LocalRmiProxy
|
||||
|
|
|
@ -15,6 +15,8 @@
|
|||
*/
|
||||
package dorkbox.network.connection;
|
||||
|
||||
import io.netty.util.concurrent.Promise;
|
||||
|
||||
public
|
||||
interface ConnectionPoint {
|
||||
|
||||
|
@ -24,7 +26,12 @@ interface ConnectionPoint {
|
|||
boolean isWritable();
|
||||
|
||||
/**
|
||||
* Flushes the contents of the TCP/UDP/etc pipes to the wire.
|
||||
* Writes data to the pipe. <b>DOES NOT FLUSH</b> the pipe to the wire!
|
||||
*/
|
||||
void flush();
|
||||
void write(Object object) throws Exception;
|
||||
|
||||
/**
|
||||
* Creates a new promise associated with this connection type
|
||||
*/
|
||||
<V> Promise<V> newPromise();
|
||||
}
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* 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 dorkbox.network.connection;
|
||||
|
||||
public
|
||||
interface ConnectionPointWriter extends ConnectionPoint {
|
||||
|
||||
/**
|
||||
* Writes data to the pipe. <b>DOES NOT FLUSH</b> the pipe to the wire!
|
||||
*/
|
||||
void write(Object object);
|
||||
}
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
package dorkbox.network.connection;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
|
@ -32,8 +33,6 @@ import dorkbox.network.connection.registration.MetaChannel;
|
|||
import dorkbox.network.connection.wrapper.ChannelLocalWrapper;
|
||||
import dorkbox.network.connection.wrapper.ChannelNetworkWrapper;
|
||||
import dorkbox.network.connection.wrapper.ChannelWrapper;
|
||||
import dorkbox.network.pipeline.KryoEncoder;
|
||||
import dorkbox.network.pipeline.KryoEncoderCrypto;
|
||||
import dorkbox.network.rmi.RmiBridge;
|
||||
import dorkbox.network.rmi.RmiObjectHandler;
|
||||
import dorkbox.network.rmi.RmiObjectLocalHandler;
|
||||
|
@ -151,9 +150,7 @@ class EndPoint extends Shutdownable {
|
|||
// to expose to external code. "this" escaping can be ignored, because it is benign.
|
||||
//noinspection ThisEscapedInObjectConstruction
|
||||
registrationWrapper = new RegistrationWrapper(this,
|
||||
logger,
|
||||
new KryoEncoder(serializationManager),
|
||||
new KryoEncoderCrypto(serializationManager));
|
||||
logger);
|
||||
|
||||
|
||||
// we have to be able to specify WHAT property store we want to use, since it can change!
|
||||
|
@ -214,7 +211,7 @@ class EndPoint extends Shutdownable {
|
|||
|
||||
// we don't care about un-instantiated/constructed members, since the class type is the only interest.
|
||||
//noinspection unchecked
|
||||
connectionManager = new ConnectionManager(type.getSimpleName(), connection0(null).getClass());
|
||||
connectionManager = new ConnectionManager(type.getSimpleName(), connection0(null, null).getClass());
|
||||
|
||||
// add the ping listener (internal use only!)
|
||||
connectionManager.add(new PingSystemListener());
|
||||
|
@ -259,13 +256,22 @@ class EndPoint extends Shutdownable {
|
|||
return (S) propertyStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal call by the pipeline to check if the client has more protocol registrations to complete.
|
||||
*
|
||||
* @return true if there are more registrations to process, false if we are 100% done with all types to register (TCP/UDP/etc)
|
||||
*/
|
||||
protected
|
||||
boolean hasMoreRegistrations() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal call by the pipeline to notify the client to continue registering the different session protocols.
|
||||
* The server does not use this.
|
||||
*/
|
||||
protected
|
||||
boolean registerNextProtocol0() {
|
||||
return true;
|
||||
void startNextProtocolRegistration() {
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -326,9 +332,10 @@ class EndPoint extends Shutdownable {
|
|||
* - when determining the baseClass for listeners
|
||||
*
|
||||
* @param metaChannel can be NULL (when getting the baseClass)
|
||||
* @param remoteAddress be NULL (when getting the baseClass or when creating a local channel)
|
||||
*/
|
||||
protected final
|
||||
Connection connection0(MetaChannel metaChannel) {
|
||||
Connection connection0(final MetaChannel metaChannel, final InetSocketAddress remoteAddress) {
|
||||
ConnectionImpl connection;
|
||||
|
||||
RmiBridge rmiBridge = null;
|
||||
|
@ -354,16 +361,11 @@ class EndPoint extends Shutdownable {
|
|||
}
|
||||
}
|
||||
else {
|
||||
RmiObjectHandler rmiObjectHandler = rmiHandler;
|
||||
if (rmiEnabled) {
|
||||
rmiObjectHandler = networkRmiHandler;
|
||||
}
|
||||
|
||||
if (this instanceof EndPointServer) {
|
||||
wrapper = new ChannelNetworkWrapper(metaChannel, registrationWrapper, rmiObjectHandler);
|
||||
wrapper = new ChannelNetworkWrapper(metaChannel, remoteAddress, networkRmiHandler);
|
||||
}
|
||||
else {
|
||||
wrapper = new ChannelNetworkWrapper(metaChannel, null, rmiObjectHandler);
|
||||
wrapper = new ChannelNetworkWrapper(metaChannel, remoteAddress, rmiHandler);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -433,7 +435,7 @@ class EndPoint extends Shutdownable {
|
|||
connectionManager.closeConnections(shouldKeepListeners);
|
||||
|
||||
// Sometimes there might be "lingering" connections (ie, halfway though registration) that need to be closed.
|
||||
registrationWrapper.closeChannels(maxShutdownWaitTimeInMilliSeconds);
|
||||
registrationWrapper.clearSessions();
|
||||
|
||||
isConnected.set(false);
|
||||
}
|
||||
|
@ -516,7 +518,19 @@ class EndPoint extends Shutdownable {
|
|||
*/
|
||||
public
|
||||
<T> int createGlobalObject(final T globalObject) {
|
||||
int globalObjectId = globalRmiBridge.register(globalObject);
|
||||
return globalObjectId;
|
||||
return globalRmiBridge.register(globalObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a previously created "global" RMI object
|
||||
*
|
||||
* @param objectRmiId the ID of the RMI object to get
|
||||
*
|
||||
* @return null if the object doesn't exist or the ID is invalid.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public
|
||||
<T> T getGlobalObject(final int objectRmiId) {
|
||||
return (T) globalRmiBridge.getRegisteredObject(objectRmiId);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -77,12 +77,6 @@ class EndPointClient extends EndPoint {
|
|||
}
|
||||
}
|
||||
|
||||
// protected by bootstrapLock
|
||||
private
|
||||
boolean isRegistrationComplete() {
|
||||
return !bootstrapIterator.hasNext();
|
||||
}
|
||||
|
||||
// this is called by 2 threads. The startup thread, and the registration-in-progress thread
|
||||
private void doRegistration() {
|
||||
synchronized (bootstrapLock) {
|
||||
|
@ -116,12 +110,12 @@ class EndPointClient extends EndPoint {
|
|||
|
||||
if (!future.isSuccess()) {
|
||||
Throwable cause = future.cause();
|
||||
// extra space here is so it aligns with "Connecting to server:"
|
||||
String errorMessage = "Connection refused :" + bootstrapWrapper.address + " at " + bootstrapWrapper.type + " port: " + bootstrapWrapper.port;
|
||||
|
||||
if (cause instanceof java.net.ConnectException) {
|
||||
if (cause.getMessage()
|
||||
.contains("refused")) {
|
||||
// extra space here is so it aligns with "Connecting to server:"
|
||||
logger.error(errorMessage);
|
||||
}
|
||||
|
||||
|
@ -147,21 +141,27 @@ class EndPointClient extends EndPoint {
|
|||
*/
|
||||
@Override
|
||||
protected
|
||||
boolean registerNextProtocol0() {
|
||||
boolean registrationComplete;
|
||||
void startNextProtocolRegistration() {
|
||||
logger.trace("Registered protocol from server.");
|
||||
|
||||
synchronized (bootstrapLock) {
|
||||
registrationComplete = isRegistrationComplete();
|
||||
if (!registrationComplete) {
|
||||
if (hasMoreRegistrations()) {
|
||||
doRegistration();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.trace("Registered protocol from server.");
|
||||
|
||||
// only let us continue with connections (this starts up the client/server implementations) once ALL of the
|
||||
// bootstraps have connected
|
||||
return registrationComplete;
|
||||
/**
|
||||
* Internal call by the pipeline to check if the client has more protocol registrations to complete.
|
||||
*
|
||||
* @return true if there are more registrations to process, false if we are 100% done with all types to register (TCP/UDP/etc)
|
||||
*/
|
||||
@Override
|
||||
protected
|
||||
boolean hasMoreRegistrations() {
|
||||
synchronized (bootstrapLock) {
|
||||
return !(bootstrapIterator == null || !bootstrapIterator.hasNext());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -182,16 +182,25 @@ class EndPointClient extends EndPoint {
|
|||
@Override
|
||||
public
|
||||
ConnectionPoint TCP(Object message) {
|
||||
ConnectionPoint tcp = connection.TCP_backpressure(message);
|
||||
tcp.flush();
|
||||
ConnectionPoint tcp = connection.TCP(message);
|
||||
flush();
|
||||
|
||||
// needed to place back-pressure when writing too much data to the connection. Will create deadlocks if called from
|
||||
// INSIDE the event loop
|
||||
connection.controlBackPressure(tcp);
|
||||
|
||||
return tcp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
ConnectionPoint UDP(Object message) {
|
||||
ConnectionPoint udp = connection.UDP_backpressure(message);
|
||||
udp.flush();
|
||||
ConnectionPoint udp = connection.UDP(message);
|
||||
flush();
|
||||
|
||||
// needed to place back-pressure when writing too much data to the connection. Will create deadlocks if called from
|
||||
// INSIDE the event loop
|
||||
connection.controlBackPressure(udp);
|
||||
return udp;
|
||||
}
|
||||
|
||||
|
@ -215,6 +224,9 @@ class EndPointClient extends EndPoint {
|
|||
|
||||
synchronized (bootstrapLock) {
|
||||
// we're done with registration, so no need to keep this around
|
||||
if (bootstrapIterator.hasNext()) {
|
||||
System.err.println("WHAT");
|
||||
}
|
||||
bootstrapIterator = null;
|
||||
registration.countDown();
|
||||
}
|
||||
|
@ -224,8 +236,9 @@ class EndPointClient extends EndPoint {
|
|||
super.connectionConnected0(connection);
|
||||
}
|
||||
|
||||
|
||||
private
|
||||
void registrationCompleted() {
|
||||
void stopRegistration() {
|
||||
// make sure we're not waiting on registration
|
||||
synchronized (bootstrapLock) {
|
||||
// we're done with registration, so no need to keep this around
|
||||
|
@ -256,14 +269,13 @@ class EndPointClient extends EndPoint {
|
|||
// Only keep the listeners for connections IF we are the client. If we remove listeners as a client,
|
||||
// ALL of the client logic will be lost. The server is reactive, so listeners are added to connections as needed (instead of before startup)
|
||||
closeConnections(true);
|
||||
|
||||
// make sure we're not waiting on registration
|
||||
registrationCompleted();
|
||||
|
||||
// for the CLIENT only, we clear these connections! (the server only clears them on shutdown)
|
||||
shutdownChannels();
|
||||
|
||||
connection = null;
|
||||
|
||||
// make sure we're not waiting on registration
|
||||
stopRegistration();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -271,6 +283,6 @@ class EndPointClient extends EndPoint {
|
|||
*/
|
||||
void abortRegistration() {
|
||||
// make sure we're not waiting on registration
|
||||
registrationCompleted();
|
||||
stopRegistration();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,24 +20,32 @@ import java.util.Collection;
|
|||
public
|
||||
interface ISessionManager {
|
||||
/**
|
||||
* Called when a message is received
|
||||
* Called when a message is received.
|
||||
* <p>
|
||||
* Will auto-flush the connection queue if necessary.
|
||||
*/
|
||||
void onMessage(ConnectionImpl connection, Object message);
|
||||
|
||||
/**
|
||||
* Called when the connection has been idle (read & write) for 2 seconds
|
||||
* Called when the connection has been idle (read & write) for 2 seconds.
|
||||
* <p>
|
||||
* Will auto-flush the connection queue if necessary.
|
||||
*/
|
||||
void onIdle(Connection connection);
|
||||
void onIdle(ConnectionImpl connection);
|
||||
|
||||
/**
|
||||
* Invoked when a Channel is open, bound to a local address, and connected to a remote address.
|
||||
* <p>
|
||||
* Will auto-flush the connection queue if necessary.
|
||||
*/
|
||||
void onConnected(Connection connection);
|
||||
void onConnected(ConnectionImpl connection);
|
||||
|
||||
/**
|
||||
* Invoked when a Channel was disconnected from its remote peer.
|
||||
* <p>
|
||||
* Will auto-flush the connection queue if necessary.
|
||||
*/
|
||||
void onDisconnected(Connection connection);
|
||||
void onDisconnected(ConnectionImpl connection);
|
||||
|
||||
/**
|
||||
* Returns a non-modifiable list of active connections. This is extremely slow, and not recommended!
|
||||
|
|
|
@ -25,6 +25,7 @@ import com.esotericsoftware.kryo.Kryo;
|
|||
|
||||
import dorkbox.network.pipeline.ByteBufInput;
|
||||
import dorkbox.network.pipeline.ByteBufOutput;
|
||||
import dorkbox.network.pipeline.MagicBytes;
|
||||
import dorkbox.network.serialization.CryptoSerializationManager;
|
||||
import dorkbox.util.bytes.BigEndian;
|
||||
import dorkbox.util.bytes.OptimizeUtilsByteArray;
|
||||
|
@ -40,23 +41,6 @@ import net.jpountz.lz4.LZ4FastDecompressor;
|
|||
*/
|
||||
public
|
||||
class KryoExtra<C extends ICryptoConnection> extends Kryo {
|
||||
/**
|
||||
* bit masks
|
||||
*/
|
||||
static final byte crypto = (byte) (1 << 1);
|
||||
|
||||
/**
|
||||
* Determines if this buffer is encrypted or not.
|
||||
*/
|
||||
public static
|
||||
boolean isEncrypted(final ByteBuf buffer) {
|
||||
// read off the magic byte
|
||||
byte magicByte = buffer.getByte(buffer.readerIndex());
|
||||
return (magicByte & crypto) == crypto;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// snappycomp : 7.534 micros/op; 518.5 MB/s (output: 55.1%)
|
||||
// snappyuncomp : 1.391 micros/op; 2808.1 MB/s
|
||||
// lz4comp : 6.210 micros/op; 629.0 MB/s (output: 55.4%)
|
||||
|
@ -111,7 +95,6 @@ class KryoExtra<C extends ICryptoConnection> extends Kryo {
|
|||
this.connection = null;
|
||||
|
||||
// during INIT and handshake, we don't use connection encryption/compression
|
||||
// magic byte
|
||||
buffer.writeByte(0);
|
||||
|
||||
// write the object to the NORMAL output buffer!
|
||||
|
@ -125,7 +108,6 @@ class KryoExtra<C extends ICryptoConnection> extends Kryo {
|
|||
// connection will always be NULL during connection initialization
|
||||
this.connection = null;
|
||||
|
||||
|
||||
////////////////
|
||||
// Note: we CANNOT write BACK to the buffer as "temp" storage, since there could be additional data on it!
|
||||
////////////////
|
||||
|
@ -224,11 +206,11 @@ class KryoExtra<C extends ICryptoConnection> extends Kryo {
|
|||
inputOffset = maxLengthLengthOffset - lengthLength;
|
||||
|
||||
|
||||
// now write the ORIGINAL (uncompressed) length to the front of the byte array. This is so we can use the FAST decompress version
|
||||
// now write the ORIGINAL (uncompressed) length to the front of the byte array (this is NOT THE BUFFER!). This is so we can use the FAST decompress version
|
||||
OptimizeUtilsByteArray.writeInt(inputArray, length, true, inputOffset);
|
||||
|
||||
// write out the "magic" byte.
|
||||
buffer.writeByte(crypto);
|
||||
buffer.writeByte(MagicBytes.crypto);
|
||||
|
||||
// have to copy over the orig data, because we used the temp buffer. Also have to account for the length of the uncompressed size
|
||||
buffer.writeBytes(inputArray, inputOffset, compressedLength + lengthLength);
|
||||
|
@ -451,7 +433,7 @@ class KryoExtra<C extends ICryptoConnection> extends Kryo {
|
|||
}
|
||||
|
||||
// write out the "magic" byte.
|
||||
buffer.writeByte(crypto);
|
||||
buffer.writeByte(MagicBytes.crypto);
|
||||
|
||||
// write out our GCM counter
|
||||
OptimizeUtilsByteBuf.writeLong(buffer, nextGcmSequence, true);
|
||||
|
|
|
@ -15,111 +15,73 @@
|
|||
*/
|
||||
package dorkbox.network.connection;
|
||||
|
||||
import static dorkbox.network.connection.registration.remote.RegistrationRemoteHandler.checkEqual;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import org.bouncycastle.crypto.CipherParameters;
|
||||
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import com.esotericsoftware.kryo.util.ObjectMap;
|
||||
|
||||
import dorkbox.network.connection.registration.MetaChannel;
|
||||
import dorkbox.network.connection.registration.remote.RegistrationRemoteHandler;
|
||||
import dorkbox.network.pipeline.KryoEncoder;
|
||||
import dorkbox.network.pipeline.KryoEncoderCrypto;
|
||||
import dorkbox.network.pipeline.tcp.KryoEncoder;
|
||||
import dorkbox.network.pipeline.tcp.KryoEncoderCrypto;
|
||||
import dorkbox.network.pipeline.udp.KryoDecoderUdp;
|
||||
import dorkbox.network.pipeline.udp.KryoDecoderUdpCrypto;
|
||||
import dorkbox.network.pipeline.udp.KryoEncoderUdp;
|
||||
import dorkbox.network.pipeline.udp.KryoEncoderUdpCrypto;
|
||||
import dorkbox.network.serialization.CryptoSerializationManager;
|
||||
import dorkbox.util.RandomUtil;
|
||||
import dorkbox.util.collections.IntMap;
|
||||
import dorkbox.util.collections.IntMap.Values;
|
||||
import dorkbox.util.collections.LockFreeIntMap;
|
||||
import dorkbox.util.crypto.CryptoECC;
|
||||
import dorkbox.util.exceptions.SecurityException;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
|
||||
/**
|
||||
* Just wraps common/needed methods of the client/server endpoint by the registration stage/handshake.
|
||||
* <p/>
|
||||
* This is in the connection package, so it can access the endpoint methods that it needs to (without having to publicly expose them)
|
||||
* This is in the connection package, so it can access the endpoint methods that it needs to without having to publicly expose them
|
||||
*/
|
||||
public
|
||||
class RegistrationWrapper implements UdpServer {
|
||||
class RegistrationWrapper {
|
||||
private final org.slf4j.Logger logger;
|
||||
|
||||
private final KryoEncoder kryoEncoder;
|
||||
private final KryoEncoderCrypto kryoEncoderCrypto;
|
||||
public final KryoEncoder kryoTcpEncoder;
|
||||
public final KryoEncoderCrypto kryoTcpEncoderCrypto;
|
||||
|
||||
private final EndPoint endPointConnection;
|
||||
public final KryoEncoderUdp kryoUdpEncoder;
|
||||
public final KryoEncoderUdpCrypto kryoUdpEncoderCrypto;
|
||||
public final KryoDecoderUdp kryoUdpDecoder;
|
||||
public final KryoDecoderUdpCrypto kryoUdpDecoderCrypto;
|
||||
|
||||
// keeps track of connections (TCP/UDP-client)
|
||||
private final ReentrantLock channelMapLock = new ReentrantLock();
|
||||
private final IntMap<MetaChannel> channelMap = new IntMap<MetaChannel>();
|
||||
|
||||
// keeps track of connections (UDP-server)
|
||||
@SuppressWarnings({"FieldCanBeLocal", "unused"})
|
||||
private volatile ObjectMap<InetSocketAddress, ConnectionImpl> udpRemoteMap;
|
||||
|
||||
|
||||
// synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this
|
||||
// section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our
|
||||
// use-case 99% of the time)
|
||||
private final Object singleWriterLock1 = new Object();
|
||||
|
||||
// Recommended for best performance while adhering to the "single writer principle". Must be static-final
|
||||
private static final AtomicReferenceFieldUpdater<RegistrationWrapper, ObjectMap> udpRemoteMapREF =
|
||||
AtomicReferenceFieldUpdater.newUpdater(RegistrationWrapper.class,
|
||||
ObjectMap.class,
|
||||
"udpRemoteMap");
|
||||
private final EndPoint endPoint;
|
||||
|
||||
// keeps track of connections/sessions (TCP/UDP/Local). The session ID '0' is reserved to mean "no session ID yet"
|
||||
private final LockFreeIntMap<MetaChannel> sessionMap = new LockFreeIntMap<MetaChannel>(32, ConnectionManager.LOAD_FACTOR);
|
||||
|
||||
public
|
||||
RegistrationWrapper(final EndPoint endPointConnection,
|
||||
final Logger logger,
|
||||
final KryoEncoder kryoEncoder,
|
||||
final KryoEncoderCrypto kryoEncoderCrypto) {
|
||||
this.endPointConnection = endPointConnection;
|
||||
RegistrationWrapper(final EndPoint endPoint,
|
||||
final Logger logger) {
|
||||
this.endPoint = endPoint;
|
||||
this.logger = logger;
|
||||
this.kryoEncoder = kryoEncoder;
|
||||
this.kryoEncoderCrypto = kryoEncoderCrypto;
|
||||
|
||||
if (endPointConnection instanceof EndPointServer) {
|
||||
this.udpRemoteMap = new ObjectMap<InetSocketAddress, ConnectionImpl>(32, ConnectionManager.LOAD_FACTOR);
|
||||
}
|
||||
else {
|
||||
this.udpRemoteMap = null;
|
||||
}
|
||||
this.kryoTcpEncoder = new KryoEncoder(endPoint.serializationManager);
|
||||
this.kryoTcpEncoderCrypto = new KryoEncoderCrypto(endPoint.serializationManager);
|
||||
|
||||
|
||||
this.kryoUdpEncoder = new KryoEncoderUdp(endPoint.serializationManager);
|
||||
this.kryoUdpEncoderCrypto = new KryoEncoderUdpCrypto(endPoint.serializationManager);
|
||||
|
||||
this.kryoUdpDecoder = new KryoDecoderUdp(endPoint.serializationManager);
|
||||
this.kryoUdpDecoderCrypto = new KryoDecoderUdpCrypto(endPoint.serializationManager);
|
||||
}
|
||||
|
||||
public
|
||||
KryoEncoder getKryoEncoder() {
|
||||
return this.kryoEncoder;
|
||||
}
|
||||
|
||||
public
|
||||
KryoEncoderCrypto getKryoEncoderCrypto() {
|
||||
return this.kryoEncoderCrypto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Locks, and then returns the channelMap used by the registration process.
|
||||
* <p/>
|
||||
* Make SURE to use this in a try/finally block with releaseChannelMap in the finally block!
|
||||
*/
|
||||
private
|
||||
IntMap<MetaChannel> getAndLockChannelMap() {
|
||||
// try to lock access, also guarantees that the contents of this map are visible across threads
|
||||
this.channelMapLock.lock();
|
||||
return this.channelMap;
|
||||
}
|
||||
|
||||
private
|
||||
void releaseChannelMap() {
|
||||
// try to unlock access
|
||||
this.channelMapLock.unlock();
|
||||
CryptoSerializationManager getSerializtion() {
|
||||
return endPoint.getSerialization();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -127,18 +89,26 @@ class RegistrationWrapper implements UdpServer {
|
|||
*/
|
||||
public
|
||||
int getIdleTimeout() {
|
||||
return this.endPointConnection.getIdleTimeout();
|
||||
return this.endPoint.getIdleTimeout();
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal call by the pipeline to check if the client has more protocol registrations to complete.
|
||||
*
|
||||
* @return true if there are more registrations to process, false if we are 100% done with all types to register (TCP/UDP/etc)
|
||||
*/
|
||||
public
|
||||
boolean hasMoreRegistrations() {
|
||||
return this.endPoint.hasMoreRegistrations();
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal call by the pipeline to notify the client to continue registering the different session protocols. The server does not use
|
||||
* this.
|
||||
*
|
||||
* @return true if we are done registering bootstraps
|
||||
*/
|
||||
public
|
||||
boolean registerNextProtocol0() {
|
||||
return this.endPointConnection.registerNextProtocol0();
|
||||
void startNextProtocolRegistration() {
|
||||
this.endPoint.startNextProtocolRegistration();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -147,7 +117,7 @@ class RegistrationWrapper implements UdpServer {
|
|||
*/
|
||||
public
|
||||
void connectionConnected0(ConnectionImpl networkConnection) {
|
||||
this.endPointConnection.connectionConnected0(networkConnection);
|
||||
this.endPoint.connectionConnected0(networkConnection);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -156,45 +126,38 @@ class RegistrationWrapper implements UdpServer {
|
|||
* @param metaChannel can be NULL (when getting the baseClass)
|
||||
*/
|
||||
public
|
||||
Connection connection0(MetaChannel metaChannel) {
|
||||
return this.endPointConnection.connection0(metaChannel);
|
||||
Connection connection0(MetaChannel metaChannel, final InetSocketAddress remoteAddress) {
|
||||
return this.endPoint.connection0(metaChannel, remoteAddress);
|
||||
}
|
||||
|
||||
public
|
||||
SecureRandom getSecureRandom() {
|
||||
return this.endPointConnection.secureRandom;
|
||||
return this.endPoint.secureRandom;
|
||||
}
|
||||
|
||||
public
|
||||
ECPublicKeyParameters getPublicKey() {
|
||||
return this.endPointConnection.publicKey;
|
||||
return this.endPoint.publicKey;
|
||||
}
|
||||
|
||||
public
|
||||
CipherParameters getPrivateKey() {
|
||||
return this.endPointConnection.privateKey;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return true if the remote address public key matches the one saved
|
||||
* If the key does not match AND we have disabled remote key validation, then metachannel.changedRemoteKey = true. OTHERWISE, key validation is REQUIRED!
|
||||
*
|
||||
* @throws SecurityException
|
||||
* @return true if the remote address public key matches the one saved or we disabled remote key validation.
|
||||
*/
|
||||
public
|
||||
boolean validateRemoteAddress(final InetSocketAddress tcpRemoteServer, final ECPublicKeyParameters publicKey)
|
||||
throws SecurityException {
|
||||
|
||||
InetAddress address = tcpRemoteServer.getAddress();
|
||||
boolean validateRemoteAddress(final MetaChannel metaChannel, final InetSocketAddress remoteAddress, final ECPublicKeyParameters publicKey) {
|
||||
InetAddress address = remoteAddress.getAddress();
|
||||
byte[] hostAddress = address.getAddress();
|
||||
|
||||
ECPublicKeyParameters savedPublicKey = this.endPointConnection.propertyStore.getRegisteredServerKey(hostAddress);
|
||||
try {
|
||||
ECPublicKeyParameters savedPublicKey = this.endPoint.propertyStore.getRegisteredServerKey(hostAddress);
|
||||
Logger logger2 = this.logger;
|
||||
if (savedPublicKey == null) {
|
||||
if (logger2.isDebugEnabled()) {
|
||||
logger2.debug("Adding new remote IP address key for {}", address.getHostAddress());
|
||||
}
|
||||
this.endPointConnection.propertyStore.addRegisteredServerKey(hostAddress, publicKey);
|
||||
this.endPoint.propertyStore.addRegisteredServerKey(hostAddress, publicKey);
|
||||
}
|
||||
else {
|
||||
// COMPARE!
|
||||
|
@ -207,25 +170,29 @@ class RegistrationWrapper implements UdpServer {
|
|||
byAddress = "Unknown Address";
|
||||
}
|
||||
|
||||
if (this.endPointConnection.disableRemoteKeyValidation) {
|
||||
logger2.warn("Invalid or non-matching public key from remote server. Their public key has changed. To fix, remove entry for: {}", byAddress);
|
||||
if (this.endPoint.disableRemoteKeyValidation) {
|
||||
logger2.warn("Invalid or non-matching public key from remote server, their public key has changed. Toggling extra flag in channel to indicate key change. To fix, remove entry for: {}", byAddress);
|
||||
|
||||
metaChannel.changedRemoteKey = true;
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
// keys do not match, abort!
|
||||
logger2.error("Invalid or non-matching public key from remote server. Their public key has changed. To fix, remove entry for: {}", byAddress);
|
||||
logger2.error("Invalid or non-matching public key from remote server, their public key has changed. To fix, remove entry for: {}", byAddress);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (SecurityException e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@SuppressWarnings("AutoBoxing")
|
||||
public
|
||||
void removeRegisteredServerKey(final byte[] hostAddress) throws SecurityException {
|
||||
ECPublicKeyParameters savedPublicKey = this.endPointConnection.propertyStore.getRegisteredServerKey(hostAddress);
|
||||
ECPublicKeyParameters savedPublicKey = this.endPoint.propertyStore.getRegisteredServerKey(hostAddress);
|
||||
if (savedPublicKey != null) {
|
||||
Logger logger2 = this.logger;
|
||||
if (logger2.isDebugEnabled()) {
|
||||
|
@ -235,284 +202,146 @@ class RegistrationWrapper implements UdpServer {
|
|||
hostAddress[2],
|
||||
hostAddress[3]);
|
||||
}
|
||||
this.endPointConnection.propertyStore.removeRegisteredServerKey(hostAddress);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ONLY SERVER SIDE CALLS THIS Called when creating a connection. Only called if we have a UDP channel
|
||||
*/
|
||||
@Override
|
||||
public final
|
||||
void registerServerUDP(final MetaChannel metaChannel) {
|
||||
if (metaChannel != null && metaChannel.udpRemoteAddress != null) {
|
||||
// synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this
|
||||
// section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our
|
||||
// use-case 99% of the time)
|
||||
synchronized (singleWriterLock1) {
|
||||
// access a snapshot of the connections (single-writer-principle)
|
||||
final ObjectMap<InetSocketAddress, ConnectionImpl> udpRemoteMap = udpRemoteMapREF.get(this);
|
||||
|
||||
udpRemoteMap.put(metaChannel.udpRemoteAddress, metaChannel.connection);
|
||||
|
||||
// save this snapshot back to the original (single writer principle)
|
||||
udpRemoteMapREF.lazySet(this, udpRemoteMap);
|
||||
}
|
||||
|
||||
this.logger.info("Connected to remote UDP connection. [{} <== {}]",
|
||||
metaChannel.udpChannel.localAddress(),
|
||||
metaChannel.udpRemoteAddress);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ONLY SERVER SIDE CALLS THIS Called when closing a connection.
|
||||
*/
|
||||
@Override
|
||||
public final
|
||||
void unRegisterServerUDP(final InetSocketAddress udpRemoteAddress) {
|
||||
if (udpRemoteAddress != null) {
|
||||
// synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this
|
||||
// section. Because of this, we can have unlimited reader threads all going at the same time, without contention (which is our
|
||||
// use-case 99% of the time)
|
||||
synchronized (singleWriterLock1) {
|
||||
// access a snapshot of the connections (single-writer-principle)
|
||||
final ObjectMap<InetSocketAddress, ConnectionImpl> udpRemoteMap = udpRemoteMapREF.get(this);
|
||||
|
||||
udpRemoteMap.remove(udpRemoteAddress);
|
||||
|
||||
// save this snapshot back to the original (single writer principle)
|
||||
udpRemoteMapREF.lazySet(this, udpRemoteMap);
|
||||
}
|
||||
|
||||
logger.info("Closed remote UDP connection: {}", udpRemoteAddress);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ONLY SERVER SIDE CALLS THIS
|
||||
*/
|
||||
@Override
|
||||
public
|
||||
ConnectionImpl getServerUDP(final InetSocketAddress udpRemoteAddress) {
|
||||
if (udpRemoteAddress != null) {
|
||||
// access a snapshot of the connections (single-writer-principle)
|
||||
final ObjectMap<InetSocketAddress, ConnectionImpl> udpRemoteMap = udpRemoteMapREF.get(this);
|
||||
return udpRemoteMap.get(udpRemoteAddress);
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
this.endPoint.propertyStore.removeRegisteredServerKey(hostAddress);
|
||||
}
|
||||
}
|
||||
|
||||
public
|
||||
void abortRegistrationIfClient() {
|
||||
if (this.endPointConnection instanceof EndPointClient) {
|
||||
((EndPointClient) this.endPointConnection).abortRegistration();
|
||||
if (this.endPoint instanceof EndPointClient) {
|
||||
((EndPointClient) this.endPoint).abortRegistration();
|
||||
}
|
||||
}
|
||||
|
||||
public
|
||||
void addChannel(final int channelHashCodeOrId, final MetaChannel metaChannel) {
|
||||
try {
|
||||
IntMap<MetaChannel> channelMap = this.getAndLockChannelMap();
|
||||
channelMap.put(channelHashCodeOrId, metaChannel);
|
||||
} finally {
|
||||
this.releaseChannelMap();
|
||||
boolean isClient() {
|
||||
return (this.endPoint instanceof EndPointClient);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public
|
||||
MetaChannel removeChannel(final int channelHashCodeOrId) {
|
||||
try {
|
||||
IntMap<MetaChannel> channelMap = getAndLockChannelMap();
|
||||
return channelMap.remove(channelHashCodeOrId);
|
||||
} finally {
|
||||
releaseChannelMap();
|
||||
}
|
||||
}
|
||||
|
||||
public
|
||||
MetaChannel getChannel(final int channelHashCodeOrId) {
|
||||
try {
|
||||
IntMap<MetaChannel> channelMap = getAndLockChannelMap();
|
||||
return channelMap.get(channelHashCodeOrId);
|
||||
} finally {
|
||||
releaseChannelMap();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Closes all connections ONLY (keeps the server/client running).
|
||||
*
|
||||
* @param maxShutdownWaitTimeInMilliSeconds
|
||||
* The amount of time in milli-seconds to wait for this endpoint to close all {@link Channel}s and shutdown gracefully.
|
||||
* MetaChannel allow access to the same "session" across TCP/UDP/etc
|
||||
* <p>
|
||||
* The connection ID '0' is reserved to mean "no channel ID yet"
|
||||
*/
|
||||
public
|
||||
void closeChannels(final long maxShutdownWaitTimeInMilliSeconds) {
|
||||
try {
|
||||
IntMap<MetaChannel> channelMap = getAndLockChannelMap();
|
||||
IntMap.Entries<MetaChannel> entries = channelMap.entries();
|
||||
while (entries.hasNext()) {
|
||||
MetaChannel metaChannel = entries.next().value;
|
||||
metaChannel.close(maxShutdownWaitTimeInMilliSeconds);
|
||||
Thread.yield();
|
||||
}
|
||||
MetaChannel createSessionClient(int sessionId) {
|
||||
MetaChannel metaChannel = new MetaChannel(sessionId);
|
||||
sessionMap.put(sessionId, metaChannel);
|
||||
|
||||
channelMap.clear();
|
||||
|
||||
} finally {
|
||||
releaseChannelMap();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the specified connections ONLY (keeps the server/client running).
|
||||
*
|
||||
* @param maxShutdownWaitTimeInMilliSeconds
|
||||
* The amount of time in milli-seconds to wait for this endpoint to close all {@link Channel}s and shutdown gracefully.
|
||||
*/
|
||||
public
|
||||
MetaChannel closeChannel(final Channel channel, final long maxShutdownWaitTimeInMilliSeconds) {
|
||||
try {
|
||||
IntMap<MetaChannel> channelMap = getAndLockChannelMap();
|
||||
IntMap.Entries<MetaChannel> entries = channelMap.entries();
|
||||
while (entries.hasNext()) {
|
||||
MetaChannel metaChannel = entries.next().value;
|
||||
|
||||
if (metaChannel.localChannel == channel ||
|
||||
metaChannel.tcpChannel == channel ||
|
||||
metaChannel.udpChannel == channel) {
|
||||
|
||||
entries.remove();
|
||||
metaChannel.close(maxShutdownWaitTimeInMilliSeconds);
|
||||
return metaChannel;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
releaseChannelMap();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* now that we are CONNECTED, we want to remove ourselves (and channel ID's) from the map.
|
||||
* they will be ADDED in another map, in the followup handler!!
|
||||
* MetaChannel allow access to the same "session" across TCP/UDP/etc.
|
||||
* <p>
|
||||
* The connection ID '0' is reserved to mean "no channel ID yet"
|
||||
*/
|
||||
public
|
||||
boolean setupChannels(final RegistrationRemoteHandler handler, final MetaChannel metaChannel) {
|
||||
boolean registerServer = false;
|
||||
|
||||
try {
|
||||
IntMap<MetaChannel> channelMap = getAndLockChannelMap();
|
||||
|
||||
channelMap.remove(metaChannel.tcpChannel.hashCode());
|
||||
channelMap.remove(metaChannel.connectionID);
|
||||
|
||||
|
||||
ChannelPipeline pipeline = metaChannel.tcpChannel.pipeline();
|
||||
// The TCP channel is what calls this method, so we can use "this" for TCP, and the others are handled during the registration process
|
||||
pipeline.remove(handler);
|
||||
|
||||
if (metaChannel.udpChannel != null) {
|
||||
// the setup is different between CLIENT / SERVER
|
||||
if (metaChannel.udpRemoteAddress == null) {
|
||||
// CLIENT RUNS THIS
|
||||
// don't want to muck with the SERVER udp pipeline, as it NEVER CHANGES.
|
||||
// More specifically, the UDP SERVER doesn't use a channelMap, it uses the udpRemoteMap
|
||||
// to keep track of UDP connections. This is very different than how the client works
|
||||
// only the client will have the udp remote address
|
||||
channelMap.remove(metaChannel.udpChannel.hashCode());
|
||||
}
|
||||
else {
|
||||
// SERVER RUNS THIS
|
||||
// don't ALWAYS have UDP on SERVER...
|
||||
registerServer = true;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
releaseChannelMap();
|
||||
MetaChannel createSessionServer() {
|
||||
int sessionId = RandomUtil.int_();
|
||||
while (sessionId == 0 && sessionMap.containsKey(sessionId)) {
|
||||
sessionId = RandomUtil.int_();
|
||||
}
|
||||
|
||||
return registerServer;
|
||||
}
|
||||
|
||||
public
|
||||
Integer initializeChannel(final MetaChannel metaChannel) {
|
||||
Integer connectionID = RandomUtil.int_();
|
||||
try {
|
||||
IntMap<MetaChannel> channelMap = getAndLockChannelMap();
|
||||
while (channelMap.containsKey(connectionID)) {
|
||||
connectionID = RandomUtil.int_();
|
||||
}
|
||||
|
||||
metaChannel.connectionID = connectionID;
|
||||
channelMap.put(connectionID, metaChannel);
|
||||
|
||||
} finally {
|
||||
releaseChannelMap();
|
||||
}
|
||||
|
||||
return connectionID;
|
||||
}
|
||||
|
||||
public
|
||||
boolean associateChannels(final Channel channel, final InetAddress remoteAddress) {
|
||||
boolean success = false;
|
||||
|
||||
try {
|
||||
IntMap<MetaChannel> channelMap = getAndLockChannelMap();
|
||||
IntMap.Entries<MetaChannel> entries = channelMap.entries();
|
||||
while (entries.hasNext()) {
|
||||
MetaChannel metaChannel = entries.next().value;
|
||||
|
||||
// associate TCP and UDP!
|
||||
final InetSocketAddress inetSocketAddress = (InetSocketAddress) metaChannel.tcpChannel.remoteAddress();
|
||||
InetAddress tcpRemoteServer = inetSocketAddress.getAddress();
|
||||
if (checkEqual(tcpRemoteServer, remoteAddress)) {
|
||||
channelMap.put(channel.hashCode(), metaChannel);
|
||||
metaChannel.udpChannel = channel;
|
||||
success = true;
|
||||
// only allow one server per registration!
|
||||
break;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
releaseChannelMap();
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
public
|
||||
MetaChannel getAssociatedChannel_UDP(final InetAddress remoteAddress) {
|
||||
try {
|
||||
MetaChannel metaChannel;
|
||||
IntMap<MetaChannel> channelMap = getAndLockChannelMap();
|
||||
IntMap.Entries<MetaChannel> entries = channelMap.entries();
|
||||
synchronized (sessionMap) {
|
||||
// one final check, but slower...
|
||||
while (sessionId == 0 && sessionMap.containsKey(sessionId)) {
|
||||
sessionId = RandomUtil.int_();
|
||||
}
|
||||
|
||||
while (entries.hasNext()) {
|
||||
metaChannel = entries.next().value;
|
||||
metaChannel = new MetaChannel(sessionId);
|
||||
sessionMap.put(sessionId, metaChannel);
|
||||
|
||||
// only look at connections that do not have UDP already setup.
|
||||
if (metaChannel.udpChannel == null) {
|
||||
InetSocketAddress tcpRemote = (InetSocketAddress) metaChannel.tcpChannel.remoteAddress();
|
||||
InetAddress tcpRemoteAddress = tcpRemote.getAddress();
|
||||
|
||||
if (RegistrationRemoteHandler.checkEqual(tcpRemoteAddress, remoteAddress)) {
|
||||
// TODO: clean out sessions that are stale!
|
||||
}
|
||||
|
||||
return metaChannel;
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
releaseChannelMap();
|
||||
|
||||
/**
|
||||
* For UDP, this map "exists forever" because we have to look up each session on inbound coms
|
||||
* <p>
|
||||
* The session ID '0' is reserved to mean "no session ID yet"
|
||||
*/
|
||||
public
|
||||
MetaChannel getSession(final int sessionId) {
|
||||
return sessionMap.get(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the first session we have available. This is for the CLIENT to track sessions (between TCP/UDP) to a server
|
||||
*/
|
||||
public MetaChannel getFirstSession() {
|
||||
Values<MetaChannel> values = sessionMap.values();
|
||||
if (values.hasNext) {
|
||||
return values.next();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The SERVER AND CLIENT will stop tracking a session once the session is complete.
|
||||
*/
|
||||
public
|
||||
void removeSession(final MetaChannel metaChannel) {
|
||||
int sessionId = metaChannel.sessionId;
|
||||
if (sessionId != 0) {
|
||||
sessionMap.remove(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The SERVER will stop tracking a session if there are errors
|
||||
*/
|
||||
public
|
||||
void closeSession(final int sessionId) {
|
||||
if (sessionId != 0) {
|
||||
MetaChannel metaChannel = sessionMap.remove(sessionId);
|
||||
if (metaChannel != null) {
|
||||
if (metaChannel.tcpChannel != null && metaChannel.tcpChannel.isOpen()) {
|
||||
metaChannel.tcpChannel.close();
|
||||
}
|
||||
if (metaChannel.udpChannel != null && metaChannel.udpChannel.isOpen()) {
|
||||
metaChannel.udpChannel.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all session associations (keeps the server/client running).
|
||||
*/
|
||||
public
|
||||
void clearSessions() {
|
||||
List<Channel> channels = new LinkedList<Channel>();
|
||||
|
||||
synchronized (sessionMap) {
|
||||
Values<MetaChannel> values = sessionMap.values();
|
||||
for (MetaChannel metaChannel : values) {
|
||||
if (metaChannel.tcpChannel != null && metaChannel.tcpChannel.isOpen()) {
|
||||
channels.add(metaChannel.tcpChannel);
|
||||
}
|
||||
if (metaChannel.udpChannel != null && metaChannel.udpChannel.isOpen()) {
|
||||
channels.add(metaChannel.udpChannel);
|
||||
}
|
||||
}
|
||||
|
||||
// remote all session associations. Any session in progress will have to restart it's registration process
|
||||
sessionMap.clear();
|
||||
}
|
||||
|
||||
// close all "in progress" registrations as well
|
||||
for (Channel channel : channels) {
|
||||
channel.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* 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 dorkbox.network.connection;
|
||||
|
||||
import dorkbox.network.connection.registration.MetaChannel;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
public
|
||||
interface UdpServer {
|
||||
/**
|
||||
* Called when creating a connection.
|
||||
*/
|
||||
void registerServerUDP(MetaChannel metaChannel);
|
||||
|
||||
/**
|
||||
* Called when closing a connection.
|
||||
*/
|
||||
void unRegisterServerUDP(InetSocketAddress udpRemoteAddress);
|
||||
|
||||
/**
|
||||
* @return the connection for a remote UDP address
|
||||
*/
|
||||
ConnectionImpl getServerUDP(InetSocketAddress udpRemoteAddress);
|
||||
}
|
|
@ -15,7 +15,7 @@
|
|||
*/
|
||||
package dorkbox.network.connection.registration;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
|
||||
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
|
||||
|
@ -23,29 +23,25 @@ import org.bouncycastle.crypto.params.ECPublicKeyParameters;
|
|||
import dorkbox.network.connection.ConnectionImpl;
|
||||
import io.netty.channel.Channel;
|
||||
|
||||
// @formatter:off
|
||||
public
|
||||
class MetaChannel {
|
||||
// @formatter:off
|
||||
|
||||
// how long between receiving data over TCP. This is used to determine how long to wait before notifying the APP,
|
||||
// so the registration message has time to arrive to the other endpoint.
|
||||
private volatile long nanoSecBetweenTCP = 0L;
|
||||
private AtomicLong nanoSecRoundTrip = new AtomicLong();
|
||||
|
||||
// used to keep track and associate TCP/UDP/etc sessions. This is always defined by the server
|
||||
// a sessionId if '0', means we are still figuring it out.
|
||||
public int sessionId;
|
||||
|
||||
public Integer connectionID = null; // only used during the registration process
|
||||
public Channel localChannel = null; // only available for local "in jvm" channels. XOR with tcp/udp channels with CLIENT.
|
||||
public Channel tcpChannel = null;
|
||||
|
||||
// channel here (on server or socket.bind connections) doesn't have the remote address available.
|
||||
// It is apart of the inbound message, however.
|
||||
// ALSO not necessary to close it, since the server handles that.
|
||||
public Channel udpChannel = null;
|
||||
public InetSocketAddress udpRemoteAddress = null; // SERVER ONLY. needed to be aware of the remote address to send UDP replies to
|
||||
|
||||
public ConnectionImpl connection; // only needed until the connection has been notified.
|
||||
|
||||
public ECPublicKeyParameters publicKey; // used for ECC crypto + handshake on NETWORK (remote) connections. This is the remote public key.
|
||||
|
||||
public AsymmetricCipherKeyPair ecdhKey; // used for ECC Diffie-Hellman-Merkle key exchanges: see http://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange
|
||||
|
||||
// since we are using AES-GCM, the aesIV here **MUST** be exactly 12 bytes
|
||||
|
@ -57,6 +53,13 @@ class MetaChannel {
|
|||
// If the server detects this, it has the option for additional security (two-factor auth, perhaps?)
|
||||
public boolean changedRemoteKey = false;
|
||||
|
||||
// @formatter:on
|
||||
|
||||
public
|
||||
MetaChannel(final int sessionId) {
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
|
||||
public
|
||||
void close() {
|
||||
if (this.localChannel != null) {
|
||||
|
@ -67,9 +70,16 @@ class MetaChannel {
|
|||
this.tcpChannel.close();
|
||||
}
|
||||
|
||||
// only the CLIENT will have this.
|
||||
if (this.udpChannel != null && this.udpRemoteAddress == null) {
|
||||
if (this.udpChannel != null) {
|
||||
// if (this.udpRemoteAddress == null) {
|
||||
// FIXME: ?? only the CLIENT will have this.
|
||||
this.udpChannel.close();
|
||||
// }
|
||||
// else if (this.handlerServerUDP != null) {
|
||||
// only the SERVER will have this
|
||||
// we DO NOT want to close the UDP channel, otherwise no other UDP clients can connect
|
||||
// this.handlerServerUDP.unRegisterServerUDP(this.udpRemoteAddress);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -84,23 +94,33 @@ class MetaChannel {
|
|||
.awaitUninterruptibly(maxShutdownWaitTimeInMilliSeconds);
|
||||
}
|
||||
|
||||
// only the CLIENT will have this.
|
||||
if (this.udpChannel != null && this.udpRemoteAddress == null && this.udpChannel.isOpen()) {
|
||||
if (this.udpChannel != null && this.udpChannel.isOpen()) {
|
||||
// if (this.udpRemoteAddress == null) {
|
||||
// FIXME: ?? only the CLIENT will have this.
|
||||
this.udpChannel.close()
|
||||
.awaitUninterruptibly(maxShutdownWaitTimeInMilliSeconds);
|
||||
// }
|
||||
// else {
|
||||
// only the SERVER will have this
|
||||
// we DO NOT want to close the UDP channel, otherwise no other UDP clients can connect
|
||||
// this.handlerServerUDP.unRegisterServerUDP(this.udpRemoteAddress);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the TCP round trip time. Make sure to REFRESH this every time you SEND TCP data!!
|
||||
* Update the network round trip time.
|
||||
*/
|
||||
public
|
||||
void updateTcpRoundTripTime() {
|
||||
this.nanoSecBetweenTCP = System.nanoTime() - this.nanoSecBetweenTCP;
|
||||
void updateRoundTripOnWrite() {
|
||||
this.nanoSecRoundTrip.set(System.nanoTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the difference in time from the last write
|
||||
*/
|
||||
public
|
||||
long getNanoSecBetweenTCP() {
|
||||
return this.nanoSecBetweenTCP;
|
||||
long getRoundTripTime() {
|
||||
return System.nanoTime() - this.nanoSecRoundTrip.get();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,10 +23,25 @@ import org.bouncycastle.crypto.params.IESParameters;
|
|||
*/
|
||||
public
|
||||
class Registration {
|
||||
// used to keep track and associate TCP/UDP/etc sessions. This is always defined by the server
|
||||
// a sessionId if '0', means we are still figuring it out.
|
||||
public int sessionID;
|
||||
|
||||
public ECPublicKeyParameters publicKey;
|
||||
public IESParameters eccParameters;
|
||||
public byte[] aesKey;
|
||||
public byte[] aesIV;
|
||||
|
||||
public byte[] payload;
|
||||
|
||||
// true if we have more registrations to process, false if we are done
|
||||
public boolean hasMore;
|
||||
|
||||
private
|
||||
Registration() {
|
||||
// for serialization
|
||||
}
|
||||
|
||||
public
|
||||
Registration(final int sessionID) {
|
||||
this.sessionID = sessionID;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
*/
|
||||
package dorkbox.network.connection.registration;
|
||||
|
||||
import dorkbox.network.connection.EndPoint;
|
||||
import dorkbox.network.connection.RegistrationWrapper;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandler.Sharable;
|
||||
|
@ -31,7 +30,6 @@ class RegistrationHandler extends ChannelInboundHandlerAdapter {
|
|||
protected final org.slf4j.Logger logger;
|
||||
protected final String name;
|
||||
|
||||
|
||||
public
|
||||
RegistrationHandler(final String name, RegistrationWrapper registrationWrapper) {
|
||||
this.name = name;
|
||||
|
@ -83,23 +81,20 @@ class RegistrationHandler extends ChannelInboundHandlerAdapter {
|
|||
public abstract
|
||||
void exceptionCaught(final ChannelHandlerContext context, final Throwable cause) throws Exception;
|
||||
|
||||
public
|
||||
MetaChannel shutdown(final RegistrationWrapper registrationWrapper, final Channel channel) {
|
||||
// shutdown. Something messed up or was incorrect
|
||||
/**
|
||||
* shutdown. Something messed up or was incorrect
|
||||
*/
|
||||
protected final
|
||||
void shutdown(final Channel channel, final int sessionId) {
|
||||
// properly shutdown the TCP/UDP channels.
|
||||
if (channel.isOpen()) {
|
||||
if (sessionId == 0 && channel.isOpen()) {
|
||||
channel.close();
|
||||
}
|
||||
|
||||
// also, once we notify, we unregister this.
|
||||
if (registrationWrapper != null) {
|
||||
MetaChannel metaChannel = registrationWrapper.closeChannel(channel, EndPoint.maxShutdownWaitTimeInMilliSeconds);
|
||||
registrationWrapper.abortRegistrationIfClient();
|
||||
|
||||
return metaChannel;
|
||||
registrationWrapper.closeSession(sessionId);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -15,16 +15,16 @@
|
|||
*/
|
||||
package dorkbox.network.connection.registration.local;
|
||||
|
||||
import static dorkbox.network.connection.EndPoint.maxShutdownWaitTimeInMilliSeconds;
|
||||
|
||||
import dorkbox.network.connection.RegistrationWrapper;
|
||||
import dorkbox.network.connection.registration.MetaChannel;
|
||||
import dorkbox.network.connection.registration.RegistrationHandler;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.util.AttributeKey;
|
||||
|
||||
public abstract
|
||||
class RegistrationLocalHandler extends RegistrationHandler {
|
||||
public static final AttributeKey<MetaChannel> META_CHANNEL = AttributeKey.valueOf(RegistrationLocalHandler.class, "MetaChannel.local");
|
||||
|
||||
RegistrationLocalHandler(String name, RegistrationWrapper registrationWrapper) {
|
||||
super(name, registrationWrapper);
|
||||
|
@ -36,14 +36,15 @@ class RegistrationLocalHandler extends RegistrationHandler {
|
|||
@Override
|
||||
protected
|
||||
void initChannel(Channel channel) {
|
||||
MetaChannel metaChannel = new MetaChannel();
|
||||
MetaChannel metaChannel = registrationWrapper.createSessionServer();
|
||||
metaChannel.localChannel = channel;
|
||||
|
||||
registrationWrapper.addChannel(channel.hashCode(), metaChannel);
|
||||
channel.attr(META_CHANNEL)
|
||||
.set(metaChannel);
|
||||
|
||||
logger.trace("New LOCAL connection.");
|
||||
|
||||
registrationWrapper.connection0(metaChannel);
|
||||
registrationWrapper.connection0(metaChannel, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -74,9 +75,6 @@ class RegistrationLocalHandler extends RegistrationHandler {
|
|||
|
||||
logger.info("Closed LOCAL connection: {}", channel.remoteAddress());
|
||||
|
||||
// also, once we notify, we unregister this.
|
||||
registrationWrapper.closeChannel(channel, maxShutdownWaitTimeInMilliSeconds);
|
||||
|
||||
super.channelInactive(context);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@ class RegistrationLocalHandlerClient extends RegistrationLocalHandler {
|
|||
channel.remoteAddress());
|
||||
|
||||
// client starts the registration process
|
||||
channel.writeAndFlush(new Registration());
|
||||
channel.writeAndFlush(new Registration(0));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -64,8 +64,8 @@ class RegistrationLocalHandlerClient extends RegistrationLocalHandler {
|
|||
ReferenceCountUtil.release(message);
|
||||
|
||||
Channel channel = context.channel();
|
||||
|
||||
MetaChannel metaChannel = this.registrationWrapper.removeChannel(channel.hashCode());
|
||||
MetaChannel metaChannel = channel.attr(META_CHANNEL)
|
||||
.getAndSet(null);
|
||||
|
||||
// have to setup new listeners
|
||||
if (metaChannel != null) {
|
||||
|
@ -73,7 +73,7 @@ class RegistrationLocalHandlerClient extends RegistrationLocalHandler {
|
|||
pipeline.remove(this);
|
||||
|
||||
// Event though a local channel is XOR with everything else, we still have to make the client clean up it's state.
|
||||
registrationWrapper.registerNextProtocol0();
|
||||
registrationWrapper.startNextProtocolRegistration();
|
||||
|
||||
ConnectionImpl connection = metaChannel.connection;
|
||||
|
||||
|
@ -85,7 +85,7 @@ class RegistrationLocalHandlerClient extends RegistrationLocalHandler {
|
|||
else {
|
||||
// this should NEVER happen!
|
||||
logger.error("Error registering LOCAL channel! MetaChannel is null!");
|
||||
shutdown(registrationWrapper, channel);
|
||||
shutdown(channel, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,11 +70,10 @@ class RegistrationLocalHandlerServer extends RegistrationLocalHandler {
|
|||
ReferenceCountUtil.release(message);
|
||||
logger.trace("Sent registration");
|
||||
|
||||
ConnectionImpl connection = null;
|
||||
MetaChannel metaChannel = registrationWrapper.removeChannel(channel.hashCode());
|
||||
MetaChannel metaChannel = channel.attr(META_CHANNEL)
|
||||
.getAndSet(null);
|
||||
if (metaChannel != null) {
|
||||
connection = metaChannel.connection;
|
||||
}
|
||||
ConnectionImpl connection = metaChannel.connection;
|
||||
|
||||
if (connection != null) {
|
||||
// have to setup connection handler
|
||||
|
@ -83,5 +82,6 @@ class RegistrationLocalHandlerServer extends RegistrationLocalHandler {
|
|||
registrationWrapper.connectionConnected0(connection);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -15,37 +15,33 @@
|
|||
*/
|
||||
package dorkbox.network.connection.registration.remote;
|
||||
|
||||
import static dorkbox.network.connection.EndPoint.maxShutdownWaitTimeInMilliSeconds;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.bouncycastle.crypto.engines.AESFastEngine;
|
||||
import org.bouncycastle.crypto.engines.IESEngine;
|
||||
import org.bouncycastle.crypto.modes.GCMBlockCipher;
|
||||
import org.slf4j.Logger;
|
||||
import org.bouncycastle.jce.ECNamedCurveTable;
|
||||
import org.bouncycastle.jce.spec.ECParameterSpec;
|
||||
|
||||
import dorkbox.network.connection.ConnectionImpl;
|
||||
import dorkbox.network.connection.RegistrationWrapper;
|
||||
import dorkbox.network.connection.registration.MetaChannel;
|
||||
import dorkbox.network.connection.registration.Registration;
|
||||
import dorkbox.network.connection.registration.RegistrationHandler;
|
||||
import dorkbox.network.pipeline.KryoDecoder;
|
||||
import dorkbox.network.pipeline.KryoDecoderCrypto;
|
||||
import dorkbox.network.pipeline.udp.KryoDecoderUdpCrypto;
|
||||
import dorkbox.network.pipeline.udp.KryoEncoderUdpCrypto;
|
||||
import dorkbox.network.pipeline.tcp.KryoDecoder;
|
||||
import dorkbox.network.pipeline.tcp.KryoDecoderCrypto;
|
||||
import dorkbox.network.serialization.CryptoSerializationManager;
|
||||
import dorkbox.util.FastThreadLocal;
|
||||
import dorkbox.util.crypto.CryptoECC;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import io.netty.handler.timeout.IdleState;
|
||||
import io.netty.handler.timeout.IdleStateEvent;
|
||||
import io.netty.handler.timeout.IdleStateHandler;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
|
||||
public abstract
|
||||
class RegistrationRemoteHandler extends RegistrationHandler {
|
||||
static final String DELETE_IP = "eleteIP"; // purposefully missing the "D", since that is a system parameter, which starts with "-D"
|
||||
static final ECParameterSpec eccSpec = ECNamedCurveTable.getParameterSpec(CryptoECC.curve25519);
|
||||
|
||||
static final String KRYO_ENCODER = "kryoEncoder";
|
||||
static final String KRYO_DECODER = "kryoDecoder";
|
||||
|
||||
|
@ -61,45 +57,12 @@ class RegistrationRemoteHandler extends RegistrationHandler {
|
|||
|
||||
private static final String IDLE_HANDLER = "idleHandler";
|
||||
|
||||
static final
|
||||
FastThreadLocal<GCMBlockCipher> aesEngine = new FastThreadLocal<GCMBlockCipher>() {
|
||||
@Override
|
||||
public
|
||||
GCMBlockCipher initialValue() {
|
||||
return new GCMBlockCipher(new AESFastEngine());
|
||||
}
|
||||
};
|
||||
|
||||
final
|
||||
FastThreadLocal<IESEngine> eccEngineLocal = new FastThreadLocal<IESEngine>() {
|
||||
@Override
|
||||
public
|
||||
IESEngine initialValue() {
|
||||
return CryptoECC.createEngine();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check to verify if two InetAddresses are equal, by comparing the underlying byte arrays.
|
||||
*/
|
||||
public static
|
||||
boolean checkEqual(InetAddress serverA, InetAddress serverB) {
|
||||
//noinspection SimplifiableIfStatement
|
||||
if (serverA == null || serverB == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Arrays.equals(serverA.getAddress(), serverB.getAddress());
|
||||
}
|
||||
|
||||
protected final CryptoSerializationManager serializationManager;
|
||||
|
||||
RegistrationRemoteHandler(final String name,
|
||||
final RegistrationWrapper registrationWrapper,
|
||||
final CryptoSerializationManager serializationManager) {
|
||||
RegistrationRemoteHandler(final String name, final RegistrationWrapper registrationWrapper) {
|
||||
super(name, registrationWrapper);
|
||||
|
||||
this.serializationManager = serializationManager;
|
||||
this.serializationManager = registrationWrapper.getSerializtion();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -110,23 +73,42 @@ class RegistrationRemoteHandler extends RegistrationHandler {
|
|||
void initChannel(final Channel channel) {
|
||||
ChannelPipeline pipeline = channel.pipeline();
|
||||
|
||||
Class<? extends Channel> channelClass = channel.getClass();
|
||||
// because of the way TCP works, we have to have special readers/writers. For UDP, all data must be in a single packet.
|
||||
|
||||
boolean isTcpChannel = ConnectionImpl.isTcp(channelClass);
|
||||
boolean isUdpChannel = !isTcpChannel && ConnectionImpl.isUdp(channelClass);
|
||||
|
||||
if (isTcpChannel) {
|
||||
///////////////////////
|
||||
// DECODE (or upstream)
|
||||
///////////////////////
|
||||
pipeline.addFirst(FRAME_AND_KRYO_DECODER,
|
||||
new KryoDecoder(this.serializationManager)); // cannot be shared because of possible fragmentation.
|
||||
}
|
||||
else if (isUdpChannel) {
|
||||
// can be shared because there cannot be fragmentation for our UDP packets. If there is, we throw an error and continue...
|
||||
pipeline.addFirst(KRYO_DECODER, this.registrationWrapper.kryoUdpDecoder);
|
||||
}
|
||||
|
||||
|
||||
int idleTimeout = this.registrationWrapper.getIdleTimeout();
|
||||
if (idleTimeout > 0) {
|
||||
// this makes the proper event get raised in the registrationHandler to kill NEW idle connections. Once "connected" they last a lot longer.
|
||||
// we ALWAYS have this initial IDLE handler, so we don't have to worry about a slow-loris attack against the server.
|
||||
pipeline.addFirst(IDLE_HANDLER, new IdleStateHandler(4, 0, 0)); // in Seconds -- not shared, because it is per-connection
|
||||
// we ALWAYS have this initial IDLE handler, so we don't have to worry about a SLOW-LORIS ATTACK against the server.
|
||||
// in Seconds -- not shared, because it is per-connection
|
||||
pipeline.addFirst(IDLE_HANDLER, new IdleStateHandler(2, 0, 0));
|
||||
}
|
||||
|
||||
if (isTcpChannel) {
|
||||
/////////////////////////
|
||||
// ENCODE (or downstream)
|
||||
/////////////////////////
|
||||
pipeline.addFirst(FRAME_AND_KRYO_ENCODER, this.registrationWrapper.getKryoEncoder()); // this is shared
|
||||
pipeline.addFirst(FRAME_AND_KRYO_ENCODER, this.registrationWrapper.kryoTcpEncoder); // this is shared
|
||||
}
|
||||
else if (isUdpChannel) {
|
||||
pipeline.addFirst(KRYO_ENCODER, this.registrationWrapper.kryoUdpEncoder);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -184,13 +166,32 @@ class RegistrationRemoteHandler extends RegistrationHandler {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a {@link Channel} has been idle for a while.
|
||||
*/
|
||||
@Override
|
||||
public
|
||||
void userEventTriggered(ChannelHandlerContext context, Object event) throws Exception {
|
||||
if (event instanceof IdleStateEvent) {
|
||||
if (((IdleStateEvent) event).state() == IdleState.ALL_IDLE) {
|
||||
// this IS BAD, because we specify an idle handler to prevent slow-loris type attacks on the webserver
|
||||
Channel channel = context.channel();
|
||||
channel.close();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
super.userEventTriggered(context, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
void exceptionCaught(ChannelHandlerContext context, Throwable cause) throws Exception {
|
||||
Channel channel = context.channel();
|
||||
|
||||
this.logger.error("Unexpected exception while trying to send/receive data on Client remote (network) channel. ({})" +
|
||||
this.logger.error("Unexpected exception while trying to send/receive data on remote network channel. ({})" +
|
||||
System.getProperty("line.separator"), channel.remoteAddress(), cause);
|
||||
|
||||
if (channel.isOpen()) {
|
||||
channel.close();
|
||||
}
|
||||
|
@ -202,134 +203,127 @@ class RegistrationRemoteHandler extends RegistrationHandler {
|
|||
protected abstract
|
||||
String getConnectionDirection();
|
||||
|
||||
// have to setup AFTER establish connection, data, as we don't want to enable AES until we're ready.
|
||||
final
|
||||
void setupConnectionCrypto(MetaChannel metaChannel) {
|
||||
|
||||
if (this.logger.isDebugEnabled()) {
|
||||
String type = "TCP";
|
||||
if (metaChannel.udpChannel != null) {
|
||||
type += "/UDP";
|
||||
}
|
||||
|
||||
InetSocketAddress address = (InetSocketAddress) metaChannel.tcpChannel.remoteAddress();
|
||||
this.logger.debug("Encrypting {} session with {}", type, address.getAddress());
|
||||
}
|
||||
|
||||
ChannelPipeline pipeline = metaChannel.tcpChannel.pipeline();
|
||||
int idleTimeout = this.registrationWrapper.getIdleTimeout();
|
||||
|
||||
// add the new handlers (FORCE encryption and longer IDLE handler)
|
||||
pipeline.replace(FRAME_AND_KRYO_DECODER,
|
||||
FRAME_AND_KRYO_CRYPTO_DECODER,
|
||||
new KryoDecoderCrypto(this.serializationManager)); // cannot be shared because of possible fragmentation.
|
||||
|
||||
if (idleTimeout > 0) {
|
||||
pipeline.replace(IDLE_HANDLER, IDLE_HANDLER_FULL, new IdleStateHandler(0, 0, idleTimeout, TimeUnit.MILLISECONDS));
|
||||
}
|
||||
|
||||
pipeline.replace(FRAME_AND_KRYO_ENCODER,
|
||||
FRAME_AND_KRYO_CRYPTO_ENCODER,
|
||||
this.registrationWrapper.getKryoEncoderCrypto()); // this is shared
|
||||
|
||||
|
||||
if (metaChannel.udpChannel != null && metaChannel.udpRemoteAddress == null) {
|
||||
// CLIENT ONLY. The server handles this very differently.
|
||||
pipeline = metaChannel.udpChannel.pipeline();
|
||||
pipeline.replace(KRYO_DECODER, KRYO_CRYPTO_DECODER, new KryoDecoderUdpCrypto(this.serializationManager));
|
||||
pipeline.replace(KRYO_ENCODER, KRYO_CRYPTO_ENCODER, new KryoEncoderUdpCrypto(this.serializationManager));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup our meta-channel to migrate to the correct connection handler for all regular data.
|
||||
* @return true if validation was successful
|
||||
*/
|
||||
final
|
||||
void establishConnection(MetaChannel metaChannel) {
|
||||
ChannelPipeline tcpPipe = metaChannel.tcpChannel.pipeline();
|
||||
ChannelPipeline udpPipe;
|
||||
|
||||
if (metaChannel.udpChannel != null && metaChannel.udpRemoteAddress == null) {
|
||||
// don't want to muck with the SERVER udp pipeline, as it NEVER CHANGES.
|
||||
// only the client will have the udp remote address
|
||||
udpPipe = metaChannel.udpChannel.pipeline();
|
||||
}
|
||||
else {
|
||||
udpPipe = null;
|
||||
}
|
||||
|
||||
// add the "connected"/"normal" handler now that we have established a "new" connection.
|
||||
// This will have state, etc. for this connection.
|
||||
ConnectionImpl connection = (ConnectionImpl) this.registrationWrapper.connection0(metaChannel);
|
||||
|
||||
|
||||
// to have connection notified via the disruptor, we have to specify a custom ChannelHandlerInvoker.
|
||||
tcpPipe.addLast(CONNECTION_HANDLER, connection);
|
||||
|
||||
if (udpPipe != null) {
|
||||
// remember, server is different than client!
|
||||
udpPipe.addLast(CONNECTION_HANDLER, connection);
|
||||
}
|
||||
}
|
||||
|
||||
final
|
||||
boolean verifyAesInfo(final Object message,
|
||||
final Channel channel,
|
||||
final RegistrationWrapper registrationWrapper,
|
||||
final MetaChannel metaChannel,
|
||||
final Logger logger) {
|
||||
|
||||
boolean invalidAES(final MetaChannel metaChannel) {
|
||||
if (metaChannel.aesKey.length != 32) {
|
||||
logger.error("Fatal error trying to use AES key (wrong key length).");
|
||||
shutdown(registrationWrapper, channel);
|
||||
|
||||
ReferenceCountUtil.release(message);
|
||||
return true;
|
||||
}
|
||||
// IV length must == 12 because we are using GCM!
|
||||
else if (metaChannel.aesIV.length != 12) {
|
||||
logger.error("Fatal error trying to use AES IV (wrong IV length).");
|
||||
shutdown(registrationWrapper, channel);
|
||||
|
||||
ReferenceCountUtil.release(message);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// have to setup AFTER establish connection, data, as we don't want to enable AES until we're ready.
|
||||
final
|
||||
void setupConnectionCrypto(final MetaChannel metaChannel, final InetSocketAddress remoteAddress) {
|
||||
|
||||
if (this.logger.isDebugEnabled()) {
|
||||
String type = "";
|
||||
|
||||
if (metaChannel.tcpChannel != null) {
|
||||
type = "TCP";
|
||||
|
||||
if (metaChannel.udpChannel != null) {
|
||||
type += "/";
|
||||
}
|
||||
}
|
||||
|
||||
if (metaChannel.udpChannel != null) {
|
||||
type += "UDP";
|
||||
}
|
||||
|
||||
this.logger.debug("Encrypting {} session with {}", type, remoteAddress);
|
||||
}
|
||||
|
||||
if (metaChannel.tcpChannel != null) {
|
||||
ChannelPipeline pipeline = metaChannel.tcpChannel.pipeline();
|
||||
|
||||
// add the new handlers (FORCE encryption and longer IDLE handler)
|
||||
pipeline.replace(FRAME_AND_KRYO_DECODER,
|
||||
FRAME_AND_KRYO_CRYPTO_DECODER,
|
||||
new KryoDecoderCrypto(this.serializationManager)); // cannot be shared because of possible fragmentation.
|
||||
|
||||
int idleTimeout = this.registrationWrapper.getIdleTimeout();
|
||||
if (idleTimeout > 0) {
|
||||
pipeline.replace(IDLE_HANDLER, IDLE_HANDLER_FULL, new IdleStateHandler(0, 0, idleTimeout, TimeUnit.MILLISECONDS));
|
||||
}
|
||||
|
||||
pipeline.replace(FRAME_AND_KRYO_ENCODER,
|
||||
FRAME_AND_KRYO_CRYPTO_ENCODER,
|
||||
this.registrationWrapper.kryoTcpEncoderCrypto); // this is shared
|
||||
}
|
||||
|
||||
if (metaChannel.udpChannel != null) {
|
||||
ChannelPipeline pipeline = metaChannel.udpChannel.pipeline();
|
||||
|
||||
int idleTimeout = this.registrationWrapper.getIdleTimeout();
|
||||
if (idleTimeout > 0) {
|
||||
pipeline.replace(IDLE_HANDLER, IDLE_HANDLER_FULL, new IdleStateHandler(0, 0, idleTimeout, TimeUnit.MILLISECONDS));
|
||||
}
|
||||
|
||||
pipeline.replace(KRYO_DECODER, KRYO_CRYPTO_DECODER, this.registrationWrapper.kryoUdpDecoderCrypto);
|
||||
pipeline.replace(KRYO_ENCODER, KRYO_CRYPTO_ENCODER, this.registrationWrapper.kryoUdpEncoderCrypto);
|
||||
}
|
||||
}
|
||||
|
||||
// have to setup AFTER establish connection, data, as we don't want to enable AES until we're ready.
|
||||
@SuppressWarnings("AutoUnboxing")
|
||||
final
|
||||
void setupConnection(MetaChannel metaChannel) {
|
||||
// now that we are CONNECTED, we want to remove ourselves (and channel ID's) from the map.
|
||||
// they will be ADDED in another map, in the followup handler!!
|
||||
boolean registerServer = this.registrationWrapper.setupChannels(this, metaChannel);
|
||||
void setupConnection(final MetaChannel metaChannel, final Channel channel) {
|
||||
// Now setup our meta-channel to migrate to the correct connection handler for all regular data.
|
||||
|
||||
if (registerServer) {
|
||||
// Only called if we have a UDP channel
|
||||
setupServerUdpConnection(metaChannel);
|
||||
// add the "connected"/"normal" handler now that we have established a "new" connection.
|
||||
// This will have state, etc. for this connection.
|
||||
InetSocketAddress remoteAddress = (InetSocketAddress) channel.remoteAddress();
|
||||
ConnectionImpl connection = (ConnectionImpl) this.registrationWrapper.connection0(metaChannel, remoteAddress);
|
||||
|
||||
if (metaChannel.tcpChannel != null) {
|
||||
ChannelPipeline pipeline = metaChannel.tcpChannel.pipeline();
|
||||
if (registrationWrapper.isClient()) {
|
||||
pipeline.remove(RegistrationRemoteHandlerClientTCP.class);
|
||||
}
|
||||
else {
|
||||
pipeline.remove(RegistrationRemoteHandlerServerTCP.class);
|
||||
}
|
||||
pipeline.addLast(CONNECTION_HANDLER, connection);
|
||||
}
|
||||
|
||||
if (metaChannel.udpChannel != null) {
|
||||
ChannelPipeline pipeline = metaChannel.udpChannel.pipeline();
|
||||
if (registrationWrapper.isClient()) {
|
||||
pipeline.remove(RegistrationRemoteHandlerClientUDP.class);
|
||||
}
|
||||
else {
|
||||
pipeline.remove(RegistrationRemoteHandlerServerUDP.class);
|
||||
}
|
||||
pipeline.addLast(CONNECTION_HANDLER, connection);
|
||||
}
|
||||
|
||||
if (this.logger.isInfoEnabled()) {
|
||||
String type = "TCP";
|
||||
String type = "";
|
||||
|
||||
if (metaChannel.tcpChannel != null) {
|
||||
type = "TCP";
|
||||
|
||||
if (metaChannel.udpChannel != null) {
|
||||
type += "/UDP";
|
||||
}
|
||||
|
||||
InetSocketAddress address = (InetSocketAddress) metaChannel.tcpChannel.remoteAddress();
|
||||
this.logger.info("Created a {} connection with {}", type, address.getAddress());
|
||||
type += "/";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the metachannel for the UDP server. Default is to do nothing.
|
||||
* <p/>
|
||||
* The server will override this. Only called if we have a UDP channel when we finalize the setup of the TCP connection
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
protected
|
||||
void setupServerUdpConnection(MetaChannel metaChannel) {
|
||||
if (metaChannel.udpChannel != null) {
|
||||
type += "UDP";
|
||||
}
|
||||
|
||||
this.logger.info("Created a {} connection with {}", type, remoteAddress.getAddress());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -339,21 +333,38 @@ class RegistrationRemoteHandler extends RegistrationHandler {
|
|||
final
|
||||
void notifyConnection(MetaChannel metaChannel) {
|
||||
this.registrationWrapper.connectionConnected0(metaChannel.connection);
|
||||
this.registrationWrapper.removeSession(metaChannel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final
|
||||
void channelInactive(ChannelHandlerContext context) throws Exception {
|
||||
Channel channel = context.channel();
|
||||
// whoa! Didn't send valid public key info!
|
||||
boolean invalidPublicKey(final Registration message, final String type) {
|
||||
if (message.publicKey == null) {
|
||||
logger.error("Null ECC public key during " + type + " handshake. This shouldn't happen!");
|
||||
return true;
|
||||
}
|
||||
|
||||
this.logger.info("Closed connection: {}", channel.remoteAddress());
|
||||
return false;
|
||||
}
|
||||
|
||||
// also, once we notify, we unregister this.
|
||||
// SEARCH for our channel!
|
||||
// want to validate the public key used! This is similar to how SSH works, in that once we use a public key, we want to validate
|
||||
// against that ip-address::key pair, so we can better protect against MITM/spoof attacks.
|
||||
boolean invalidRemoteAddress(final MetaChannel metaChannel,
|
||||
final Registration message,
|
||||
final String type,
|
||||
final InetSocketAddress remoteAddress) {
|
||||
|
||||
// on the server, we only get this for TCP events!
|
||||
this.registrationWrapper.closeChannel(channel, maxShutdownWaitTimeInMilliSeconds);
|
||||
|
||||
super.channelInactive(context);
|
||||
boolean valid = registrationWrapper.validateRemoteAddress(metaChannel, remoteAddress, message.publicKey);
|
||||
if (!valid) {
|
||||
//whoa! abort since something messed up! (log happens inside of validate method)
|
||||
String hostAddress = remoteAddress.getAddress()
|
||||
.getHostAddress();
|
||||
logger.error("Invalid ECC public key for server IP {} during {} handshake. WARNING. The server has changed!", hostAddress, type);
|
||||
logger.error("Fix by adding the argument -D{} {} when starting the client.", DELETE_IP, hostAddress);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,16 +15,69 @@
|
|||
*/
|
||||
package dorkbox.network.connection.registration.remote;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.bouncycastle.crypto.BasicAgreement;
|
||||
import org.bouncycastle.crypto.agreement.ECDHCBasicAgreement;
|
||||
import org.bouncycastle.crypto.digests.SHA384Digest;
|
||||
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
|
||||
|
||||
import com.esotericsoftware.kryo.KryoException;
|
||||
import com.esotericsoftware.kryo.io.Input;
|
||||
import com.esotericsoftware.kryo.io.Output;
|
||||
|
||||
import dorkbox.network.connection.RegistrationWrapper;
|
||||
import dorkbox.network.serialization.CryptoSerializationManager;
|
||||
import dorkbox.network.connection.registration.MetaChannel;
|
||||
import dorkbox.network.connection.registration.Registration;
|
||||
import dorkbox.util.crypto.CryptoECC;
|
||||
import dorkbox.util.exceptions.SecurityException;
|
||||
import dorkbox.util.serialization.EccPublicKeySerializer;
|
||||
import io.netty.channel.Channel;
|
||||
|
||||
public
|
||||
class RegistrationRemoteHandlerClient extends RegistrationRemoteHandler {
|
||||
|
||||
RegistrationRemoteHandlerClient(final String name,
|
||||
final RegistrationWrapper registrationWrapper,
|
||||
final CryptoSerializationManager serializationManager) {
|
||||
super(name, registrationWrapper, serializationManager);
|
||||
RegistrationRemoteHandlerClient(final String name, final RegistrationWrapper registrationWrapper) {
|
||||
super(name, registrationWrapper);
|
||||
|
||||
// check to see if we need to delete an IP address as commanded from the user prompt
|
||||
String ipAsString = System.getProperty(DELETE_IP);
|
||||
if (ipAsString != null) {
|
||||
System.setProperty(DELETE_IP, "");
|
||||
byte[] address = null;
|
||||
try {
|
||||
String[] split = ipAsString.split("\\.");
|
||||
if (split.length == 4) {
|
||||
address = new byte[4];
|
||||
for (int i = 0; i < split.length; i++) {
|
||||
int asInt = Integer.parseInt(split[i]);
|
||||
if (asInt >= 0 && asInt <= 255) {
|
||||
//noinspection NumericCastThatLosesPrecision
|
||||
address[i] = (byte) Integer.parseInt(split[i]);
|
||||
}
|
||||
else {
|
||||
address = null;
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
address = null;
|
||||
}
|
||||
|
||||
if (address != null) {
|
||||
try {
|
||||
registrationWrapper.removeRegisteredServerKey(address);
|
||||
} catch (SecurityException e) {
|
||||
this.logger.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
// end command
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -35,4 +88,129 @@ class RegistrationRemoteHandlerClient extends RegistrationRemoteHandler {
|
|||
String getConnectionDirection() {
|
||||
return " ==> ";
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("Duplicates")
|
||||
void readClient(final Channel channel, final Registration registration, final String type, final MetaChannel metaChannel) {
|
||||
InetSocketAddress remoteAddress = (InetSocketAddress) channel.remoteAddress();
|
||||
|
||||
// IN: session ID + public key + ecc parameters (which are a nonce. the SERVER defines what these are)
|
||||
// OUT: remote ECDH shared payload
|
||||
if (metaChannel.aesKey == null && registration.publicKey != null) {
|
||||
// whoa! Didn't send valid public key info!
|
||||
if (invalidPublicKey(registration, type)) {
|
||||
shutdown(channel, registration.sessionID);
|
||||
return;
|
||||
}
|
||||
|
||||
// want to validate the public key used! This is similar to how SSH works, in that once we use a public key, we want to validate
|
||||
// against that ip-address::key pair, so we can better protect against MITM/spoof attacks.
|
||||
if (invalidRemoteAddress(metaChannel, registration, type, remoteAddress)) {
|
||||
// whoa! abort since something messed up! (log and recording if key changed happens inside of validate method)
|
||||
shutdown(channel, registration.sessionID);
|
||||
return;
|
||||
}
|
||||
|
||||
// save off remote public key. This is ALWAYS the same, where the ECDH changes every time...
|
||||
metaChannel.publicKey = registration.publicKey;
|
||||
|
||||
// It is OK that we generate a new ECC keypair for ECDHE every time that we connect from the client.
|
||||
// The server rotates keys every XXXX seconds, since this step is expensive (and the server is the 'trusted' endpoint).
|
||||
metaChannel.ecdhKey = CryptoECC.generateKeyPair(eccSpec, registrationWrapper.getSecureRandom());
|
||||
|
||||
Registration outboundRegister = new Registration(metaChannel.sessionId);
|
||||
|
||||
Output output = new Output(1024);
|
||||
EccPublicKeySerializer.write(output, (ECPublicKeyParameters) metaChannel.ecdhKey.getPublic());
|
||||
outboundRegister.payload = output.toBytes();
|
||||
|
||||
metaChannel.updateRoundTripOnWrite();
|
||||
channel.writeAndFlush(outboundRegister);
|
||||
return;
|
||||
}
|
||||
|
||||
// IN: remote ECDH shared payload
|
||||
// OUT: hasMore=true if we have more registrations to do, false otherwise
|
||||
if (metaChannel.aesKey == null) {
|
||||
/*
|
||||
* Diffie-Hellman-Merkle key exchange for the AES key
|
||||
* http://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange
|
||||
*/
|
||||
byte[] ecdhPubKeyBytes = Arrays.copyOfRange(registration.payload, 0, registration.payload.length);
|
||||
ECPublicKeyParameters ecdhPubKey;
|
||||
try {
|
||||
ecdhPubKey = EccPublicKeySerializer.read(new Input(ecdhPubKeyBytes));
|
||||
} catch (KryoException e) {
|
||||
logger.error("Invalid decode of ECDH public key. Aborting.");
|
||||
shutdown(channel, registration.sessionID);
|
||||
return;
|
||||
}
|
||||
|
||||
BasicAgreement agreement = new ECDHCBasicAgreement();
|
||||
agreement.init(metaChannel.ecdhKey.getPrivate());
|
||||
BigInteger shared = agreement.calculateAgreement(ecdhPubKey);
|
||||
|
||||
// now we setup our AES key based on our shared secret! (from ECDH)
|
||||
// the shared secret is different each time a connection is made
|
||||
byte[] keySeed = shared.toByteArray();
|
||||
|
||||
SHA384Digest sha384 = new SHA384Digest();
|
||||
byte[] digest = new byte[sha384.getDigestSize()];
|
||||
sha384.update(keySeed, 0, keySeed.length);
|
||||
sha384.doFinal(digest, 0);
|
||||
|
||||
metaChannel.aesKey = Arrays.copyOfRange(digest, 0, 32); // 256bit keysize (32 bytes)
|
||||
metaChannel.aesIV = Arrays.copyOfRange(digest, 32, 44); // 96bit blocksize (12 bytes) required by AES-GCM
|
||||
|
||||
if (invalidAES(metaChannel)) {
|
||||
// abort if something messed up!
|
||||
shutdown(channel, registration.sessionID);
|
||||
return;
|
||||
}
|
||||
|
||||
Registration outboundRegister = new Registration(metaChannel.sessionId);
|
||||
|
||||
// do we have any more registrations?
|
||||
boolean hasMoreRegistrations = registrationWrapper.hasMoreRegistrations();
|
||||
outboundRegister.hasMore = hasMoreRegistrations;
|
||||
|
||||
metaChannel.updateRoundTripOnWrite();
|
||||
channel.writeAndFlush(outboundRegister);
|
||||
|
||||
|
||||
if (hasMoreRegistrations) {
|
||||
// start the process for the next protocol.
|
||||
registrationWrapper.startNextProtocolRegistration();
|
||||
}
|
||||
|
||||
// always return!
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// // when we have a "continuing registration" for another protocol, we have to have another roundtrip.
|
||||
// if (registration.payload != null) {
|
||||
// metaChannel.updateRoundTripTime();
|
||||
// channel.writeAndFlush(new Registration(metaChannel.sessionId));
|
||||
// return;
|
||||
// }
|
||||
|
||||
|
||||
// We ONLY get here after the server acks our registration status.
|
||||
// The server will only ack if we DO NOT have more registrations. If we have more registrations, the server waits.
|
||||
setupConnectionCrypto(metaChannel, remoteAddress);
|
||||
setupConnection(metaChannel, channel);
|
||||
|
||||
// wait for a "round trip" amount of time, then notify the APP!
|
||||
final long delay = TimeUnit.NANOSECONDS.toMillis(metaChannel.getRoundTripTime() * 2);
|
||||
channel.eventLoop()
|
||||
.schedule(new Runnable() {
|
||||
@Override
|
||||
public
|
||||
void run() {
|
||||
logger.trace("Notify Connection");
|
||||
notifyConnection(metaChannel);
|
||||
}
|
||||
}, delay, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,102 +15,17 @@
|
|||
*/
|
||||
package dorkbox.network.connection.registration.remote;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.bouncycastle.crypto.BasicAgreement;
|
||||
import org.bouncycastle.crypto.agreement.ECDHCBasicAgreement;
|
||||
import org.bouncycastle.crypto.digests.SHA384Digest;
|
||||
import org.bouncycastle.crypto.engines.IESEngine;
|
||||
import org.bouncycastle.crypto.modes.GCMBlockCipher;
|
||||
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
|
||||
import org.bouncycastle.jce.ECNamedCurveTable;
|
||||
import org.bouncycastle.jce.spec.ECParameterSpec;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import com.esotericsoftware.kryo.KryoException;
|
||||
import com.esotericsoftware.kryo.io.Input;
|
||||
import com.esotericsoftware.kryo.io.Output;
|
||||
|
||||
import dorkbox.network.connection.RegistrationWrapper;
|
||||
import dorkbox.network.connection.registration.MetaChannel;
|
||||
import dorkbox.network.connection.registration.Registration;
|
||||
import dorkbox.network.serialization.CryptoSerializationManager;
|
||||
import dorkbox.util.bytes.OptimizeUtilsByteArray;
|
||||
import dorkbox.util.crypto.CryptoAES;
|
||||
import dorkbox.util.crypto.CryptoECC;
|
||||
import dorkbox.util.exceptions.SecurityException;
|
||||
import dorkbox.util.serialization.EccPublicKeySerializer;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
|
||||
public
|
||||
class RegistrationRemoteHandlerClientTCP extends RegistrationRemoteHandlerClient {
|
||||
|
||||
private static final String DELETE_IP = "eleteIP"; // purposefully missing the "D", since that is a system parameter, which starts with "-D"
|
||||
private static final ECParameterSpec eccSpec = ECNamedCurveTable.getParameterSpec(CryptoECC.curve25519);
|
||||
|
||||
public
|
||||
RegistrationRemoteHandlerClientTCP(final String name,
|
||||
final RegistrationWrapper registrationWrapper,
|
||||
final CryptoSerializationManager serializationManager) {
|
||||
super(name, registrationWrapper, serializationManager);
|
||||
|
||||
// check to see if we need to delete an IP address as commanded from the user prompt
|
||||
String ipAsString = System.getProperty(DELETE_IP);
|
||||
if (ipAsString != null) {
|
||||
System.setProperty(DELETE_IP, "");
|
||||
byte[] address = null;
|
||||
try {
|
||||
String[] split = ipAsString.split("\\.");
|
||||
if (split.length == 4) {
|
||||
address = new byte[4];
|
||||
for (int i = 0; i < split.length; i++) {
|
||||
int asInt = Integer.parseInt(split[i]);
|
||||
if (asInt >= 0 && asInt <= 255) {
|
||||
//noinspection NumericCastThatLosesPrecision
|
||||
address[i] = (byte) Integer.parseInt(split[i]);
|
||||
}
|
||||
else {
|
||||
address = null;
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
address = null;
|
||||
}
|
||||
|
||||
if (address != null) {
|
||||
try {
|
||||
registrationWrapper.removeRegisteredServerKey(address);
|
||||
} catch (SecurityException e) {
|
||||
this.logger.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
// end command
|
||||
}
|
||||
|
||||
/**
|
||||
* STEP 1: Channel is first created
|
||||
*/
|
||||
@Override
|
||||
protected
|
||||
void initChannel(final Channel channel) {
|
||||
this.logger.trace("Channel registered: {}",
|
||||
channel.getClass()
|
||||
.getSimpleName());
|
||||
|
||||
|
||||
// TCP
|
||||
// use the default.
|
||||
super.initChannel(channel);
|
||||
RegistrationRemoteHandlerClientTCP(final String name, final RegistrationWrapper registrationWrapper) {
|
||||
super(name, registrationWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -121,28 +36,13 @@ class RegistrationRemoteHandlerClientTCP extends RegistrationRemoteHandlerClient
|
|||
void channelActive(final ChannelHandlerContext context) throws Exception {
|
||||
super.channelActive(context);
|
||||
|
||||
Channel channel = context.channel();
|
||||
logger.trace("Start new TCP Connection. Sending request to server");
|
||||
|
||||
// look to see if we already have a connection (in progress) for the destined IP address.
|
||||
// Note: our CHANNEL MAP can only have one item at a time, since we do NOT RELEASE the registration lock until it's complete!!
|
||||
|
||||
// The ORDER has to be TCP (always)
|
||||
// TCP
|
||||
MetaChannel metaChannel = new MetaChannel();
|
||||
metaChannel.tcpChannel = channel;
|
||||
|
||||
this.registrationWrapper.addChannel(channel.hashCode(), metaChannel);
|
||||
|
||||
Logger logger2 = this.logger;
|
||||
if (logger2.isTraceEnabled()) {
|
||||
logger2.trace("Start new TCP Connection. Sending request to server");
|
||||
}
|
||||
|
||||
Registration registration = new Registration();
|
||||
Registration registration = new Registration(0);
|
||||
registration.publicKey = this.registrationWrapper.getPublicKey();
|
||||
|
||||
// client start the handshake with a registration packet
|
||||
channel.writeAndFlush(registration);
|
||||
context.channel().writeAndFlush(registration);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"AutoUnboxing", "AutoBoxing", "Duplicates"})
|
||||
|
@ -151,201 +51,32 @@ class RegistrationRemoteHandlerClientTCP extends RegistrationRemoteHandlerClient
|
|||
void channelRead(final ChannelHandlerContext context, final Object message) throws Exception {
|
||||
Channel channel = context.channel();
|
||||
|
||||
RegistrationWrapper registrationWrapper2 = this.registrationWrapper;
|
||||
Logger logger2 = this.logger;
|
||||
if (message instanceof Registration) {
|
||||
// make sure this connection was properly registered in the map. (IT SHOULD BE)
|
||||
MetaChannel metaChannel = registrationWrapper2.getChannel(channel.hashCode());
|
||||
|
||||
//noinspection StatementWithEmptyBody
|
||||
if (metaChannel != null) {
|
||||
metaChannel.updateTcpRoundTripTime();
|
||||
|
||||
Registration registration = (Registration) message;
|
||||
|
||||
if (metaChannel.connectionID == null) {
|
||||
// want to validate the public key used! This is similar to how SSH works, in that once we use a public key, we want to validate
|
||||
// against that ip-address::key pair, so we can better protect against MITM/spoof attacks.
|
||||
InetSocketAddress tcpRemoteServer = (InetSocketAddress) channel.remoteAddress();
|
||||
MetaChannel metaChannel;
|
||||
int sessionId = registration.sessionID;
|
||||
|
||||
boolean valid = registrationWrapper2.validateRemoteAddress(tcpRemoteServer, registration.publicKey);
|
||||
|
||||
if (!valid) {
|
||||
//whoa! abort since something messed up! (log happens inside of validate method)
|
||||
String hostAddress = tcpRemoteServer.getAddress()
|
||||
.getHostAddress();
|
||||
logger2.error("Invalid ECC public key for server IP {} during handshake. WARNING. The server has changed!",
|
||||
hostAddress);
|
||||
logger2.error("Fix by adding the argument -D{} {} when starting the client.", DELETE_IP, hostAddress);
|
||||
metaChannel.changedRemoteKey = true;
|
||||
|
||||
shutdown(registrationWrapper2, channel);
|
||||
|
||||
ReferenceCountUtil.release(message);
|
||||
if (sessionId == 0) {
|
||||
logger.error("Invalid TCP channel session ID 0!");
|
||||
shutdown(channel, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// setup crypto state
|
||||
IESEngine decrypt = this.eccEngineLocal.get();
|
||||
|
||||
byte[] aesKeyBytes = CryptoECC.decrypt(decrypt,
|
||||
registrationWrapper2.getPrivateKey(),
|
||||
registration.publicKey,
|
||||
registration.eccParameters,
|
||||
registration.aesKey,
|
||||
logger);
|
||||
|
||||
if (aesKeyBytes.length != 32) {
|
||||
logger2.error("Invalid decryption of aesKey. Aborting.");
|
||||
shutdown(registrationWrapper2, channel);
|
||||
|
||||
ReferenceCountUtil.release(message);
|
||||
return;
|
||||
}
|
||||
|
||||
final GCMBlockCipher gcmAesEngine = aesEngine.get();
|
||||
|
||||
// now decrypt payload using AES
|
||||
byte[] payload = CryptoAES.decrypt(gcmAesEngine, aesKeyBytes, registration.aesIV, registration.payload, logger);
|
||||
|
||||
if (payload.length == 0) {
|
||||
logger2.error("Invalid decryption of payload. Aborting.");
|
||||
shutdown(registrationWrapper2, channel);
|
||||
|
||||
ReferenceCountUtil.release(message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!OptimizeUtilsByteArray.canReadInt(payload)) {
|
||||
logger2.error("Invalid decryption of connection ID. Aborting.");
|
||||
shutdown(registrationWrapper2, channel);
|
||||
|
||||
ReferenceCountUtil.release(message);
|
||||
return;
|
||||
}
|
||||
|
||||
metaChannel.connectionID = OptimizeUtilsByteArray.readInt(payload, true);
|
||||
int intLength = OptimizeUtilsByteArray.intLength(metaChannel.connectionID, true);
|
||||
|
||||
/*
|
||||
* Diffie-Hellman-Merkle key exchange for the AES key
|
||||
* see http://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange
|
||||
*/
|
||||
byte[] ecdhPubKeyBytes = Arrays.copyOfRange(payload, intLength, payload.length);
|
||||
ECPublicKeyParameters ecdhPubKey;
|
||||
try {
|
||||
ecdhPubKey = EccPublicKeySerializer.read(new Input(ecdhPubKeyBytes));
|
||||
} catch (KryoException e) {
|
||||
logger2.error("Invalid decode of ecdh public key. Aborting.");
|
||||
shutdown(registrationWrapper2, channel);
|
||||
|
||||
ReferenceCountUtil.release(message);
|
||||
return;
|
||||
}
|
||||
|
||||
// It is OK that we generate a new ECC keypair for ECDHE every time that we connect from the client.
|
||||
// The server rotates keys every XXXX seconds, since this step is expensive (and the server is the 'trusted' endpoint).
|
||||
metaChannel.ecdhKey = CryptoECC.generateKeyPair(eccSpec, new SecureRandom());
|
||||
|
||||
// register the channel!
|
||||
registrationWrapper2.addChannel(metaChannel.connectionID, metaChannel);
|
||||
|
||||
metaChannel.publicKey = registration.publicKey;
|
||||
|
||||
// now save our shared AES keys
|
||||
BasicAgreement agreement = new ECDHCBasicAgreement();
|
||||
agreement.init(metaChannel.ecdhKey.getPrivate());
|
||||
BigInteger shared = agreement.calculateAgreement(ecdhPubKey);
|
||||
|
||||
// now we setup our AES key based on our shared secret! (from ECDH)
|
||||
// the shared secret is different each time a connection is made
|
||||
byte[] keySeed = shared.toByteArray();
|
||||
|
||||
SHA384Digest sha384 = new SHA384Digest();
|
||||
byte[] digest = new byte[sha384.getDigestSize()];
|
||||
sha384.update(keySeed, 0, keySeed.length);
|
||||
sha384.doFinal(digest, 0);
|
||||
|
||||
metaChannel.aesKey = Arrays.copyOfRange(digest, 0, 32); // 256bit keysize (32 bytes)
|
||||
metaChannel.aesIV = Arrays.copyOfRange(digest, 32, 44); // 96bit blocksize (12 bytes) required by AES-GCM
|
||||
|
||||
// abort if something messed up!
|
||||
if (verifyAesInfo(message, channel, registrationWrapper2, metaChannel, logger2)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Registration register = new Registration();
|
||||
|
||||
// encrypt the ECDH public key using our previous AES info
|
||||
Output output = new Output(1024);
|
||||
EccPublicKeySerializer.write(output, (ECPublicKeyParameters) metaChannel.ecdhKey.getPublic());
|
||||
byte[] pubKeyAsBytes = output.toBytes();
|
||||
register.payload = CryptoAES.encrypt(gcmAesEngine, aesKeyBytes, registration.aesIV, pubKeyAsBytes, logger);
|
||||
|
||||
channel.writeAndFlush(register);
|
||||
|
||||
ReferenceCountUtil.release(message);
|
||||
return;
|
||||
}
|
||||
|
||||
// else, we are further along in our registration process
|
||||
// REGISTRATION CONNECTED!
|
||||
else {
|
||||
if (metaChannel.connection == null) {
|
||||
// STEP 1: do we have our aes keys?
|
||||
if (metaChannel.ecdhKey != null) {
|
||||
// wipe out our ECDH value.
|
||||
metaChannel.ecdhKey = null;
|
||||
|
||||
// notify the client that we are ready to continue registering other session protocols (bootstraps)
|
||||
boolean isDoneWithRegistration = registrationWrapper2.registerNextProtocol0();
|
||||
|
||||
// tell the server we are done, and to setup crypto on it's side
|
||||
if (isDoneWithRegistration) {
|
||||
channel.writeAndFlush(registration);
|
||||
|
||||
// re-sync the TCP delta round trip time
|
||||
metaChannel.updateTcpRoundTripTime();
|
||||
}
|
||||
|
||||
// if we are NOT done, then we will continue registering other protocols, so do nothing else here.
|
||||
}
|
||||
// we only get this when we are 100% done with the registration of all connection types.
|
||||
else {
|
||||
setupConnectionCrypto(metaChannel);
|
||||
// AES ENCRYPTION NOW USED
|
||||
|
||||
// this sets up the pipeline for the client, so all the necessary handlers are ready to go
|
||||
establishConnection(metaChannel);
|
||||
setupConnection(metaChannel);
|
||||
|
||||
final MetaChannel metaChannel2 = metaChannel;
|
||||
// wait for a "round trip" amount of time, then notify the APP!
|
||||
channel.eventLoop()
|
||||
.schedule(new Runnable() {
|
||||
@Override
|
||||
public
|
||||
void run() {
|
||||
Logger logger2 = RegistrationRemoteHandlerClientTCP.this.logger;
|
||||
if (logger2.isTraceEnabled()) {
|
||||
logger2.trace("Notify Connection");
|
||||
}
|
||||
notifyConnection(metaChannel2);
|
||||
}
|
||||
}, metaChannel.getNanoSecBetweenTCP() * 2, TimeUnit.NANOSECONDS);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// this means that UDP beat us to the "punch", and notified before we did. (notify removes all the entries from the map)
|
||||
metaChannel = registrationWrapper.getSession(sessionId);
|
||||
|
||||
if (metaChannel == null) {
|
||||
metaChannel = registrationWrapper.createSessionClient(sessionId);
|
||||
metaChannel.tcpChannel = channel;
|
||||
logger.debug("New TCP connection. Saving meta-channel id: {}", metaChannel.sessionId);
|
||||
}
|
||||
}
|
||||
else {
|
||||
logger2.error("Error registering TCP with remote server!");
|
||||
shutdown(registrationWrapper2, channel);
|
||||
}
|
||||
|
||||
ReferenceCountUtil.release(message);
|
||||
readClient(channel, registration, "TCP client", metaChannel);
|
||||
}
|
||||
else {
|
||||
logger.error("Error registering TCP with remote server!");
|
||||
shutdown(channel, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,58 +16,20 @@
|
|||
package dorkbox.network.connection.registration.remote;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import dorkbox.network.connection.EndPoint;
|
||||
import dorkbox.network.connection.RegistrationWrapper;
|
||||
import dorkbox.network.connection.registration.MetaChannel;
|
||||
import dorkbox.network.connection.registration.Registration;
|
||||
import dorkbox.network.pipeline.udp.KryoDecoderUdp;
|
||||
import dorkbox.network.pipeline.udp.KryoEncoderUdp;
|
||||
import dorkbox.network.serialization.CryptoSerializationManager;
|
||||
import dorkbox.util.bytes.OptimizeUtilsByteArray;
|
||||
import dorkbox.util.crypto.CryptoAES;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import io.netty.channel.FixedRecvByteBufAllocator;
|
||||
|
||||
@SuppressWarnings("Duplicates")
|
||||
public
|
||||
class RegistrationRemoteHandlerClientUDP extends RegistrationRemoteHandlerClient {
|
||||
|
||||
public
|
||||
RegistrationRemoteHandlerClientUDP(final String name,
|
||||
final RegistrationWrapper registrationWrapper,
|
||||
final CryptoSerializationManager serializationManager) {
|
||||
super(name, registrationWrapper, serializationManager);
|
||||
}
|
||||
|
||||
/**
|
||||
* STEP 1: Channel is first created
|
||||
*/
|
||||
@Override
|
||||
protected
|
||||
void initChannel(final Channel channel) {
|
||||
Logger logger2 = this.logger;
|
||||
if (logger2.isTraceEnabled()) {
|
||||
logger2.trace("Channel registered: " + channel.getClass()
|
||||
.getSimpleName());
|
||||
}
|
||||
|
||||
// Netty4 has default of 2048 bytes as upper limit for datagram packets.
|
||||
channel.config()
|
||||
.setRecvByteBufAllocator(new FixedRecvByteBufAllocator(EndPoint.udpMaxSize));
|
||||
|
||||
ChannelPipeline pipeline = channel.pipeline();
|
||||
|
||||
// UDP
|
||||
// add first to "inject" these handlers in front of myself.
|
||||
// this is only called ONCE for UDP for the CLIENT.
|
||||
pipeline.addFirst(RegistrationRemoteHandler.KRYO_DECODER, new KryoDecoderUdp(this.serializationManager));
|
||||
pipeline.addFirst(RegistrationRemoteHandler.KRYO_ENCODER, new KryoEncoderUdp(this.serializationManager));
|
||||
RegistrationRemoteHandlerClientUDP(final String name, final RegistrationWrapper registrationWrapper) {
|
||||
super(name, registrationWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -80,91 +42,67 @@ class RegistrationRemoteHandlerClientUDP extends RegistrationRemoteHandlerClient
|
|||
|
||||
Channel channel = context.channel();
|
||||
|
||||
// look to see if we already have a connection (in progress) for the destined IP address.
|
||||
// Note: our CHANNEL MAP can only have one item at a time, since we do NOT RELEASE the registration lock until it's complete!!
|
||||
|
||||
// The ORDER has to be TCP (always) -> UDP (optional)
|
||||
// UDP
|
||||
|
||||
InetSocketAddress udpRemoteAddress = (InetSocketAddress) channel.remoteAddress();
|
||||
if (udpRemoteAddress != null) {
|
||||
InetAddress udpRemoteServer = udpRemoteAddress.getAddress();
|
||||
Registration outboundRegister = new Registration(0);
|
||||
outboundRegister.publicKey = this.registrationWrapper.getPublicKey();
|
||||
|
||||
boolean success = registrationWrapper.associateChannels(channel, udpRemoteServer);
|
||||
if (!success) {
|
||||
throw new IOException("UDP cannot connect to a remote server before TCP is established!");
|
||||
// check to see if we have an already existing TCP connection to the server, so we can reuse the MetaChannel.
|
||||
// UDP will always be registered after TCP
|
||||
MetaChannel firstSession = this.registrationWrapper.getFirstSession();
|
||||
if (firstSession != null) {
|
||||
outboundRegister.sessionID = firstSession.sessionId;
|
||||
outboundRegister.hasMore = registrationWrapper.hasMoreRegistrations();
|
||||
|
||||
|
||||
// when we have a "continuing registration" for another protocol, we have to have another roundtrip.
|
||||
// outboundRegister.payload = new byte[0];
|
||||
|
||||
firstSession.updateRoundTripOnWrite();
|
||||
}
|
||||
|
||||
Logger logger2 = this.logger;
|
||||
if (logger2.isTraceEnabled()) {
|
||||
logger2.trace("Start new UDP Connection. Sending request to server");
|
||||
}
|
||||
|
||||
Registration registration = new Registration();
|
||||
// client start the handshake with a registration packet
|
||||
channel.writeAndFlush(registration);
|
||||
// no size info, since this is UDP, it is not segmented
|
||||
channel.writeAndFlush(outboundRegister);
|
||||
}
|
||||
else {
|
||||
throw new IOException("UDP cannot connect to remote server! No remote address specified!");
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"AutoUnboxing", "AutoBoxing"})
|
||||
@Override
|
||||
public
|
||||
void channelRead(final ChannelHandlerContext context, Object message) throws Exception {
|
||||
// REGISTRATION is the ONLY thing NOT encrypted. ALSO, this handler is REMOVED once registration is complete
|
||||
|
||||
Channel channel = context.channel();
|
||||
|
||||
// if we also have a UDP channel, we will receive the "connected" message on UDP (otherwise it will be on TCP)
|
||||
|
||||
RegistrationWrapper registrationWrapper2 = this.registrationWrapper;
|
||||
MetaChannel metaChannel = registrationWrapper2.getChannel(channel.hashCode());
|
||||
|
||||
if (metaChannel != null) {
|
||||
if (message instanceof Registration) {
|
||||
Registration registration = (Registration) message;
|
||||
|
||||
// now decrypt channelID using AES
|
||||
byte[] payload = CryptoAES.decrypt(aesEngine.get(), metaChannel.aesKey, metaChannel.aesIV, registration.payload, logger);
|
||||
MetaChannel metaChannel;
|
||||
int sessionId = registration.sessionID;
|
||||
|
||||
if (!OptimizeUtilsByteArray.canReadInt(payload)) {
|
||||
this.logger.error("Invalid decryption of connection ID. Aborting.");
|
||||
shutdown(registrationWrapper2, channel);
|
||||
if (sessionId == 0) {
|
||||
logger.error("Invalid UDP channel session ID 0!");
|
||||
return;
|
||||
}
|
||||
else {
|
||||
metaChannel = registrationWrapper.getSession(sessionId);
|
||||
|
||||
Integer connectionID = OptimizeUtilsByteArray.readInt(payload, true);
|
||||
|
||||
MetaChannel metaChannel2 = registrationWrapper2.getChannel(connectionID);
|
||||
|
||||
if (metaChannel2 != null) {
|
||||
// hooray! we are successful
|
||||
|
||||
// notify the client that we are ready to continue registering other session protocols (bootstraps)
|
||||
boolean isDoneWithRegistration = registrationWrapper2.registerNextProtocol0();
|
||||
|
||||
// tell the server we are done, and to setup crypto on it's side
|
||||
if (isDoneWithRegistration) {
|
||||
// bounce it back over TCP, so we can receive a "final" connected message over TCP.
|
||||
metaChannel.tcpChannel.writeAndFlush(registration);
|
||||
|
||||
// re-sync the TCP delta round trip time
|
||||
metaChannel.updateTcpRoundTripTime();
|
||||
if (metaChannel == null) {
|
||||
metaChannel = registrationWrapper.createSessionClient(sessionId);
|
||||
logger.debug("New UDP connection. Saving meta-channel id: {}", metaChannel.sessionId);
|
||||
}
|
||||
|
||||
// since we are done here, we need to REMOVE this handler
|
||||
channel.pipeline()
|
||||
.remove(this);
|
||||
|
||||
// if we are NOT done, then we will continue registering other protocols, so do nothing else here.
|
||||
return;
|
||||
}
|
||||
}
|
||||
// in the event that we start with a TCP channel first, we still have to set the UDP channel
|
||||
metaChannel.udpChannel = channel;
|
||||
}
|
||||
|
||||
// if we get here, there was an error!
|
||||
|
||||
this.logger.error("Error registering UDP with remote server!");
|
||||
shutdown(registrationWrapper2, channel);
|
||||
readClient(channel, registration, "UDP client", metaChannel);
|
||||
}
|
||||
else {
|
||||
logger.error("Error registering UDP with remote server!");
|
||||
shutdown(channel, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,17 +15,43 @@
|
|||
*/
|
||||
package dorkbox.network.connection.registration.remote;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
|
||||
import org.bouncycastle.crypto.BasicAgreement;
|
||||
import org.bouncycastle.crypto.agreement.ECDHCBasicAgreement;
|
||||
import org.bouncycastle.crypto.digests.SHA384Digest;
|
||||
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
|
||||
import org.bouncycastle.jce.ECNamedCurveTable;
|
||||
import org.bouncycastle.jce.spec.ECParameterSpec;
|
||||
import org.bouncycastle.util.Arrays;
|
||||
|
||||
import com.esotericsoftware.kryo.KryoException;
|
||||
import com.esotericsoftware.kryo.io.Input;
|
||||
import com.esotericsoftware.kryo.io.Output;
|
||||
|
||||
import dorkbox.network.connection.RegistrationWrapper;
|
||||
import dorkbox.network.connection.registration.MetaChannel;
|
||||
import dorkbox.network.serialization.CryptoSerializationManager;
|
||||
import dorkbox.network.connection.registration.Registration;
|
||||
import dorkbox.util.crypto.CryptoECC;
|
||||
import dorkbox.util.serialization.EccPublicKeySerializer;
|
||||
import io.netty.channel.Channel;
|
||||
|
||||
public
|
||||
class RegistrationRemoteHandlerServer extends RegistrationRemoteHandler {
|
||||
private static final long ECDH_TIMEOUT = TimeUnit.MINUTES.toNanos(10L); // 10 minutes in nanoseconds
|
||||
|
||||
RegistrationRemoteHandlerServer(final String name,
|
||||
final RegistrationWrapper registrationWrapper,
|
||||
final CryptoSerializationManager serializationManager) {
|
||||
super(name, registrationWrapper, serializationManager);
|
||||
private static final ECParameterSpec eccSpec = ECNamedCurveTable.getParameterSpec(CryptoECC.curve25519);
|
||||
private final Object ecdhKeyLock = new Object();
|
||||
private AsymmetricCipherKeyPair ecdhKeyPair;
|
||||
private volatile long ecdhTimeout = System.nanoTime();
|
||||
|
||||
|
||||
RegistrationRemoteHandlerServer(final String name, final RegistrationWrapper registrationWrapper) {
|
||||
super(name, registrationWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -37,12 +63,157 @@ class RegistrationRemoteHandlerServer extends RegistrationRemoteHandler {
|
|||
return " <== ";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Registers the metachannel for the UDP server
|
||||
* Rotates the ECDH key every 10 minutes, as this is a VERY expensive calculation to keep on doing for every connection.
|
||||
*/
|
||||
private
|
||||
AsymmetricCipherKeyPair getEchdKeyOnRotate(final SecureRandom secureRandom) {
|
||||
if (this.ecdhKeyPair == null || System.nanoTime() - this.ecdhTimeout > ECDH_TIMEOUT) {
|
||||
synchronized (this.ecdhKeyLock) {
|
||||
this.ecdhTimeout = System.nanoTime();
|
||||
this.ecdhKeyPair = CryptoECC.generateKeyPair(eccSpec, secureRandom);
|
||||
}
|
||||
}
|
||||
|
||||
return this.ecdhKeyPair;
|
||||
}
|
||||
|
||||
/*
|
||||
* UDP has a VERY limited size, so we have to break up registration steps into the following
|
||||
* 1) session ID == 0 -> exchange session ID and public keys (session ID != 0 now)
|
||||
* 2) session ID != 0 -> establish ECDH shared secret as AES key/iv
|
||||
* 3)
|
||||
*
|
||||
*/
|
||||
@SuppressWarnings("Duplicates")
|
||||
void readServer(final Channel channel, final Registration registration, final String type, final MetaChannel metaChannel) {
|
||||
InetSocketAddress remoteAddress = (InetSocketAddress) channel.remoteAddress();
|
||||
|
||||
// IN: session ID == 0 (start of new connection)
|
||||
// OUT: session ID + public key + ecc parameters (which are a nonce. the SERVER defines what these are)
|
||||
if (registration.sessionID == 0) {
|
||||
// whoa! Didn't send valid public key info!
|
||||
if (invalidPublicKey(registration, type)) {
|
||||
shutdown(channel, registration.sessionID);
|
||||
return;
|
||||
}
|
||||
|
||||
// want to validate the public key used! This is similar to how SSH works, in that once we use a public key, we want to validate
|
||||
// against that ip-address::key pair, so we can better protect against MITM/spoof attacks.
|
||||
if (invalidRemoteAddress(metaChannel, registration, type, remoteAddress)) {
|
||||
// whoa! abort since something messed up! (log and recording if key changed happens inside of validate method)
|
||||
shutdown(channel, registration.sessionID);
|
||||
return;
|
||||
}
|
||||
|
||||
// save off remote public key. This is ALWAYS the same, where the ECDH changes every time...
|
||||
metaChannel.publicKey = registration.publicKey;
|
||||
|
||||
// tell the client to continue it's registration process.
|
||||
Registration outboundRegister = new Registration(metaChannel.sessionId);
|
||||
outboundRegister.publicKey = registrationWrapper.getPublicKey();
|
||||
outboundRegister.eccParameters = CryptoECC.generateSharedParameters(registrationWrapper.getSecureRandom());
|
||||
|
||||
metaChannel.updateRoundTripOnWrite();
|
||||
channel.writeAndFlush(outboundRegister);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// IN: remote ECDH shared payload
|
||||
// OUT: server ECDH shared payload
|
||||
if (metaChannel.aesKey == null) {
|
||||
/*
|
||||
* Diffie-Hellman-Merkle key exchange for the AES key
|
||||
* http://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange
|
||||
*/
|
||||
|
||||
// the ECDH key will ROTATE every 10 minutes, since generating it for EVERY connection is expensive
|
||||
// and since we are combining ECDHE+ECC public/private keys for each connection, other
|
||||
// connections cannot break someone else's connection, since they are still protected by their own private keys.
|
||||
metaChannel.ecdhKey = getEchdKeyOnRotate(registrationWrapper.getSecureRandom());
|
||||
|
||||
byte[] ecdhPubKeyBytes = java.util.Arrays.copyOfRange(registration.payload, 0, registration.payload.length);
|
||||
ECPublicKeyParameters ecdhPubKey;
|
||||
try {
|
||||
ecdhPubKey = EccPublicKeySerializer.read(new Input(ecdhPubKeyBytes));
|
||||
} catch (KryoException e) {
|
||||
logger.error("Invalid decode of ECDH public key. Aborting.");
|
||||
shutdown(channel, registration.sessionID);
|
||||
return;
|
||||
}
|
||||
|
||||
BasicAgreement agreement = new ECDHCBasicAgreement();
|
||||
agreement.init(metaChannel.ecdhKey.getPrivate());
|
||||
BigInteger shared = agreement.calculateAgreement(ecdhPubKey);
|
||||
|
||||
// now we setup our AES key based on our shared secret! (from ECDH)
|
||||
// the shared secret is different each time a connection is made
|
||||
byte[] keySeed = shared.toByteArray();
|
||||
|
||||
SHA384Digest sha384 = new SHA384Digest();
|
||||
byte[] digest = new byte[sha384.getDigestSize()];
|
||||
sha384.update(keySeed, 0, keySeed.length);
|
||||
sha384.doFinal(digest, 0);
|
||||
|
||||
metaChannel.aesKey = Arrays.copyOfRange(digest, 0, 32); // 256bit keysize (32 bytes)
|
||||
metaChannel.aesIV = Arrays.copyOfRange(digest, 32, 44); // 96bit blocksize (12 bytes) required by AES-GCM
|
||||
|
||||
if (invalidAES(metaChannel)) {
|
||||
// abort if something messed up!
|
||||
shutdown(channel, registration.sessionID);
|
||||
return;
|
||||
}
|
||||
|
||||
Registration outboundRegister = new Registration(metaChannel.sessionId);
|
||||
|
||||
Output output = new Output(1024);
|
||||
EccPublicKeySerializer.write(output, (ECPublicKeyParameters) metaChannel.ecdhKey.getPublic());
|
||||
outboundRegister.payload = output.toBytes();
|
||||
|
||||
metaChannel.updateRoundTripOnWrite();
|
||||
channel.writeAndFlush(outboundRegister);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// do we have any more registrations?
|
||||
|
||||
// IN: hasMore=true if we have more registrations to do, false otherwise
|
||||
if (!registration.hasMore) {
|
||||
// // when we have a "continuing registration" for another protocol, we have to have another roundtrip.
|
||||
// if (registration.payload != null) {
|
||||
// metaChannel.updateRoundTripTime();
|
||||
// channel.writeAndFlush(registration);
|
||||
// return;
|
||||
// }
|
||||
|
||||
|
||||
// we only get this when we are 100% done with the registration of all connection types.
|
||||
|
||||
// have to get the delay before we update the round-trip time
|
||||
final long delay = TimeUnit.NANOSECONDS.toMillis(metaChannel.getRoundTripTime() * 2);
|
||||
|
||||
// causes client to setup network connection & AES (we just bounce back the original message)
|
||||
metaChannel.updateRoundTripOnWrite();
|
||||
channel.writeAndFlush(registration);
|
||||
|
||||
setupConnectionCrypto(metaChannel, remoteAddress);
|
||||
setupConnection(metaChannel, channel);
|
||||
|
||||
// wait for a "round trip" amount of time, then notify the APP!
|
||||
channel.eventLoop()
|
||||
.schedule(new Runnable() {
|
||||
@Override
|
||||
protected
|
||||
void setupServerUdpConnection(MetaChannel metaChannel) {
|
||||
registrationWrapper.registerServerUDP(metaChannel);
|
||||
public
|
||||
void run() {
|
||||
logger.trace("Notify Connection");
|
||||
notifyConnection(metaChannel);
|
||||
}
|
||||
}, delay, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
// otherwise we have more registrations...
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,103 +15,18 @@
|
|||
*/
|
||||
package dorkbox.network.connection.registration.remote;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
|
||||
import org.bouncycastle.crypto.BasicAgreement;
|
||||
import org.bouncycastle.crypto.agreement.ECDHCBasicAgreement;
|
||||
import org.bouncycastle.crypto.digests.SHA384Digest;
|
||||
import org.bouncycastle.crypto.engines.IESEngine;
|
||||
import org.bouncycastle.crypto.modes.GCMBlockCipher;
|
||||
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
|
||||
import org.bouncycastle.jce.ECNamedCurveTable;
|
||||
import org.bouncycastle.jce.spec.ECParameterSpec;
|
||||
import org.bouncycastle.util.Arrays;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import com.esotericsoftware.kryo.KryoException;
|
||||
import com.esotericsoftware.kryo.io.Input;
|
||||
import com.esotericsoftware.kryo.io.Output;
|
||||
|
||||
import dorkbox.network.connection.RegistrationWrapper;
|
||||
import dorkbox.network.connection.registration.MetaChannel;
|
||||
import dorkbox.network.connection.registration.Registration;
|
||||
import dorkbox.network.serialization.CryptoSerializationManager;
|
||||
import dorkbox.util.bytes.OptimizeUtilsByteArray;
|
||||
import dorkbox.util.crypto.CryptoAES;
|
||||
import dorkbox.util.crypto.CryptoECC;
|
||||
import dorkbox.util.serialization.EccPublicKeySerializer;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
|
||||
public
|
||||
class RegistrationRemoteHandlerServerTCP extends RegistrationRemoteHandlerServer {
|
||||
|
||||
private static final long ECDH_TIMEOUT = 10L * 60L * 60L * 1000L * 1000L * 1000L; // 10 minutes in nanoseconds
|
||||
|
||||
private static final ECParameterSpec eccSpec = ECNamedCurveTable.getParameterSpec(CryptoECC.curve25519);
|
||||
private final Object ecdhKeyLock = new Object();
|
||||
private AsymmetricCipherKeyPair ecdhKeyPair;
|
||||
private volatile long ecdhTimeout = System.nanoTime();
|
||||
|
||||
|
||||
public
|
||||
RegistrationRemoteHandlerServerTCP(final String name,
|
||||
final RegistrationWrapper registrationWrapper,
|
||||
final CryptoSerializationManager serializationManager) {
|
||||
super(name, registrationWrapper, serializationManager);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates the ECDH key every 10 minutes, as this is a VERY expensive calculation to keep on doing for every connection.
|
||||
*/
|
||||
private
|
||||
AsymmetricCipherKeyPair getEchdKeyOnRotate(final SecureRandom secureRandom) {
|
||||
if (this.ecdhKeyPair == null || System.nanoTime() - this.ecdhTimeout > ECDH_TIMEOUT) {
|
||||
synchronized (this.ecdhKeyLock) {
|
||||
this.ecdhTimeout = System.nanoTime();
|
||||
this.ecdhKeyPair = CryptoECC.generateKeyPair(eccSpec, secureRandom);
|
||||
}
|
||||
}
|
||||
|
||||
return this.ecdhKeyPair;
|
||||
}
|
||||
|
||||
/**
|
||||
* STEP 1: Channel is first created (This is TCP only, as such it differs from the client which is TCP/UDP)
|
||||
*/
|
||||
@Override
|
||||
protected
|
||||
void initChannel(Channel channel) {
|
||||
super.initChannel(channel);
|
||||
}
|
||||
|
||||
/**
|
||||
* STEP 2: Channel is now active. Prepare the meta channel to listen for the registration process
|
||||
*/
|
||||
@Override
|
||||
public
|
||||
void channelActive(ChannelHandlerContext context) throws Exception {
|
||||
super.channelActive(context);
|
||||
|
||||
Channel channel = context.channel();
|
||||
|
||||
// The ORDER has to be TCP (always)
|
||||
// TCP
|
||||
// save this new connection in our associated map. We will get a new one for each new connection from a client.
|
||||
MetaChannel metaChannel = new MetaChannel();
|
||||
metaChannel.tcpChannel = channel;
|
||||
|
||||
this.registrationWrapper.addChannel(channel.hashCode(), metaChannel);
|
||||
|
||||
Logger logger2 = this.logger;
|
||||
if (logger2.isTraceEnabled()) {
|
||||
logger2.trace(this.name, "New TCP connection. Saving TCP channel info.");
|
||||
}
|
||||
RegistrationRemoteHandlerServerTCP(final String name, final RegistrationWrapper registrationWrapper) {
|
||||
super(name, registrationWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -123,225 +38,32 @@ class RegistrationRemoteHandlerServerTCP extends RegistrationRemoteHandlerServer
|
|||
void channelRead(ChannelHandlerContext context, Object message) throws Exception {
|
||||
Channel channel = context.channel();
|
||||
|
||||
// only TCP will come across here for the server. (UDP here is called by the UDP handler/wrapper)
|
||||
|
||||
RegistrationWrapper registrationWrapper2 = this.registrationWrapper;
|
||||
if (message instanceof Registration) {
|
||||
Registration registration = (Registration) message;
|
||||
|
||||
MetaChannel metaChannel = registrationWrapper2.getChannel(channel.hashCode());
|
||||
|
||||
// make sure this connection was properly registered in the map. (IT SHOULD BE)
|
||||
Logger logger2 = this.logger;
|
||||
if (metaChannel != null) {
|
||||
metaChannel.updateTcpRoundTripTime();
|
||||
SecureRandom secureRandom = registrationWrapper2.getSecureRandom();
|
||||
final GCMBlockCipher gcmAesEngine = aesEngine.get();
|
||||
|
||||
// first time we've seen data from this new TCP connection
|
||||
if (metaChannel.connectionID == null) {
|
||||
// whoa! Didn't send valid public key info!
|
||||
if (registration.publicKey == null) {
|
||||
logger2.error("Null ECC public key during client handshake. This shouldn't happen!");
|
||||
shutdown(registrationWrapper2, channel);
|
||||
|
||||
ReferenceCountUtil.release(message);
|
||||
return;
|
||||
MetaChannel metaChannel;
|
||||
int sessionId = registration.sessionID;
|
||||
if (sessionId == 0) {
|
||||
metaChannel = registrationWrapper.createSessionServer();
|
||||
metaChannel.tcpChannel = channel;
|
||||
logger.debug("New TCP connection. Saving meta-channel id: {}", metaChannel.sessionId);
|
||||
}
|
||||
|
||||
// want to validate the public key used! This is similar to how SSH works, in that once we use a public key, we want to validate
|
||||
// against that ip-address::key pair, so we can better protect against MITM/spoof attacks.
|
||||
InetSocketAddress tcpRemoteClient = (InetSocketAddress) channel.remoteAddress();
|
||||
|
||||
boolean valid = registrationWrapper2.validateRemoteAddress(tcpRemoteClient, registration.publicKey);
|
||||
|
||||
if (!valid) {
|
||||
//whoa! abort since something messed up! (log happens inside of validate method)
|
||||
if (logger2.isInfoEnabled()) {
|
||||
logger2.info("Invalid ECC public key for IP {} during handshake with client. Toggling extra flag in channel to indicate this.",
|
||||
tcpRemoteClient.getAddress()
|
||||
.getHostAddress());
|
||||
}
|
||||
metaChannel.changedRemoteKey = true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// if I'm unlucky, keep from confusing connections!
|
||||
|
||||
Integer connectionID = registrationWrapper2.initializeChannel(metaChannel);
|
||||
|
||||
Registration register = new Registration();
|
||||
|
||||
// save off encryption handshake info
|
||||
metaChannel.publicKey = registration.publicKey;
|
||||
|
||||
// use ECC to create an AES key, which is used to encrypt the ECDH public key and the connectionID
|
||||
|
||||
/*
|
||||
* Diffie-Hellman-Merkle key
|
||||
* see http://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange
|
||||
*/
|
||||
|
||||
// the ecdh key will ROTATE every 10 minutes, since generating it for EVERY connection is expensive
|
||||
// and since we are combining ECDHE+ECC public/private keys for each connection, other
|
||||
// connections cannot break someone else's connection, since they are still protected by their own private keys.
|
||||
metaChannel.ecdhKey = getEchdKeyOnRotate(secureRandom);
|
||||
Output output = new Output(1024);
|
||||
EccPublicKeySerializer.write(output, (ECPublicKeyParameters) metaChannel.ecdhKey.getPublic());
|
||||
byte[] pubKeyAsBytes = output.toBytes();
|
||||
|
||||
// save off the connectionID as a byte array
|
||||
|
||||
int intLength = OptimizeUtilsByteArray.intLength(connectionID, true);
|
||||
byte[] idAsBytes = new byte[intLength];
|
||||
OptimizeUtilsByteArray.writeInt(idAsBytes, connectionID, true);
|
||||
|
||||
byte[] combinedBytes = Arrays.concatenate(idAsBytes, pubKeyAsBytes);
|
||||
|
||||
|
||||
// now we have to setup the TEMP AES key!
|
||||
metaChannel.aesKey = new byte[32]; // 256bit keysize (32 bytes)
|
||||
metaChannel.aesIV = new byte[12]; // 96bit blocksize (12 bytes) required by AES-GCM
|
||||
secureRandom.nextBytes(metaChannel.aesKey);
|
||||
secureRandom.nextBytes(metaChannel.aesIV);
|
||||
|
||||
IESEngine encrypt = this.eccEngineLocal.get();
|
||||
|
||||
register.publicKey = registrationWrapper2.getPublicKey();
|
||||
register.eccParameters = CryptoECC.generateSharedParameters(secureRandom);
|
||||
|
||||
// now we have to ENCRYPT the AES key!
|
||||
register.eccParameters = CryptoECC.generateSharedParameters(secureRandom);
|
||||
register.aesIV = metaChannel.aesIV;
|
||||
register.aesKey = CryptoECC.encrypt(encrypt,
|
||||
registrationWrapper2.getPrivateKey(),
|
||||
metaChannel.publicKey,
|
||||
register.eccParameters,
|
||||
metaChannel.aesKey,
|
||||
logger);
|
||||
|
||||
// now encrypt payload via AES
|
||||
register.payload = CryptoAES.encrypt(gcmAesEngine, metaChannel.aesKey, register.aesIV, combinedBytes, logger);
|
||||
|
||||
channel.writeAndFlush(register);
|
||||
|
||||
if (logger2.isTraceEnabled()) {
|
||||
logger2.trace("Assigning new random connection ID for TCP and performing ECDH.");
|
||||
}
|
||||
|
||||
// re-sync the TCP delta round trip time
|
||||
metaChannel.updateTcpRoundTripTime();
|
||||
|
||||
ReferenceCountUtil.release(message);
|
||||
return;
|
||||
}
|
||||
|
||||
// else continue the registration process
|
||||
else {
|
||||
// do we have a connection setup yet?
|
||||
if (metaChannel.connection == null) {
|
||||
// check if we have ECDH specified (if we do, then we are at STEP 1).
|
||||
if (metaChannel.ecdhKey != null) {
|
||||
// now we have to decrypt the ECDH key using our TEMP AES keys
|
||||
metaChannel = registrationWrapper.getSession(sessionId);
|
||||
|
||||
byte[] payload = CryptoAES.decrypt(gcmAesEngine,
|
||||
metaChannel.aesKey,
|
||||
metaChannel.aesIV,
|
||||
registration.payload,
|
||||
logger);
|
||||
|
||||
// abort if we cannot properly get the key info from the payload
|
||||
if (payload.length == 0) {
|
||||
logger2.error("Invalid decryption of payload. Aborting.");
|
||||
shutdown(registrationWrapper2, channel);
|
||||
|
||||
ReferenceCountUtil.release(message);
|
||||
if (metaChannel == null) {
|
||||
logger.error("Error getting invalid TCP channel session ID {}! MetaChannel is null!", sessionId);
|
||||
shutdown(channel, sessionId);
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* Diffie-Hellman-Merkle key exchange for the AES key
|
||||
* see http://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange
|
||||
*/
|
||||
ECPublicKeyParameters ecdhPubKey;
|
||||
try {
|
||||
ecdhPubKey = EccPublicKeySerializer.read(new Input(payload));
|
||||
} catch (KryoException e) {
|
||||
logger2.error("Invalid decode of ecdh public key. Aborting.");
|
||||
shutdown(registrationWrapper2, channel);
|
||||
|
||||
ReferenceCountUtil.release(message);
|
||||
return;
|
||||
}
|
||||
|
||||
BasicAgreement agreement = new ECDHCBasicAgreement();
|
||||
agreement.init(metaChannel.ecdhKey.getPrivate());
|
||||
BigInteger shared = agreement.calculateAgreement(ecdhPubKey);
|
||||
|
||||
// wipe out our saved values.
|
||||
metaChannel.aesKey = null;
|
||||
metaChannel.aesIV = null;
|
||||
metaChannel.ecdhKey = null;
|
||||
|
||||
// now we setup our AES key based on our shared secret! (from ECDH)
|
||||
// the shared secret is different each time a connection is made
|
||||
byte[] keySeed = shared.toByteArray();
|
||||
|
||||
SHA384Digest sha384 = new SHA384Digest();
|
||||
byte[] digest = new byte[sha384.getDigestSize()];
|
||||
sha384.update(keySeed, 0, keySeed.length);
|
||||
sha384.doFinal(digest, 0);
|
||||
|
||||
metaChannel.aesKey = Arrays.copyOfRange(digest, 0, 32); // 256bit keysize (32 bytes)
|
||||
metaChannel.aesIV = Arrays.copyOfRange(digest, 32, 44); // 96bit blocksize (12 bytes) required by AES-GCM
|
||||
|
||||
// abort if something messed up!
|
||||
if (verifyAesInfo(message, channel, registrationWrapper2, metaChannel, logger2)) {
|
||||
return;
|
||||
readServer(channel, registration, "TCP server", metaChannel);
|
||||
}
|
||||
|
||||
// tell the client to continue it's registration process.
|
||||
channel.writeAndFlush(new Registration());
|
||||
}
|
||||
|
||||
// we only get this when we are 100% done with the registration of all connection types.
|
||||
else {
|
||||
channel.writeAndFlush(registration); // causes client to setup network connection & AES
|
||||
|
||||
setupConnectionCrypto(metaChannel);
|
||||
// AES ENCRYPTION NOW USED
|
||||
|
||||
// this sets up the pipeline for the server, so all the necessary handlers are ready to go
|
||||
establishConnection(metaChannel);
|
||||
setupConnection(metaChannel);
|
||||
|
||||
final MetaChannel chan2 = metaChannel;
|
||||
// wait for a "round trip" amount of time, then notify the APP!
|
||||
channel.eventLoop()
|
||||
.schedule(new Runnable() {
|
||||
@Override
|
||||
public
|
||||
void run() {
|
||||
Logger logger2 = RegistrationRemoteHandlerServerTCP.this.logger;
|
||||
if (logger2.isTraceEnabled()) {
|
||||
logger2.trace("Notify Connection");
|
||||
logger.error("Error registering TCP with remote client!");
|
||||
shutdown(channel, 0);
|
||||
}
|
||||
notifyConnection(chan2);
|
||||
}
|
||||
}, metaChannel.getNanoSecBetweenTCP() * 2, TimeUnit.NANOSECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
ReferenceCountUtil.release(message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// this should NEVER happen!
|
||||
logger2.error("Error registering TCP channel! MetaChannel is null!");
|
||||
}
|
||||
|
||||
shutdown(registrationWrapper2, channel);
|
||||
ReferenceCountUtil.release(message);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,265 +15,56 @@
|
|||
*/
|
||||
package dorkbox.network.connection.registration.remote;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.List;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import dorkbox.network.Broadcast;
|
||||
import dorkbox.network.connection.ConnectionImpl;
|
||||
import dorkbox.network.connection.EndPoint;
|
||||
import dorkbox.network.connection.KryoExtra;
|
||||
import dorkbox.network.connection.RegistrationWrapper;
|
||||
import dorkbox.network.connection.registration.MetaChannel;
|
||||
import dorkbox.network.connection.registration.Registration;
|
||||
import dorkbox.network.connection.wrapper.UdpWrapper;
|
||||
import dorkbox.network.serialization.CryptoSerializationManager;
|
||||
import dorkbox.util.bytes.OptimizeUtilsByteArray;
|
||||
import dorkbox.util.crypto.CryptoAES;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandler.Sharable;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.FixedRecvByteBufAllocator;
|
||||
import io.netty.channel.socket.DatagramPacket;
|
||||
import io.netty.handler.codec.MessageToMessageCodec;
|
||||
|
||||
@SuppressWarnings("Duplicates")
|
||||
@Sharable
|
||||
public
|
||||
class RegistrationRemoteHandlerServerUDP extends MessageToMessageCodec<DatagramPacket, UdpWrapper> {
|
||||
|
||||
// this is for the SERVER only. UDP channel is ALWAYS the SAME channel (it's the server's listening channel).
|
||||
|
||||
private final org.slf4j.Logger logger;
|
||||
private final ByteBuf discoverResponseBuffer;
|
||||
private final RegistrationWrapper registrationWrapper;
|
||||
private final CryptoSerializationManager serializationManager;
|
||||
|
||||
class RegistrationRemoteHandlerServerUDP extends RegistrationRemoteHandlerServer {
|
||||
public
|
||||
RegistrationRemoteHandlerServerUDP(final String name,
|
||||
final RegistrationWrapper registrationWrapper,
|
||||
final CryptoSerializationManager serializationManager) {
|
||||
final String name1 = name + " Registration-UDP-Server";
|
||||
this.logger = org.slf4j.LoggerFactory.getLogger(name1);
|
||||
this.registrationWrapper = registrationWrapper;
|
||||
this.serializationManager = serializationManager;
|
||||
|
||||
// absolutely MUST send packet > 0 across, otherwise netty will think it failed to write to the socket, and keep trying. (bug was fixed by netty. Keeping this code)
|
||||
this.discoverResponseBuffer = Unpooled.buffer(1);
|
||||
this.discoverResponseBuffer.writeByte(Broadcast.broadcastResponseID);
|
||||
}
|
||||
|
||||
/**
|
||||
* STEP 2: Channel is now active. We are now LISTENING to UDP messages!
|
||||
*/
|
||||
@Override
|
||||
public
|
||||
void channelActive(final ChannelHandlerContext context) throws Exception {
|
||||
// Netty4 has default of 2048 bytes as upper limit for datagram packets.
|
||||
context.channel()
|
||||
.config()
|
||||
.setRecvByteBufAllocator(new FixedRecvByteBufAllocator(EndPoint.udpMaxSize));
|
||||
|
||||
// do NOT want to add UDP channels, since they are tracked differently for the server.
|
||||
RegistrationRemoteHandlerServerUDP(final String name, final RegistrationWrapper registrationWrapper) {
|
||||
super(name, registrationWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
void exceptionCaught(final ChannelHandlerContext context, final Throwable cause) throws Exception {
|
||||
// log UDP errors.
|
||||
this.logger.error("Exception caught in UDP stream.", cause);
|
||||
super.exceptionCaught(context, cause);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected
|
||||
void encode(final ChannelHandlerContext context, final UdpWrapper msg, final List<Object> out) throws Exception {
|
||||
Object object = msg.object();
|
||||
InetSocketAddress remoteAddress = msg.remoteAddress();
|
||||
|
||||
if (object instanceof ByteBuf) {
|
||||
// this is the response from a discoverHost query
|
||||
out.add(new DatagramPacket((ByteBuf) object, remoteAddress));
|
||||
}
|
||||
else {
|
||||
// this is regular registration stuff
|
||||
ByteBuf buffer = context.alloc()
|
||||
.buffer();
|
||||
|
||||
// writes data into buffer
|
||||
try {
|
||||
ConnectionImpl networkConnection = this.registrationWrapper.getServerUDP(remoteAddress);
|
||||
if (networkConnection != null) {
|
||||
// try to write data! (IT SHOULD ALWAYS BE ENCRYPTED HERE!)
|
||||
this.serializationManager.writeWithCrypto(networkConnection, buffer, object);
|
||||
}
|
||||
else {
|
||||
// this means we are still in the REGISTRATION phase.
|
||||
this.serializationManager.write(buffer, object);
|
||||
}
|
||||
|
||||
if (buffer != null) {
|
||||
out.add(new DatagramPacket(buffer, remoteAddress));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.error("Unable to write data to the socket.", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected
|
||||
void decode(final ChannelHandlerContext context, final DatagramPacket msg, final List<Object> out) throws Exception {
|
||||
void channelRead(final ChannelHandlerContext context, Object message) throws Exception {
|
||||
Channel channel = context.channel();
|
||||
ByteBuf data = msg.content();
|
||||
InetSocketAddress remoteAddress = msg.sender();
|
||||
|
||||
// must have a remote address in the packet. (ie, ignore broadcast)
|
||||
Logger logger2 = this.logger;
|
||||
if (remoteAddress == null) {
|
||||
if (logger2.isDebugEnabled()) {
|
||||
logger2.debug("Ignoring packet with null UDP remote address. (Is it broadcast?)");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (message instanceof Registration) {
|
||||
Registration registration = (Registration) message;
|
||||
|
||||
if (data.readableBytes() == 1) {
|
||||
if (data.readByte() == Broadcast.broadcastID) {
|
||||
// CANNOT use channel.getRemoteAddress()
|
||||
channel.writeAndFlush(new UdpWrapper(this.discoverResponseBuffer, remoteAddress));
|
||||
if (logger2.isDebugEnabled()) {
|
||||
logger2.debug("Responded to host discovery from: {}", remoteAddress);
|
||||
}
|
||||
}
|
||||
else {
|
||||
logger2.error("Invalid signature for 'Discover Host' from remote address: {}", remoteAddress);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// we cannot use the REGULAR pipeline, since we can't pass along the remote address for
|
||||
// when we establish the "network connection"
|
||||
|
||||
// send on the message, now that we have the WRITE channel figured out and the data.
|
||||
receivedUDP(context, channel, data, remoteAddress);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// this will be invoked by the UdpRegistrationHandlerServer. Remember, TCP will be established first.
|
||||
@SuppressWarnings({"unused", "AutoUnboxing"})
|
||||
private
|
||||
void receivedUDP(final ChannelHandlerContext context,
|
||||
final Channel channel,
|
||||
final ByteBuf message,
|
||||
final InetSocketAddress udpRemoteAddress) throws Exception {
|
||||
|
||||
// registration is the ONLY thing NOT encrypted
|
||||
Logger logger2 = this.logger;
|
||||
RegistrationWrapper registrationWrapper2 = this.registrationWrapper;
|
||||
CryptoSerializationManager serializationManager2 = this.serializationManager;
|
||||
|
||||
if (KryoExtra.isEncrypted(message)) {
|
||||
// we need to FORWARD this message "down the pipeline".
|
||||
|
||||
ConnectionImpl connection = registrationWrapper2.getServerUDP(udpRemoteAddress);
|
||||
//noinspection StatementWithEmptyBody
|
||||
if (connection != null) {
|
||||
// try to read data! (IT SHOULD ALWAYS BE ENCRYPTED HERE!)
|
||||
Object object;
|
||||
|
||||
try {
|
||||
object = serializationManager2.readWithCrypto(connection, message, message.writerIndex());
|
||||
} catch (Exception e) {
|
||||
logger2.error("UDP unable to deserialize buffer", e);
|
||||
shutdown(registrationWrapper2, channel);
|
||||
throw e;
|
||||
}
|
||||
|
||||
connection.channelRead(object);
|
||||
}
|
||||
// if we don't have this "from" IP address ALREADY registered, drop the packet.
|
||||
// OR the channel was shutdown while it was still receiving data.
|
||||
else {
|
||||
// we DON'T CARE about this, so we will just ignore the incoming message.
|
||||
}
|
||||
}
|
||||
// manage the registration packets!
|
||||
else {
|
||||
Object object;
|
||||
|
||||
try {
|
||||
object = serializationManager2.read(message, message.writerIndex());
|
||||
} catch (Exception e) {
|
||||
logger2.error("UDP unable to deserialize buffer", e);
|
||||
shutdown(registrationWrapper2, channel);
|
||||
return;
|
||||
}
|
||||
|
||||
if (object instanceof Registration) {
|
||||
// find out and make sure that UDP and TCP are talking to the same server
|
||||
InetAddress udpRemoteServer = udpRemoteAddress.getAddress();
|
||||
MetaChannel metaChannel = registrationWrapper2.getAssociatedChannel_UDP(udpRemoteServer);
|
||||
|
||||
if (metaChannel != null) {
|
||||
// associate TCP and UDP!
|
||||
MetaChannel metaChannel;
|
||||
int sessionId = 0;
|
||||
sessionId = registration.sessionID;
|
||||
if (sessionId == 0) {
|
||||
metaChannel = registrationWrapper.createSessionServer();
|
||||
metaChannel.udpChannel = channel;
|
||||
metaChannel.udpRemoteAddress = udpRemoteAddress;
|
||||
|
||||
Registration register = new Registration();
|
||||
|
||||
// save off the connectionID as a byte array, then encrypt it
|
||||
int intLength = OptimizeUtilsByteArray.intLength(metaChannel.connectionID, true);
|
||||
byte[] idAsBytes = new byte[intLength];
|
||||
OptimizeUtilsByteArray.writeInt(idAsBytes, metaChannel.connectionID, true);
|
||||
|
||||
// now encrypt payload via AES
|
||||
register.payload = CryptoAES.encrypt(RegistrationRemoteHandler.aesEngine.get(),
|
||||
metaChannel.aesKey,
|
||||
metaChannel.aesIV,
|
||||
idAsBytes,
|
||||
logger);
|
||||
|
||||
channel.writeAndFlush(new UdpWrapper(register, udpRemoteAddress));
|
||||
if (logger2.isTraceEnabled()) {
|
||||
logger2.trace("Register UDP connection from {}", udpRemoteAddress);
|
||||
}
|
||||
logger.debug("New UDP connection. Saving meta-channel id: {}", metaChannel.sessionId);
|
||||
}
|
||||
else {
|
||||
// if we get here, there was a failure!
|
||||
logger2.error("Error trying to register UDP with incorrect udp specified! UDP: {}", udpRemoteAddress);
|
||||
shutdown(registrationWrapper2, channel);
|
||||
metaChannel = registrationWrapper.getSession(sessionId);
|
||||
|
||||
if (metaChannel == null) {
|
||||
logger.error("Error getting invalid UDP channel session ID {}! MetaChannel is null!", sessionId);
|
||||
shutdown(channel, sessionId);
|
||||
return;
|
||||
}
|
||||
|
||||
// in the event that we start with a TCP channel first, we still have to set the UDP channel
|
||||
metaChannel.udpChannel = channel;
|
||||
}
|
||||
|
||||
readServer(channel, registration, "UDP server", metaChannel);
|
||||
}
|
||||
else {
|
||||
logger2.error("UDP attempting to spoof client! Unencrypted packet other than registration received.");
|
||||
shutdown(null, channel);
|
||||
logger.error("Error registering UDP with remote client!");
|
||||
shutdown(channel, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copied from RegistrationHandler. There were issues accessing it as static with generics.
|
||||
*/
|
||||
public
|
||||
MetaChannel shutdown(final RegistrationWrapper registrationWrapper, final Channel channel) {
|
||||
this.logger.error("SHUTDOWN HANDLER REACHED! SOMETHING MESSED UP! TRYING TO ABORT");
|
||||
|
||||
// shutdown. Something messed up. Only reach this is something messed up.
|
||||
// properly shutdown the TCP/UDP channels.
|
||||
if (channel.isOpen()) {
|
||||
channel.close();
|
||||
}
|
||||
|
||||
// also, once we notify, we unregister this.
|
||||
if (registrationWrapper != null) {
|
||||
return registrationWrapper.closeChannel(channel, EndPoint.maxShutdownWaitTimeInMilliSeconds);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,17 +20,17 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||
import org.bouncycastle.crypto.params.ParametersWithIV;
|
||||
|
||||
import dorkbox.network.connection.ConnectionImpl;
|
||||
import dorkbox.network.connection.ConnectionPointWriter;
|
||||
import dorkbox.network.connection.ConnectionPoint;
|
||||
import dorkbox.network.connection.EndPoint;
|
||||
import dorkbox.network.connection.ISessionManager;
|
||||
import dorkbox.network.connection.registration.MetaChannel;
|
||||
import dorkbox.network.rmi.RmiObjectHandler;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.EventLoop;
|
||||
import io.netty.channel.local.LocalAddress;
|
||||
import io.netty.util.concurrent.Promise;
|
||||
|
||||
public
|
||||
class ChannelLocalWrapper implements ChannelWrapper, ConnectionPointWriter {
|
||||
class ChannelLocalWrapper implements ChannelWrapper, ConnectionPoint {
|
||||
|
||||
private final Channel channel;
|
||||
private final RmiObjectHandler rmiObjectHandler;
|
||||
|
@ -66,13 +66,13 @@ class ChannelLocalWrapper implements ChannelWrapper, ConnectionPointWriter {
|
|||
|
||||
@Override
|
||||
public
|
||||
ConnectionPointWriter tcp() {
|
||||
ConnectionPoint tcp() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
ConnectionPointWriter udp() {
|
||||
ConnectionPoint udp() {
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -96,10 +96,12 @@ class ChannelLocalWrapper implements ChannelWrapper, ConnectionPointWriter {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public
|
||||
EventLoop getEventLoop() {
|
||||
return this.channel.eventLoop();
|
||||
<V> Promise<V> newPromise() {
|
||||
return channel.eventLoop()
|
||||
.newPromise();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -15,23 +15,24 @@
|
|||
*/
|
||||
package dorkbox.network.connection.wrapper;
|
||||
|
||||
import dorkbox.network.connection.ConnectionPointWriter;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelPromise;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public
|
||||
class ChannelNetwork implements ConnectionPointWriter {
|
||||
import dorkbox.network.connection.ConnectionPoint;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelPromise;
|
||||
import io.netty.util.concurrent.Promise;
|
||||
|
||||
private final Channel channel;
|
||||
private final AtomicBoolean shouldFlush = new AtomicBoolean(false);
|
||||
public
|
||||
class ChannelNetwork implements ConnectionPoint {
|
||||
|
||||
final Channel channel;
|
||||
final AtomicBoolean shouldFlush = new AtomicBoolean(false);
|
||||
private final ChannelPromise voidPromise;
|
||||
|
||||
public
|
||||
ChannelNetwork(Channel channel) {
|
||||
this.channel = channel;
|
||||
voidPromise = channel.voidPromise();
|
||||
this.voidPromise = channel.voidPromise();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -39,7 +40,7 @@ class ChannelNetwork implements ConnectionPointWriter {
|
|||
*/
|
||||
@Override
|
||||
public
|
||||
void write(Object object) {
|
||||
void write(Object object) throws Exception {
|
||||
// we don't care, or want to save the future. This is so GC is less.
|
||||
channel.write(object, voidPromise);
|
||||
shouldFlush.set(true);
|
||||
|
@ -54,7 +55,6 @@ class ChannelNetwork implements ConnectionPointWriter {
|
|||
return channel.isWritable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
void flush() {
|
||||
if (shouldFlush.compareAndSet(true, false)) {
|
||||
|
@ -62,15 +62,16 @@ class ChannelNetwork implements ConnectionPointWriter {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
<V> Promise<V> newPromise() {
|
||||
return channel.eventLoop().newPromise();
|
||||
}
|
||||
|
||||
public
|
||||
void close(long maxShutdownWaitTimeInMilliSeconds) {
|
||||
shouldFlush.set(false);
|
||||
channel.close()
|
||||
.awaitUninterruptibly(maxShutdownWaitTimeInMilliSeconds);
|
||||
}
|
||||
|
||||
public
|
||||
int id() {
|
||||
return channel.hashCode();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* 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 dorkbox.network.connection.wrapper;
|
||||
|
||||
import dorkbox.network.connection.UdpServer;
|
||||
import io.netty.channel.Channel;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
public
|
||||
class ChannelNetworkUdp extends ChannelNetwork {
|
||||
|
||||
private final InetSocketAddress udpRemoteAddress;
|
||||
private final UdpServer udpServer;
|
||||
|
||||
public
|
||||
ChannelNetworkUdp(Channel channel, InetSocketAddress udpRemoteAddress, UdpServer udpServer) {
|
||||
super(channel);
|
||||
|
||||
this.udpRemoteAddress = udpRemoteAddress;
|
||||
this.udpServer = udpServer; // ONLY valid in the server!
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
void write(Object object) {
|
||||
// this shoots out the SERVER pipeline, which is SLIGHTLY different!
|
||||
super.write(new UdpWrapper(object, udpRemoteAddress));
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
void close(long maxShutdownWaitTimeInMilliSeconds) {
|
||||
// we ONLY want to close the UDP channel when we are STOPPING the server, otherwise we close the UDP channel
|
||||
// that listens for new connections! SEE Server.close().
|
||||
// super.close(maxShutdownWaitTimeInMilliSeconds);
|
||||
|
||||
// need to UNREGISTER the address from my ChannelManager.
|
||||
if (udpServer != null) {
|
||||
// only the server does this.
|
||||
udpServer.unRegisterServerUDP(udpRemoteAddress);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -21,20 +21,19 @@ import org.bouncycastle.crypto.params.KeyParameter;
|
|||
import org.bouncycastle.crypto.params.ParametersWithIV;
|
||||
|
||||
import dorkbox.network.connection.ConnectionImpl;
|
||||
import dorkbox.network.connection.ConnectionPointWriter;
|
||||
import dorkbox.network.connection.ConnectionPoint;
|
||||
import dorkbox.network.connection.EndPoint;
|
||||
import dorkbox.network.connection.ISessionManager;
|
||||
import dorkbox.network.connection.UdpServer;
|
||||
import dorkbox.network.connection.registration.MetaChannel;
|
||||
import dorkbox.network.rmi.RmiObjectHandler;
|
||||
import dorkbox.util.FastThreadLocal;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.EventLoop;
|
||||
import io.netty.util.NetUtil;
|
||||
|
||||
public
|
||||
class ChannelNetworkWrapper implements ChannelWrapper {
|
||||
|
||||
private final int sessionId;
|
||||
|
||||
private final ChannelNetwork tcp;
|
||||
private final ChannelNetwork udp;
|
||||
|
||||
|
@ -43,7 +42,6 @@ class ChannelNetworkWrapper implements ChannelWrapper {
|
|||
|
||||
private final String remoteAddress;
|
||||
private final boolean isLoopback;
|
||||
private final EventLoop eventLoop;
|
||||
|
||||
// GCM IV. hacky way to prevent tons of GC and to not clobber the original parameters
|
||||
private final byte[] aesKey; // AES-256 requires 32 bytes
|
||||
|
@ -53,36 +51,30 @@ class ChannelNetworkWrapper implements ChannelWrapper {
|
|||
private final RmiObjectHandler rmiObjectHandler;
|
||||
|
||||
/**
|
||||
* @param udpServer is null when created by the client, non-null when created by the server
|
||||
* @param rmiObjectHandler is a no-op handler if RMI is disabled, otherwise handles RMI object registration
|
||||
*/
|
||||
public
|
||||
ChannelNetworkWrapper(MetaChannel metaChannel, UdpServer udpServer, final RmiObjectHandler rmiObjectHandler) {
|
||||
ChannelNetworkWrapper(final MetaChannel metaChannel, final InetSocketAddress remoteAddress, final RmiObjectHandler rmiObjectHandler) {
|
||||
|
||||
this.sessionId = metaChannel.sessionId;
|
||||
this.rmiObjectHandler = rmiObjectHandler;
|
||||
this.isLoopback = remoteAddress.getAddress().equals(NetUtil.LOCALHOST);
|
||||
|
||||
Channel tcpChannel = metaChannel.tcpChannel;
|
||||
this.eventLoop = tcpChannel.eventLoop();
|
||||
|
||||
isLoopback = ((InetSocketAddress)tcpChannel.remoteAddress()).getAddress().equals(NetUtil.LOCALHOST);
|
||||
|
||||
this.tcp = new ChannelNetwork(tcpChannel);
|
||||
if (metaChannel.tcpChannel != null) {
|
||||
this.tcp = new ChannelNetwork(metaChannel.tcpChannel);
|
||||
} else {
|
||||
this.tcp = null;
|
||||
}
|
||||
|
||||
if (metaChannel.udpChannel != null) {
|
||||
if (metaChannel.udpRemoteAddress != null) {
|
||||
this.udp = new ChannelNetworkUdp(metaChannel.udpChannel, metaChannel.udpRemoteAddress, udpServer);
|
||||
}
|
||||
else {
|
||||
this.udp = new ChannelNetwork(metaChannel.udpChannel);
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.udp = null;
|
||||
}
|
||||
|
||||
|
||||
this.remoteAddress = ((InetSocketAddress) tcpChannel.remoteAddress()).getAddress()
|
||||
.getHostAddress();
|
||||
this.remoteAddress = remoteAddress.getAddress().getHostAddress();
|
||||
this.remotePublicKeyChanged = metaChannel.changedRemoteKey;
|
||||
|
||||
// AES key & IV (only for networked connections)
|
||||
|
@ -105,13 +97,13 @@ class ChannelNetworkWrapper implements ChannelWrapper {
|
|||
|
||||
@Override
|
||||
public
|
||||
ConnectionPointWriter tcp() {
|
||||
ConnectionPoint tcp() {
|
||||
return this.tcp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
ConnectionPointWriter udp() {
|
||||
ConnectionPoint udp() {
|
||||
return this.udp;
|
||||
}
|
||||
|
||||
|
@ -130,19 +122,15 @@ class ChannelNetworkWrapper implements ChannelWrapper {
|
|||
@Override
|
||||
public
|
||||
void flush() {
|
||||
if (this.tcp != null) {
|
||||
this.tcp.flush();
|
||||
}
|
||||
|
||||
if (this.udp != null) {
|
||||
this.udp.flush();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
EventLoop getEventLoop() {
|
||||
return this.eventLoop;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a threadlocal AES key + IV. key=32 byte, iv=12 bytes (AES-GCM implementation). This is a threadlocal
|
||||
* because multiple protocols can be performing crypto AT THE SAME TIME, and so we have to make sure that operations don't
|
||||
|
@ -177,7 +165,9 @@ class ChannelNetworkWrapper implements ChannelWrapper {
|
|||
void close(final ConnectionImpl connection, final ISessionManager sessionManager) {
|
||||
long maxShutdownWaitTimeInMilliSeconds = EndPoint.maxShutdownWaitTimeInMilliSeconds;
|
||||
|
||||
if (this.tcp != null) {
|
||||
this.tcp.close(maxShutdownWaitTimeInMilliSeconds);
|
||||
}
|
||||
|
||||
if (this.udp != null) {
|
||||
this.udp.close(maxShutdownWaitTimeInMilliSeconds);
|
||||
|
@ -190,13 +180,14 @@ class ChannelNetworkWrapper implements ChannelWrapper {
|
|||
@Override
|
||||
public
|
||||
int id() {
|
||||
return this.tcp.id();
|
||||
return this.sessionId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
int hashCode() {
|
||||
return this.remoteAddress.hashCode();
|
||||
// a unique ID for every connection. However, these ID's can also be reused
|
||||
return this.sessionId;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -16,10 +16,11 @@
|
|||
package dorkbox.network.connection.wrapper;
|
||||
|
||||
import dorkbox.network.connection.ConnectionPoint;
|
||||
import dorkbox.network.connection.ConnectionPointWriter;
|
||||
import io.netty.util.concurrent.ImmediateEventExecutor;
|
||||
import io.netty.util.concurrent.Promise;
|
||||
|
||||
public
|
||||
class ChannelNull implements ConnectionPointWriter {
|
||||
class ChannelNull implements ConnectionPoint {
|
||||
|
||||
private static final ConnectionPoint INSTANCE = new ChannelNull();
|
||||
|
||||
|
@ -51,6 +52,7 @@ class ChannelNull implements ConnectionPointWriter {
|
|||
|
||||
@Override
|
||||
public
|
||||
void flush() {
|
||||
<V> Promise<V> newPromise() {
|
||||
return ImmediateEventExecutor.INSTANCE.newPromise();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,16 +18,15 @@ package dorkbox.network.connection.wrapper;
|
|||
import org.bouncycastle.crypto.params.ParametersWithIV;
|
||||
|
||||
import dorkbox.network.connection.ConnectionImpl;
|
||||
import dorkbox.network.connection.ConnectionPointWriter;
|
||||
import dorkbox.network.connection.ConnectionPoint;
|
||||
import dorkbox.network.connection.ISessionManager;
|
||||
import dorkbox.network.rmi.RmiObjectHandler;
|
||||
import io.netty.channel.EventLoop;
|
||||
|
||||
public
|
||||
interface ChannelWrapper {
|
||||
|
||||
ConnectionPointWriter tcp();
|
||||
ConnectionPointWriter udp();
|
||||
ConnectionPoint tcp();
|
||||
ConnectionPoint udp();
|
||||
|
||||
/**
|
||||
* Initialize the connection with any extra info that is needed but was unavailable at the channel construction.
|
||||
|
@ -39,8 +38,6 @@ interface ChannelWrapper {
|
|||
*/
|
||||
void flush();
|
||||
|
||||
EventLoop getEventLoop();
|
||||
|
||||
/**
|
||||
* @return a threadlocal AES key + IV. key=32 byte, iv=12 bytes (AES-GCM implementation). This is a threadlocal
|
||||
* because multiple protocols can be performing crypto AT THE SAME TIME, and so we have to make sure that operations don't
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
*
|
||||
* 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 dorkbox.network.connection.wrapper;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
public
|
||||
class UdpWrapper {
|
||||
|
||||
private final Object object;
|
||||
private final InetSocketAddress remoteAddress;
|
||||
|
||||
public
|
||||
UdpWrapper(Object object, InetSocketAddress remoteAddress2) {
|
||||
this.object = object;
|
||||
this.remoteAddress = remoteAddress2;
|
||||
}
|
||||
|
||||
public
|
||||
Object object() {
|
||||
return this.object;
|
||||
}
|
||||
|
||||
public
|
||||
InetSocketAddress remoteAddress() {
|
||||
return this.remoteAddress;
|
||||
}
|
||||
}
|
|
@ -31,6 +31,9 @@ class MagicBytes {
|
|||
|
||||
/**
|
||||
* Determines if this buffer is encrypted or not.
|
||||
*
|
||||
* REGISTRATION is the ONLY thing NOT encrypted
|
||||
* encrypted Y/N is always written by the serialization writer, so it is ALWAYS safe to check it here.
|
||||
*/
|
||||
public static
|
||||
boolean isEncrypted(final ByteBuf buffer) {
|
||||
|
|
54
src/dorkbox/network/pipeline/discovery/BroadcastServer.java
Normal file
54
src/dorkbox/network/pipeline/discovery/BroadcastServer.java
Normal file
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright 2018 dorkbox, llc.
|
||||
*
|
||||
* 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 dorkbox.network.pipeline.discovery;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
import dorkbox.network.Broadcast;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public
|
||||
class BroadcastServer {
|
||||
private final org.slf4j.Logger logger;
|
||||
private final ByteBuf discoverResponseBuffer;
|
||||
|
||||
public
|
||||
BroadcastServer() {
|
||||
this.logger = org.slf4j.LoggerFactory.getLogger(BroadcastServer.class.getSimpleName());
|
||||
|
||||
// absolutely MUST send packet > 0 across, otherwise netty will think it failed to write to the socket, and keep trying.
|
||||
// (this bug was fixed by netty, however we are keeping this code)
|
||||
this.discoverResponseBuffer = Unpooled.buffer(1);
|
||||
this.discoverResponseBuffer.writeByte(Broadcast.broadcastResponseID);
|
||||
}
|
||||
|
||||
public ByteBuf getBroadcastResponse(ByteBuf byteBuf, InetSocketAddress remoteAddress) {
|
||||
if (byteBuf.readableBytes() == 1) {
|
||||
// this is a BROADCAST discovery event. Don't read the byte unless it is...
|
||||
if (byteBuf.getByte(0) == Broadcast.broadcastID) {
|
||||
byteBuf.readByte(); // read the byte to consume it (now that we verified it is a broadcast byte)
|
||||
logger.info("Responded to host discovery from: {}", remoteAddress);
|
||||
return discoverResponseBuffer;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
* Copyright 2018 dorkbox, llc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -13,7 +13,7 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.pipeline;
|
||||
package dorkbox.network.pipeline.tcp;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
* Copyright 2018 dorkbox, llc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -13,7 +13,7 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.pipeline;
|
||||
package dorkbox.network.pipeline.tcp;
|
||||
|
||||
import java.io.IOException;
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
* Copyright 2018 dorkbox, llc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -13,7 +13,7 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.pipeline;
|
||||
package dorkbox.network.pipeline.tcp;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
|
@ -27,14 +27,14 @@ import io.netty.handler.codec.MessageToByteEncoder;
|
|||
@Sharable
|
||||
public
|
||||
class KryoEncoder extends MessageToByteEncoder<Object> {
|
||||
// maximum size of length field. Un-optimized will always be 4, but optimized version can take from 1 - 5 (for Integer.MAX_VALUE).
|
||||
private static final int reservedLengthIndex = 5;
|
||||
// maximum size of length field. Un-optimized will always be 4, but optimized version can take from 1 - 4 (for 0-Integer.MAX_VALUE).
|
||||
private static final int reservedLengthIndex = 4;
|
||||
private final CryptoSerializationManager serializationManager;
|
||||
|
||||
|
||||
// When this is a UDP encode, there are ALREADY size limits placed on the buffer, so any extra checks are unnecessary
|
||||
public
|
||||
KryoEncoder(final CryptoSerializationManager serializationManager) {
|
||||
super(false); // just use direct buffers anyways. When using Heap buffers, they because chunked and the backing array is invalid.
|
||||
super(true); // just use direct buffers anyways. When using Heap buffers, they because chunked and the backing array is invalid.
|
||||
this.serializationManager = serializationManager;
|
||||
}
|
||||
|
||||
|
@ -54,7 +54,7 @@ class KryoEncoder extends MessageToByteEncoder<Object> {
|
|||
protected
|
||||
void encode(final ChannelHandlerContext context, final Object msg, final ByteBuf out) throws Exception {
|
||||
// we don't necessarily start at 0!!
|
||||
// START at index = 5. This is to make room for the integer placed by the frameEncoder for TCP.
|
||||
// START at index = 4. This is to make room for the integer placed by the frameEncoder for TCP.
|
||||
int startIndex = out.writerIndex() + reservedLengthIndex;
|
||||
|
||||
if (msg != null) {
|
||||
|
@ -64,7 +64,7 @@ class KryoEncoder extends MessageToByteEncoder<Object> {
|
|||
writeObject(this.serializationManager, context, msg, out);
|
||||
int index = out.writerIndex();
|
||||
|
||||
// now set the frame length (if it's TCP)!
|
||||
// now set the frame length
|
||||
// (reservedLengthLength) 5 is the reserved space for the integer.
|
||||
int length = index - startIndex;
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2010 dorkbox, llc
|
||||
* Copyright 2018 dorkbox, llc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -13,7 +13,7 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.network.pipeline;
|
||||
package dorkbox.network.pipeline.tcp;
|
||||
|
||||
import java.io.IOException;
|
||||
|
|
@ -20,17 +20,19 @@ import java.util.List;
|
|||
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import dorkbox.network.connection.KryoExtra;
|
||||
import dorkbox.network.serialization.CryptoSerializationManager;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.AddressedEnvelope;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandler.Sharable;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.socket.DatagramPacket;
|
||||
import io.netty.handler.codec.MessageToMessageDecoder;
|
||||
import io.netty.handler.timeout.IdleState;
|
||||
import io.netty.handler.timeout.IdleStateEvent;
|
||||
|
||||
@Sharable
|
||||
public
|
||||
class KryoDecoderUdp extends MessageToMessageDecoder<DatagramPacket> {
|
||||
class KryoDecoderUdp extends MessageToMessageDecoder<Object> {
|
||||
|
||||
private final CryptoSerializationManager serializationManager;
|
||||
|
||||
|
@ -39,31 +41,59 @@ class KryoDecoderUdp extends MessageToMessageDecoder<DatagramPacket> {
|
|||
this.serializationManager = serializationManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
boolean acceptInboundMessage(final Object msg) throws Exception {
|
||||
return msg instanceof ByteBuf || msg instanceof AddressedEnvelope;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a {@link Channel} has been idle for a while.
|
||||
*/
|
||||
@Override
|
||||
public
|
||||
void userEventTriggered(ChannelHandlerContext context, Object event) throws Exception {
|
||||
// if (e.getState() == IdleState.READER_IDLE) {
|
||||
// e.getChannel().close();
|
||||
// } else if (e.getState() == IdleState.WRITER_IDLE) {
|
||||
// e.getChannel().write(new Object());
|
||||
// } else
|
||||
if (event instanceof IdleStateEvent) {
|
||||
if (((IdleStateEvent) event).state() == IdleState.ALL_IDLE) {
|
||||
// will auto-flush if necessary
|
||||
// TODO: if we have been idle TOO LONG, then we close this channel!
|
||||
// if we are idle for a much smaller amount of time, then we pass the idle message up to the connection
|
||||
|
||||
// this.sessionManager.onIdle(this);
|
||||
}
|
||||
}
|
||||
|
||||
super.userEventTriggered(context, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected
|
||||
void decode(ChannelHandlerContext ctx, DatagramPacket msg, List<Object> out) throws Exception {
|
||||
if (msg != null) {
|
||||
ByteBuf data = msg.content();
|
||||
void decode(ChannelHandlerContext context, Object message, List<Object> out) throws Exception {
|
||||
ByteBuf data;
|
||||
if (message instanceof AddressedEnvelope) {
|
||||
// this is on the client
|
||||
data = (ByteBuf) ((AddressedEnvelope) message).content();
|
||||
} else {
|
||||
// this is on the server
|
||||
data = (ByteBuf) message;
|
||||
}
|
||||
|
||||
|
||||
if (data != null) {
|
||||
// there is a REMOTE possibility that UDP traffic BEAT the TCP registration traffic, which means that THIS packet
|
||||
// COULD be encrypted!
|
||||
|
||||
if (KryoExtra.isEncrypted(data)) {
|
||||
String message = "Encrypted UDP packet received before registration complete.";
|
||||
LoggerFactory.getLogger(this.getClass()).error(message);
|
||||
throw new IOException(message);
|
||||
} else {
|
||||
try {
|
||||
// no connection here because we haven't created one yet. When we do, we replace this handler with a new one.
|
||||
Object read = serializationManager.read(data, data.writerIndex());
|
||||
out.add(read);
|
||||
} catch (IOException e) {
|
||||
String message = "Unable to deserialize object";
|
||||
LoggerFactory.getLogger(this.getClass()).error(message, e);
|
||||
throw new IOException(message, e);
|
||||
}
|
||||
}
|
||||
String msg = "Unable to deserialize object";
|
||||
LoggerFactory.getLogger(this.getClass())
|
||||
.error(msg, e);
|
||||
throw new IOException(msg, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,8 +42,8 @@ class KryoDecoderUdpCrypto extends MessageToMessageDecoder<DatagramPacket> {
|
|||
|
||||
@Override
|
||||
public
|
||||
void decode(ChannelHandlerContext ctx, DatagramPacket in, List<Object> out) throws Exception {
|
||||
ChannelHandler last = ctx.pipeline()
|
||||
void decode(ChannelHandlerContext context, DatagramPacket in, List<Object> out) throws Exception {
|
||||
ChannelHandler last = context.pipeline()
|
||||
.last();
|
||||
|
||||
try {
|
||||
|
@ -52,7 +52,8 @@ class KryoDecoderUdpCrypto extends MessageToMessageDecoder<DatagramPacket> {
|
|||
out.add(object);
|
||||
} catch (IOException e) {
|
||||
String message = "Unable to deserialize object";
|
||||
LoggerFactory.getLogger(this.getClass()).error(message, e);
|
||||
LoggerFactory.getLogger(this.getClass())
|
||||
.error(message, e);
|
||||
throw new IOException(message, e);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,15 +24,12 @@ import org.slf4j.LoggerFactory;
|
|||
import dorkbox.network.connection.EndPoint;
|
||||
import dorkbox.network.serialization.CryptoSerializationManager;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.ChannelHandler.Sharable;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.socket.DatagramPacket;
|
||||
import io.netty.handler.codec.MessageToMessageEncoder;
|
||||
|
||||
@Sharable
|
||||
// UDP uses messages --- NOT bytebuf!
|
||||
// ONLY USED BY THE CLIENT (the server has it's own handler!)
|
||||
public
|
||||
class KryoEncoderUdp extends MessageToMessageEncoder<Object> {
|
||||
|
||||
|
@ -46,42 +43,45 @@ class KryoEncoderUdp extends MessageToMessageEncoder<Object> {
|
|||
this.serializationManager = serializationManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected
|
||||
void encode(ChannelHandlerContext context, Object message, List<Object> out) throws Exception {
|
||||
if (message != null) {
|
||||
try {
|
||||
ByteBuf outBuffer = context.alloc()
|
||||
.buffer(maxSize);
|
||||
|
||||
// no size info, since this is UDP, it is not segmented
|
||||
writeObject(this.serializationManager, context, message, outBuffer);
|
||||
|
||||
// have to check to see if we are too big for UDP!
|
||||
if (outBuffer.readableBytes() > maxSize) {
|
||||
String msg =
|
||||
"Object is TOO BIG FOR UDP! " + message.toString() + " (Max " + maxSize + ", was " + outBuffer.readableBytes() +
|
||||
")";
|
||||
LoggerFactory.getLogger(this.getClass())
|
||||
.error(msg);
|
||||
throw new IOException(msg);
|
||||
}
|
||||
|
||||
DatagramPacket packet = new DatagramPacket(outBuffer,
|
||||
(InetSocketAddress) context.channel()
|
||||
.remoteAddress());
|
||||
out.add(packet);
|
||||
} catch (Exception e) {
|
||||
String msg = "Unable to serialize object of type: " + message.getClass()
|
||||
.getName();
|
||||
LoggerFactory.getLogger(this.getClass())
|
||||
.error(msg, e);
|
||||
throw new IOException(msg, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the crypto writer will override this
|
||||
void writeObject(CryptoSerializationManager serializationManager, ChannelHandlerContext context, Object msg, ByteBuf buffer)
|
||||
throws IOException {
|
||||
// no connection here because we haven't created one yet. When we do, we replace this handler with a new one.
|
||||
serializationManager.write(buffer, msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected
|
||||
void encode(ChannelHandlerContext ctx, Object msg, List<Object> out) throws Exception {
|
||||
if (msg != null) {
|
||||
try {
|
||||
ByteBuf outBuffer = Unpooled.buffer(maxSize);
|
||||
|
||||
// no size info, since this is UDP, it is not segmented
|
||||
writeObject(this.serializationManager, ctx, msg, outBuffer);
|
||||
|
||||
// have to check to see if we are too big for UDP!
|
||||
if (outBuffer.readableBytes() > maxSize) {
|
||||
|
||||
String message = "Object is TOO BIG FOR UDP! " + msg.toString() + " (Max " + maxSize + ", was " +
|
||||
outBuffer.readableBytes() + ")";
|
||||
LoggerFactory.getLogger(this.getClass()).error(message);
|
||||
throw new IOException(message);
|
||||
}
|
||||
|
||||
DatagramPacket packet = new DatagramPacket(outBuffer,
|
||||
(InetSocketAddress) ctx.channel()
|
||||
.remoteAddress());
|
||||
out.add(packet);
|
||||
} catch (Exception e) {
|
||||
String message = "Unable to serialize object of type: " + msg.getClass()
|
||||
.getName();
|
||||
LoggerFactory.getLogger(this.getClass()).error(message, e);
|
||||
throw new IOException(message, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
211
src/io/netty/channel/socket/nio/DatagramSessionChannel.java
Normal file
211
src/io/netty/channel/socket/nio/DatagramSessionChannel.java
Normal file
|
@ -0,0 +1,211 @@
|
|||
/*
|
||||
* Copyright 2018 dorkbox, llc.
|
||||
*
|
||||
* 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 io.netty.channel.socket.nio;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketAddress;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.*;
|
||||
import io.netty.channel.nio.AbstractNioChannel.NioUnsafe;
|
||||
import io.netty.channel.nio.NioEventLoop;
|
||||
import io.netty.channel.socket.DatagramPacket;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
import io.netty.util.internal.RecyclableArrayList;
|
||||
|
||||
public
|
||||
class DatagramSessionChannel extends AbstractChannel implements Channel {
|
||||
|
||||
private
|
||||
class ChannelUnsafe extends AbstractUnsafe {
|
||||
@Override
|
||||
public
|
||||
void connect(SocketAddress socketAddress, SocketAddress socketAddress2, ChannelPromise channelPromise) {
|
||||
// Connect not supported by ServerChannel implementations
|
||||
channelPromise.setFailure(new UnsupportedOperationException());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static final ChannelMetadata METADATA = new ChannelMetadata(false, 16);
|
||||
protected final DatagramSessionChannelConfig config;
|
||||
|
||||
|
||||
protected final NioServerDatagramChannel serverChannel;
|
||||
protected final InetSocketAddress remote;
|
||||
|
||||
private volatile boolean isOpen = true;
|
||||
private ByteBuf buffer;
|
||||
|
||||
protected
|
||||
DatagramSessionChannel(NioServerDatagramChannel serverChannel, InetSocketAddress remote) {
|
||||
super(serverChannel);
|
||||
this.serverChannel = serverChannel;
|
||||
this.remote = remote;
|
||||
|
||||
config = new DatagramSessionChannelConfig(this, serverChannel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
ChannelConfig config() {
|
||||
return config;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected
|
||||
void doBeginRead() throws Exception {
|
||||
// a single packet is 100% of our data, so we cannot have multiple reads (there is no "session" for UDP)
|
||||
ChannelPipeline pipeline = pipeline();
|
||||
|
||||
pipeline.fireChannelRead(buffer);
|
||||
pipeline.fireChannelReadComplete();
|
||||
buffer = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected
|
||||
void doBind(SocketAddress addr) throws Exception {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected
|
||||
void doClose() throws Exception {
|
||||
isOpen = false;
|
||||
serverChannel.doCloseChannel(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected
|
||||
void doDisconnect() throws Exception {
|
||||
doClose();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected
|
||||
void doWrite(ChannelOutboundBuffer buffer) throws Exception {
|
||||
//transfer all messages that are ready to be written to list
|
||||
final RecyclableArrayList list = RecyclableArrayList.newInstance();
|
||||
boolean free = true;
|
||||
|
||||
try {
|
||||
DatagramPacket buf = null;
|
||||
while ((buf = (DatagramPacket) buffer.current()) != null) {
|
||||
list.add(buf.retain());
|
||||
buffer.remove();
|
||||
}
|
||||
free = false;
|
||||
} finally {
|
||||
if (free) {
|
||||
for (Object obj : list) {
|
||||
ReferenceCountUtil.safeRelease(obj);
|
||||
}
|
||||
list.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
//schedule a task that will write those entries
|
||||
NioEventLoop eventLoop = serverChannel.eventLoop();
|
||||
if (eventLoop.inEventLoop()) {
|
||||
write0(list);
|
||||
}
|
||||
else {
|
||||
eventLoop.submit(new Runnable() {
|
||||
@Override
|
||||
public
|
||||
void run() {
|
||||
write0(list);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
boolean isActive() {
|
||||
return isOpen;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected
|
||||
boolean isCompatible(EventLoop eventloop) {
|
||||
return eventloop instanceof NioEventLoop;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
boolean isOpen() {
|
||||
return isOpen;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
InetSocketAddress localAddress() {
|
||||
return (InetSocketAddress) localAddress0();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected
|
||||
SocketAddress localAddress0() {
|
||||
return serverChannel.localAddress0();
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
ChannelMetadata metadata() {
|
||||
return METADATA;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected
|
||||
AbstractUnsafe newUnsafe() {
|
||||
// cannot connect, so we make this be an error if we try.
|
||||
return new ChannelUnsafe();
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
InetSocketAddress remoteAddress() {
|
||||
return remote;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected
|
||||
InetSocketAddress remoteAddress0() {
|
||||
return remote;
|
||||
}
|
||||
|
||||
public
|
||||
void setBuffer(final ByteBuf buffer) {
|
||||
this.buffer = buffer;
|
||||
}
|
||||
|
||||
private
|
||||
void write0(final RecyclableArrayList list) {
|
||||
try {
|
||||
NioUnsafe unsafe = serverChannel.unsafe();
|
||||
|
||||
for (Object buf : list) {
|
||||
unsafe.write(buf, voidPromise());
|
||||
}
|
||||
|
||||
unsafe.flush();
|
||||
} finally {
|
||||
list.recycle();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,203 @@
|
|||
/*
|
||||
* Copyright 2018 dorkbox, llc.
|
||||
*
|
||||
* 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 io.netty.channel.socket.nio;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import dorkbox.network.connection.EndPoint;
|
||||
import io.netty.buffer.ByteBufAllocator;
|
||||
import io.netty.channel.*;
|
||||
import io.netty.channel.socket.DatagramChannelConfig;
|
||||
|
||||
/**
|
||||
* The default {@link DatagramChannelConfig} implementation.
|
||||
*/
|
||||
public class DatagramSessionChannelConfig implements ChannelConfig {
|
||||
private static final MessageSizeEstimator DEFAULT_MSG_SIZE_ESTIMATOR = DefaultMessageSizeEstimator.DEFAULT;
|
||||
|
||||
|
||||
private volatile MessageSizeEstimator msgSizeEstimator = DEFAULT_MSG_SIZE_ESTIMATOR;
|
||||
|
||||
private final NioServerDatagramChannel serverDatagramSessionChannel;
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
*/
|
||||
public
|
||||
DatagramSessionChannelConfig(DatagramSessionChannel channel, final NioServerDatagramChannel serverDatagramSessionChannel) {
|
||||
this.serverDatagramSessionChannel = serverDatagramSessionChannel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
Map<ChannelOption<?>, Object> getOptions() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
boolean setOptions(final Map<ChannelOption<?>, ?> options) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
<T> T getOption(final ChannelOption<T> option) {
|
||||
return serverDatagramSessionChannel.config().getOption(option);
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
<T> boolean setOption(final ChannelOption<T> option, final T value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
int getConnectTimeoutMillis() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
ChannelConfig setConnectTimeoutMillis(final int connectTimeoutMillis) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
int getMaxMessagesPerRead() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
ChannelConfig setMaxMessagesPerRead(final int maxMessagesPerRead) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
int getWriteSpinCount() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
ChannelConfig setWriteSpinCount(final int writeSpinCount) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
ByteBufAllocator getAllocator() {
|
||||
return serverDatagramSessionChannel.config()
|
||||
.getAllocator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
ChannelConfig setAllocator(final ByteBufAllocator allocator) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
<T extends RecvByteBufAllocator> T getRecvByteBufAllocator() {
|
||||
return serverDatagramSessionChannel.config()
|
||||
.getRecvByteBufAllocator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
ChannelConfig setRecvByteBufAllocator(final RecvByteBufAllocator allocator) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
boolean isAutoRead() {
|
||||
// we implement our own reading from within the DatagramServer context.
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
ChannelConfig setAutoRead(final boolean autoRead) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
boolean isAutoClose() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
ChannelConfig setAutoClose(final boolean autoClose) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
int getWriteBufferHighWaterMark() {
|
||||
return EndPoint.udpMaxSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
ChannelConfig setWriteBufferHighWaterMark(final int writeBufferHighWaterMark) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
int getWriteBufferLowWaterMark() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
ChannelConfig setWriteBufferLowWaterMark(final int writeBufferLowWaterMark) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
MessageSizeEstimator getMessageSizeEstimator() {
|
||||
return msgSizeEstimator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
ChannelConfig setMessageSizeEstimator(final MessageSizeEstimator estimator) {
|
||||
this.msgSizeEstimator = estimator;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
WriteBufferWaterMark getWriteBufferWaterMark() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
ChannelConfig setWriteBufferWaterMark(final WriteBufferWaterMark writeBufferWaterMark) {
|
||||
return this;
|
||||
}
|
||||
}
|
492
src/io/netty/channel/socket/nio/NioServerDatagramChannel.java
Normal file
492
src/io/netty/channel/socket/nio/NioServerDatagramChannel.java
Normal file
|
@ -0,0 +1,492 @@
|
|||
/*
|
||||
* Copyright 2018 dorkbox, llc.
|
||||
*
|
||||
* 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 io.netty.channel.socket.nio;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketAddress;
|
||||
import java.net.SocketException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.SelectionKey;
|
||||
import java.nio.channels.spi.SelectorProvider;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import dorkbox.network.pipeline.discovery.BroadcastServer;
|
||||
import dorkbox.util.bytes.BigEndian.Long_;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.*;
|
||||
import io.netty.channel.nio.AbstractNioMessageChannel;
|
||||
import io.netty.channel.socket.*;
|
||||
import io.netty.util.collection.LongObjectHashMap;
|
||||
import io.netty.util.internal.PlatformDependent;
|
||||
import io.netty.util.internal.SocketUtils;
|
||||
import io.netty.util.internal.StringUtil;
|
||||
import io.netty.util.internal.logging.InternalLogger;
|
||||
import io.netty.util.internal.logging.InternalLoggerFactory;
|
||||
|
||||
/**
|
||||
* An NIO datagram {@link Channel} that sends and receives an
|
||||
* {@link AddressedEnvelope AddressedEnvelope<ByteBuf, SocketAddress>}.
|
||||
*
|
||||
* @see AddressedEnvelope
|
||||
* @see DatagramPacket
|
||||
*/
|
||||
public final
|
||||
class NioServerDatagramChannel extends AbstractNioMessageChannel implements ServerSocketChannel {
|
||||
|
||||
private static final SelectorProvider DEFAULT_SELECTOR_PROVIDER = SelectorProvider.provider();
|
||||
private static final ChannelMetadata METADATA = new ChannelMetadata(true, 16);
|
||||
|
||||
private static final String EXPECTED_TYPES = " (expected: " +
|
||||
StringUtil.simpleClassName(DatagramPacket.class) + ", " +
|
||||
StringUtil.simpleClassName(AddressedEnvelope.class) + '<' +
|
||||
StringUtil.simpleClassName(ByteBuf.class) + ", " +
|
||||
StringUtil.simpleClassName(SocketAddress.class) + ">, " +
|
||||
StringUtil.simpleClassName(ByteBuf.class) + ')';
|
||||
|
||||
private static final InternalLogger logger = InternalLoggerFactory.getInstance(NioServerDatagramChannel.class);
|
||||
|
||||
|
||||
private static
|
||||
java.nio.channels.DatagramChannel newSocket(SelectorProvider provider) {
|
||||
try {
|
||||
/**
|
||||
* Use the {@link SelectorProvider} to open {@link SocketChannel} and so remove condition in
|
||||
* {@link SelectorProvider#provider()} which is called by each DatagramSessionChannel.open() otherwise.
|
||||
*
|
||||
* See <a href="https://github.com/netty/netty/issues/2308">#2308</a>.
|
||||
*/
|
||||
return provider.openDatagramChannel();
|
||||
} catch (IOException e) {
|
||||
throw new ChannelException("Failed to open a socket.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static
|
||||
java.nio.channels.DatagramChannel newSocket(SelectorProvider provider, InternetProtocolFamily ipFamily) {
|
||||
if (ipFamily == null) {
|
||||
return newSocket(provider);
|
||||
}
|
||||
|
||||
checkJavaVersion();
|
||||
|
||||
try {
|
||||
return NioServerDatagramChannel7.newSocket(provider, ipFamily);
|
||||
} catch (IOException e) {
|
||||
throw new ChannelException("Failed to open a socket.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static
|
||||
void checkJavaVersion() {
|
||||
if (PlatformDependent.javaVersion() < 7) {
|
||||
throw new UnsupportedOperationException("Only supported on java 7+.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the specified buffer is a direct buffer and is composed of a single NIO buffer.
|
||||
* (We check this because otherwise we need to make it a non-composite buffer.)
|
||||
*/
|
||||
private static
|
||||
boolean isSingleDirectBuffer(ByteBuf buf) {
|
||||
return buf.isDirect() && buf.nioBufferCount() == 1;
|
||||
}
|
||||
|
||||
private static
|
||||
long getChannelId(final InetSocketAddress remoteAddress) {
|
||||
int address = remoteAddress.getAddress()
|
||||
.hashCode(); // we want it as an int
|
||||
int port = remoteAddress.getPort(); // this is really just 2 bytes
|
||||
|
||||
byte[] combined = new byte[8];
|
||||
combined[0] = (byte) ((port >>> 24) & 0xFF);
|
||||
combined[1] = (byte) ((port >>> 16) & 0xFF);
|
||||
combined[2] = (byte) ((port >>> 8) & 0xFF);
|
||||
combined[3] = (byte) ((port) & 0xFF);
|
||||
combined[4] = (byte) ((address >>> 24) & 0xFF);
|
||||
combined[5] = (byte) ((address >>> 16) & 0xFF);
|
||||
combined[6] = (byte) ((address >>> 8) & 0xFF);
|
||||
combined[7] = (byte) ((address) & 0xFF);
|
||||
|
||||
return Long_.from(combined);
|
||||
}
|
||||
private final ServerSocketChannelConfig config;
|
||||
|
||||
|
||||
// Does not need to be thread safe, because access only happens in the event loop
|
||||
private final LongObjectHashMap<DatagramSessionChannel> datagramChannels = new LongObjectHashMap<DatagramSessionChannel>();
|
||||
private BroadcastServer broadcastServer;
|
||||
|
||||
/**
|
||||
* Create a new instance which will use the Operation Systems default {@link InternetProtocolFamily}.
|
||||
*/
|
||||
public
|
||||
NioServerDatagramChannel() {
|
||||
this(newSocket(DEFAULT_SELECTOR_PROVIDER));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance using the given {@link SelectorProvider}
|
||||
* which will use the Operation Systems default {@link InternetProtocolFamily}.
|
||||
*/
|
||||
public
|
||||
NioServerDatagramChannel(SelectorProvider provider) {
|
||||
this(newSocket(provider));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance using the given {@link InternetProtocolFamily}. If {@code null} is used it will depend
|
||||
* on the Operation Systems default which will be chosen.
|
||||
*/
|
||||
public
|
||||
NioServerDatagramChannel(InternetProtocolFamily ipFamily) {
|
||||
this(newSocket(DEFAULT_SELECTOR_PROVIDER, ipFamily));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance using the given {@link SelectorProvider} and {@link InternetProtocolFamily}.
|
||||
* If {@link InternetProtocolFamily} is {@code null} it will depend on the Operation Systems default
|
||||
* which will be chosen.
|
||||
*/
|
||||
public
|
||||
NioServerDatagramChannel(SelectorProvider provider, InternetProtocolFamily ipFamily) {
|
||||
this(newSocket(provider, ipFamily));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance from the given {@link java.nio.channels.DatagramChannel}.
|
||||
*/
|
||||
public
|
||||
NioServerDatagramChannel(java.nio.channels.DatagramChannel socket) {
|
||||
super(null, socket, SelectionKey.OP_READ);
|
||||
config = new NioServerDatagramChannelConfig(this, socket);
|
||||
broadcastServer = new BroadcastServer();
|
||||
}
|
||||
|
||||
void clearReadPending0() {
|
||||
clearReadPending();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected
|
||||
boolean closeOnReadError(Throwable cause) {
|
||||
// We do not want to close on SocketException when using DatagramSessionChannel as we usually can continue receiving.
|
||||
// See https://github.com/netty/netty/issues/5893
|
||||
if (cause instanceof SocketException) {
|
||||
return false;
|
||||
}
|
||||
return super.closeOnReadError(cause);
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
ServerSocketChannelConfig config() {
|
||||
return config;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected
|
||||
boolean continueOnWriteError() {
|
||||
// Continue on write error as a DatagramSessionChannel can write to multiple remote peers
|
||||
//
|
||||
// See https://github.com/netty/netty/issues/2665
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected
|
||||
void doBind(SocketAddress localAddress) throws Exception {
|
||||
doBind0(localAddress);
|
||||
}
|
||||
|
||||
private
|
||||
void doBind0(SocketAddress localAddress) throws Exception {
|
||||
if (PlatformDependent.javaVersion() >= 7) {
|
||||
SocketUtils.bind(javaChannel(), localAddress);
|
||||
}
|
||||
else {
|
||||
javaChannel().socket()
|
||||
.bind(localAddress);
|
||||
}
|
||||
}
|
||||
|
||||
// Always called from the EventLoop
|
||||
@Override
|
||||
protected
|
||||
void doClose() throws Exception {
|
||||
// have to close all of the fake DatagramChannels as well. Each one will remove itself from the channel map.
|
||||
|
||||
// We make a copy of this b/c of concurrent modification, in the event this is closed BEFORE the child-channels are closed
|
||||
ArrayList<DatagramSessionChannel> channels = new ArrayList<DatagramSessionChannel>(datagramChannels.values());
|
||||
for (DatagramSessionChannel datagramSessionChannel : channels) {
|
||||
datagramSessionChannel.close();
|
||||
}
|
||||
|
||||
javaChannel().close();
|
||||
}
|
||||
|
||||
/**
|
||||
* ADDED to support closing a DatagramSessionChannel. Always called from the EventLoop
|
||||
*/
|
||||
public
|
||||
void doCloseChannel(final DatagramSessionChannel datagramSessionChannel) {
|
||||
InetSocketAddress remoteAddress = datagramSessionChannel.remoteAddress();
|
||||
long channelId = getChannelId(remoteAddress);
|
||||
|
||||
datagramChannels.remove(channelId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected
|
||||
boolean doConnect(SocketAddress remoteAddress, SocketAddress localAddress) throws Exception {
|
||||
// Unnecessary stuff
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected
|
||||
void doDisconnect() throws Exception {
|
||||
// Unnecessary stuff
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected
|
||||
void doFinishConnect() throws Exception {
|
||||
// Unnecessary stuff
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected
|
||||
int doReadMessages(List<Object> buf) throws Exception {
|
||||
java.nio.channels.DatagramChannel ch = javaChannel();
|
||||
ServerSocketChannelConfig config = config();
|
||||
RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle();
|
||||
ChannelPipeline pipeline = pipeline();
|
||||
|
||||
ByteBuf data = allocHandle.allocate(config.getAllocator());
|
||||
allocHandle.attemptedBytesRead(data.writableBytes());
|
||||
boolean free = true;
|
||||
try {
|
||||
ByteBuffer nioData = data.internalNioBuffer(data.writerIndex(), data.writableBytes());
|
||||
int pos = nioData.position();
|
||||
InetSocketAddress remoteAddress = (InetSocketAddress) ch.receive(nioData);
|
||||
if (remoteAddress == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
allocHandle.lastBytesRead(nioData.position() - pos);
|
||||
ByteBuf byteBuf = data.writerIndex(data.writerIndex() + allocHandle.lastBytesRead());
|
||||
// original behavior from NioDatagramChannel.
|
||||
// buf.add(new DatagramPacket(byteBuf, localAddress(), remoteAddress));
|
||||
// free = false;
|
||||
// return 1;
|
||||
|
||||
|
||||
|
||||
// new behavior
|
||||
|
||||
// check to see if it's a broadcast packet or not
|
||||
ByteBuf broadcast = broadcastServer.getBroadcastResponse(byteBuf, remoteAddress);
|
||||
if (broadcast != null) {
|
||||
// don't bother creating channels if this is a broadcast event. Just respond and be finished
|
||||
doWriteBytes(broadcast, remoteAddress);
|
||||
// no messages created (since we directly write to the channel).
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
long channelId = getChannelId(remoteAddress);
|
||||
|
||||
// create a new channel or reuse existing one
|
||||
DatagramSessionChannel channel = datagramChannels.get(channelId);
|
||||
if (channel == null) {
|
||||
try {
|
||||
channel = new DatagramSessionChannel(this, remoteAddress);
|
||||
datagramChannels.put(channelId, channel);
|
||||
|
||||
// This channel is registered automatically AFTER this read method completes
|
||||
} catch (Throwable t) {
|
||||
logger.warn("Failed to create a new datagram channel from a read operation.", t);
|
||||
|
||||
try {
|
||||
channel.close();
|
||||
} catch (Throwable t2) {
|
||||
logger.warn("Failed to close the datagram channel.", t2);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// set the bytes of the datagram channel
|
||||
channel.setBuffer(byteBuf);
|
||||
|
||||
pipeline.fireChannelRead(channel);
|
||||
|
||||
// immediately trigger a read
|
||||
channel.read();
|
||||
|
||||
free = false;
|
||||
// we manually fireChannelRead + read (caller class calls readComplete for us)
|
||||
return 0;
|
||||
} catch (Throwable cause) {
|
||||
PlatformDependent.throwException(cause);
|
||||
return -1; // -1 means to close this channel
|
||||
} finally {
|
||||
if (free) {
|
||||
data.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected
|
||||
boolean doWriteMessage(Object msg, ChannelOutboundBuffer in) throws Exception {
|
||||
final SocketAddress remoteAddress;
|
||||
final ByteBuf data;
|
||||
if (msg instanceof AddressedEnvelope) {
|
||||
@SuppressWarnings("unchecked")
|
||||
AddressedEnvelope<ByteBuf, SocketAddress> envelope = (AddressedEnvelope<ByteBuf, SocketAddress>) msg;
|
||||
remoteAddress = envelope.recipient();
|
||||
data = envelope.content();
|
||||
}
|
||||
else {
|
||||
data = (ByteBuf) msg;
|
||||
remoteAddress = null;
|
||||
}
|
||||
|
||||
return doWriteBytes(data, remoteAddress);
|
||||
}
|
||||
|
||||
private
|
||||
boolean doWriteBytes(final ByteBuf data, final SocketAddress remoteAddress) throws IOException {
|
||||
final int dataLen = data.readableBytes();
|
||||
if (dataLen == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final ByteBuffer nioData = data.internalNioBuffer(data.readerIndex(), dataLen);
|
||||
final int writtenBytes;
|
||||
if (remoteAddress != null) {
|
||||
writtenBytes = javaChannel().send(nioData, remoteAddress);
|
||||
}
|
||||
else {
|
||||
writtenBytes = javaChannel().write(nioData);
|
||||
}
|
||||
return writtenBytes > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected
|
||||
Object filterOutboundMessage(Object msg) {
|
||||
if (msg instanceof DatagramPacket) {
|
||||
DatagramPacket p = (DatagramPacket) msg;
|
||||
ByteBuf content = p.content();
|
||||
if (isSingleDirectBuffer(content)) {
|
||||
return p;
|
||||
}
|
||||
return new DatagramPacket(newDirectBuffer(p, content), p.recipient());
|
||||
}
|
||||
|
||||
if (msg instanceof ByteBuf) {
|
||||
ByteBuf buf = (ByteBuf) msg;
|
||||
if (isSingleDirectBuffer(buf)) {
|
||||
return buf;
|
||||
}
|
||||
return newDirectBuffer(buf);
|
||||
}
|
||||
|
||||
if (msg instanceof AddressedEnvelope) {
|
||||
@SuppressWarnings("unchecked")
|
||||
AddressedEnvelope<Object, SocketAddress> e = (AddressedEnvelope<Object, SocketAddress>) msg;
|
||||
if (e.content() instanceof ByteBuf) {
|
||||
ByteBuf content = (ByteBuf) e.content();
|
||||
if (isSingleDirectBuffer(content)) {
|
||||
return e;
|
||||
}
|
||||
return new DefaultAddressedEnvelope<ByteBuf, SocketAddress>(newDirectBuffer(e, content), e.recipient());
|
||||
}
|
||||
}
|
||||
|
||||
throw new UnsupportedOperationException("unsupported message type: " + StringUtil.simpleClassName(msg) + EXPECTED_TYPES);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("deprecation")
|
||||
public
|
||||
boolean isActive() {
|
||||
java.nio.channels.DatagramChannel ch = javaChannel();
|
||||
// we do not support registration options
|
||||
// return ch.isOpen() && (config.getOption(ChannelOption.DATAGRAM_CHANNEL_ACTIVE_ON_REGISTRATION) && isRegistered() || ch.socket().isBound());
|
||||
return ch.isOpen() || ch.socket()
|
||||
.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected
|
||||
java.nio.channels.DatagramChannel javaChannel() {
|
||||
return (java.nio.channels.DatagramChannel) super.javaChannel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
InetSocketAddress localAddress() {
|
||||
return (InetSocketAddress) super.localAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected
|
||||
SocketAddress localAddress0() {
|
||||
return javaChannel().socket()
|
||||
.getLocalSocketAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
ChannelMetadata metadata() {
|
||||
return METADATA;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
InetSocketAddress remoteAddress() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected
|
||||
SocketAddress remoteAddress0() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
protected
|
||||
void setReadPending(boolean readPending) {
|
||||
super.setReadPending(readPending);
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
String toString() {
|
||||
return "NioServerDatagramChannel";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright 2018 dorkbox, llc.
|
||||
*
|
||||
* 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 io.netty.channel.socket.nio;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.DatagramChannel;
|
||||
import java.nio.channels.spi.SelectorProvider;
|
||||
|
||||
import io.netty.channel.socket.InternetProtocolFamily;
|
||||
|
||||
/**
|
||||
* For Java7+ only!
|
||||
*/
|
||||
public
|
||||
class NioServerDatagramChannel7 {
|
||||
// NOTE: this is suppressed because we compile this for java7, and everything else for java6, and this is only called if we are java7+
|
||||
@SuppressWarnings("Since15")
|
||||
public static
|
||||
DatagramChannel newSocket(final SelectorProvider provider, final InternetProtocolFamily ipFamily) throws IOException {
|
||||
return provider.openDatagramChannel(ProtocolFamilyConverter.convert(ipFamily));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,171 @@
|
|||
/*
|
||||
* Copyright 2018 dorkbox, llc.
|
||||
*
|
||||
* 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 io.netty.channel.socket.nio;
|
||||
|
||||
import java.net.SocketException;
|
||||
import java.nio.channels.DatagramChannel;
|
||||
|
||||
import io.netty.buffer.ByteBufAllocator;
|
||||
import io.netty.channel.*;
|
||||
import io.netty.channel.socket.ServerSocketChannelConfig;
|
||||
|
||||
/**
|
||||
* This is a basic implementation of a ChannelConfig, with the exception that we take in a DatagramSessionChannel, and modify only the
|
||||
* options of that channel that make sense
|
||||
*/
|
||||
public final
|
||||
class NioServerDatagramChannelConfig extends DefaultChannelConfig implements ServerSocketChannelConfig {
|
||||
private final DatagramChannel datagramChannel;
|
||||
|
||||
public
|
||||
NioServerDatagramChannelConfig(NioServerDatagramChannel channel, DatagramChannel datagramChannel) {
|
||||
super(channel);
|
||||
this.datagramChannel = datagramChannel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
int getBacklog() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
ServerSocketChannelConfig setBacklog(final int backlog) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
boolean isReuseAddress() {
|
||||
try {
|
||||
return datagramChannel.socket()
|
||||
.getReuseAddress();
|
||||
} catch (SocketException e) {
|
||||
throw new ChannelException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
ServerSocketChannelConfig setReuseAddress(final boolean reuseAddress) {
|
||||
try {
|
||||
datagramChannel.socket()
|
||||
.setReuseAddress(true);
|
||||
} catch (SocketException e) {
|
||||
throw new ChannelException(e);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
int getReceiveBufferSize() {
|
||||
try {
|
||||
return datagramChannel.socket()
|
||||
.getReceiveBufferSize();
|
||||
} catch (SocketException e) {
|
||||
throw new ChannelException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
ServerSocketChannelConfig setReceiveBufferSize(final int receiveBufferSize) {
|
||||
try {
|
||||
datagramChannel.socket()
|
||||
.setReceiveBufferSize(receiveBufferSize);
|
||||
} catch (SocketException e) {
|
||||
throw new ChannelException(e);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
ServerSocketChannelConfig setPerformancePreferences(final int connectionTime, final int latency, final int bandwidth) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
ServerSocketChannelConfig setConnectTimeoutMillis(int timeout) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public
|
||||
ServerSocketChannelConfig setMaxMessagesPerRead(int n) {
|
||||
super.setMaxMessagesPerRead(n);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
ServerSocketChannelConfig setWriteSpinCount(int spincount) {
|
||||
super.setWriteSpinCount(spincount);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
ServerSocketChannelConfig setAllocator(ByteBufAllocator alloc) {
|
||||
super.setAllocator(alloc);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
ServerSocketChannelConfig setRecvByteBufAllocator(RecvByteBufAllocator alloc) {
|
||||
super.setRecvByteBufAllocator(alloc);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
ServerSocketChannelConfig setAutoRead(boolean autoread) {
|
||||
super.setAutoRead(autoread);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
ServerSocketChannelConfig setWriteBufferHighWaterMark(int writeBufferHighWaterMark) {
|
||||
return (ServerSocketChannelConfig) super.setWriteBufferHighWaterMark(writeBufferHighWaterMark);
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
ServerSocketChannelConfig setWriteBufferLowWaterMark(int writeBufferLowWaterMark) {
|
||||
return (ServerSocketChannelConfig) super.setWriteBufferLowWaterMark(writeBufferLowWaterMark);
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
ServerSocketChannelConfig setWriteBufferWaterMark(WriteBufferWaterMark writeBufferWaterMark) {
|
||||
return (ServerSocketChannelConfig) super.setWriteBufferWaterMark(writeBufferWaterMark);
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
ServerSocketChannelConfig setMessageSizeEstimator(MessageSizeEstimator est) {
|
||||
super.setMessageSizeEstimator(est);
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -35,6 +35,7 @@ import dorkbox.network.serialization.Serialization;
|
|||
import dorkbox.util.exceptions.SecurityException;
|
||||
import dorkbox.util.serialization.SerializationManager;
|
||||
|
||||
@SuppressWarnings("Duplicates")
|
||||
public class ChunkedDataIdleTest extends BaseTest {
|
||||
private volatile boolean success = false;
|
||||
|
||||
|
@ -45,7 +46,7 @@ public class ChunkedDataIdleTest extends BaseTest {
|
|||
|
||||
// have to test sending objects
|
||||
@Test
|
||||
public void ObjectSender() throws SecurityException, IOException {
|
||||
public void SendTcp() throws SecurityException, IOException {
|
||||
final Data mainData = new Data();
|
||||
populateData(mainData);
|
||||
|
||||
|
@ -58,10 +59,36 @@ public class ChunkedDataIdleTest extends BaseTest {
|
|||
register(configuration.serialization);
|
||||
|
||||
sendObject(mainData, configuration, ConnectionType.TCP);
|
||||
}
|
||||
|
||||
// have to test sending objects
|
||||
@Test
|
||||
public void SendUdp() throws SecurityException, IOException {
|
||||
final Data mainData = new Data();
|
||||
populateData(mainData);
|
||||
|
||||
|
||||
System.err.println("-- UDP");
|
||||
configuration = new Configuration();
|
||||
Configuration configuration = new Configuration();
|
||||
configuration.tcpPort = tcpPort;
|
||||
configuration.udpPort = udpPort;
|
||||
configuration.host = host;
|
||||
configuration.serialization = Serialization.DEFAULT();
|
||||
register(configuration.serialization);
|
||||
|
||||
sendObject(mainData, configuration, ConnectionType.UDP);
|
||||
}
|
||||
|
||||
// have to test sending objects
|
||||
@Test
|
||||
public void SendTcpAndUdp() throws SecurityException, IOException {
|
||||
final Data mainData = new Data();
|
||||
populateData(mainData);
|
||||
|
||||
|
||||
|
||||
System.err.println("-- TCP/UDP");
|
||||
Configuration configuration = new Configuration();
|
||||
configuration.tcpPort = tcpPort;
|
||||
configuration.udpPort = udpPort;
|
||||
configuration.host = host;
|
||||
|
@ -75,8 +102,9 @@ public class ChunkedDataIdleTest extends BaseTest {
|
|||
|
||||
private void sendObject(final Data mainData, Configuration configuration, final ConnectionType type)
|
||||
throws SecurityException, IOException {
|
||||
Server server = new Server(configuration);
|
||||
success = false;
|
||||
|
||||
Server server = new Server(configuration);
|
||||
addEndPoint(server);
|
||||
server.setIdleTimeout(10);
|
||||
server.bind(false);
|
||||
|
@ -135,7 +163,7 @@ public class ChunkedDataIdleTest extends BaseTest {
|
|||
data.floats = new float[] {0, -0, 1, -1, 123456, -123456, 0.1f, 0.2f, -0.3f, (float)Math.PI, Float.MAX_VALUE, Float.MIN_VALUE};
|
||||
|
||||
data.doubles = new double[] {0, -0, 1, -1, 123456, -123456, 0.1d, 0.2d, -0.3d, Math.PI, Double.MAX_VALUE, Double.MIN_VALUE};
|
||||
data.longs = new long[] {0, -0, 1, -1, 123456, -123456, 99999999999l, -99999999999l, Long.MAX_VALUE, Long.MIN_VALUE};
|
||||
data.longs = new long[] {0, -0, 1, -1, 123456, -123456, 99999999999L, -99999999999L, Long.MAX_VALUE, Long.MIN_VALUE};
|
||||
data.bytes = new byte[] {-123, 123, -1, 0, 1, Byte.MAX_VALUE, Byte.MIN_VALUE};
|
||||
data.chars = new char[] {32345, 12345, 0, 1, 63, Character.MAX_VALUE, Character.MIN_VALUE};
|
||||
|
||||
|
@ -144,7 +172,7 @@ public class ChunkedDataIdleTest extends BaseTest {
|
|||
data.Shorts = new Short[] {-12345, 12345, -1, 0, 1, Short.MAX_VALUE, Short.MIN_VALUE};
|
||||
data.Floats = new Float[] {0f, -0f, 1f, -1f, 123456f, -123456f, 0.1f, 0.2f, -0.3f, (float)Math.PI, Float.MAX_VALUE, Float.MIN_VALUE};
|
||||
data.Doubles = new Double[] {0d, -0d, 1d, -1d, 123456d, -123456d, 0.1d, 0.2d, -0.3d, Math.PI, Double.MAX_VALUE, Double.MIN_VALUE};
|
||||
data.Longs = new Long[] {0l, -0l, 1l, -1l, 123456l, -123456l, 99999999999l, -99999999999l, Long.MAX_VALUE, Long.MIN_VALUE};
|
||||
data.Longs = new Long[] {0L, -0L, 1L, -1L, 123456L, -123456L, 99999999999L, -99999999999L, Long.MAX_VALUE, Long.MIN_VALUE};
|
||||
data.Bytes = new Byte[] {-123, 123, -1, 0, 1, Byte.MAX_VALUE, Byte.MIN_VALUE};
|
||||
data.Chars = new Character[] {32345, 12345, 0, 1, 63, Character.MAX_VALUE, Character.MIN_VALUE};
|
||||
data.Booleans = new Boolean[] {true, false};
|
||||
|
@ -172,6 +200,7 @@ public class ChunkedDataIdleTest extends BaseTest {
|
|||
manager.register(TYPE.class);
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
static public class Data {
|
||||
public String string;
|
||||
public String[] strings;
|
||||
|
|
|
@ -20,9 +20,9 @@
|
|||
package dorkbox.network;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import dorkbox.network.connection.Connection;
|
||||
|
@ -34,11 +34,13 @@ import dorkbox.util.serialization.SerializationManager;
|
|||
|
||||
public
|
||||
class ConnectionTest extends BaseTest {
|
||||
private AtomicInteger succesCount;
|
||||
|
||||
@Test
|
||||
public
|
||||
void connectLocal() throws SecurityException, IOException {
|
||||
System.out.println("---- " + "Local");
|
||||
succesCount = new AtomicInteger(0);
|
||||
|
||||
Configuration configuration = new Configuration();
|
||||
configuration.localChannelName = EndPoint.LOCAL_CHANNEL;
|
||||
|
@ -49,12 +51,14 @@ class ConnectionTest extends BaseTest {
|
|||
startClient(configuration);
|
||||
|
||||
waitForThreads(10);
|
||||
Assert.assertEquals(3, succesCount.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public
|
||||
void connectTcp() throws SecurityException, IOException {
|
||||
System.out.println("---- " + "TCP");
|
||||
succesCount = new AtomicInteger(0);
|
||||
|
||||
Configuration configuration = new Configuration();
|
||||
configuration.tcpPort = tcpPort;
|
||||
|
@ -67,12 +71,34 @@ class ConnectionTest extends BaseTest {
|
|||
startClient(configuration);
|
||||
|
||||
waitForThreads(10);
|
||||
Assert.assertEquals(3, succesCount.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public
|
||||
void connectUdp() throws SecurityException, IOException {
|
||||
System.out.println("---- " + "UDP");
|
||||
succesCount = new AtomicInteger(0);
|
||||
|
||||
Configuration configuration = new Configuration();
|
||||
configuration.udpPort = udpPort;
|
||||
configuration.serialization = Serialization.DEFAULT();
|
||||
register(configuration.serialization);
|
||||
|
||||
startServer(configuration);
|
||||
|
||||
configuration.host = host;
|
||||
startClient(configuration);
|
||||
|
||||
waitForThreads(10);
|
||||
Assert.assertEquals(3, succesCount.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public
|
||||
void connectTcpUdp() throws SecurityException, IOException {
|
||||
System.out.println("---- " + "TCP UDP");
|
||||
succesCount = new AtomicInteger(0);
|
||||
|
||||
Configuration configuration = new Configuration();
|
||||
configuration.tcpPort = tcpPort;
|
||||
|
@ -86,6 +112,7 @@ class ConnectionTest extends BaseTest {
|
|||
startClient(configuration);
|
||||
|
||||
waitForThreads(10);
|
||||
Assert.assertEquals(3, succesCount.get());
|
||||
}
|
||||
|
||||
private
|
||||
|
@ -97,19 +124,10 @@ class ConnectionTest extends BaseTest {
|
|||
server.bind(false);
|
||||
server.listeners()
|
||||
.add(new Listener.OnConnected<Connection>() {
|
||||
Timer timer = new Timer();
|
||||
|
||||
@Override
|
||||
public
|
||||
void connected(final Connection connection) {
|
||||
this.timer.schedule(new TimerTask() {
|
||||
@Override
|
||||
public
|
||||
void run() {
|
||||
System.out.println("Disconnecting after 1 second.");
|
||||
connection.close();
|
||||
}
|
||||
}, 1000);
|
||||
succesCount.getAndIncrement();
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -118,6 +136,11 @@ class ConnectionTest extends BaseTest {
|
|||
@Override
|
||||
public void received(Connection connection, Object message) {
|
||||
System.err.println("Received message from client: " + message.getClass().getSimpleName());
|
||||
|
||||
succesCount.getAndIncrement();
|
||||
connection.send()
|
||||
.UDP(message);
|
||||
connection.close();
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -140,15 +163,28 @@ class ConnectionTest extends BaseTest {
|
|||
@Override
|
||||
public
|
||||
void disconnected(Connection connection) {
|
||||
succesCount.getAndIncrement();
|
||||
stopEndPoints();
|
||||
}
|
||||
})
|
||||
.add(new Listener.OnMessageReceived<Connection, Object>() {
|
||||
@Override
|
||||
public
|
||||
void received(Connection connection, Object message) {
|
||||
System.err.println("Received message from server: " + message.getClass()
|
||||
.getSimpleName());
|
||||
System.out.println("Now disconnecting!");
|
||||
succesCount.getAndIncrement();
|
||||
connection.close();
|
||||
}
|
||||
});
|
||||
client.connect(5000);
|
||||
|
||||
client.send()
|
||||
.TCP(new BMessage())
|
||||
.flush();
|
||||
.UDP(new BMessage());
|
||||
|
||||
if (true) {
|
||||
throw new RuntimeException("wreha?");
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ class DiscoverHostTest extends BaseTest {
|
|||
void broadcast() throws SecurityException, IOException {
|
||||
|
||||
Configuration configuration = new Configuration();
|
||||
configuration.tcpPort = tcpPort;
|
||||
// configuration.tcpPort = tcpPort;
|
||||
configuration.udpPort = udpPort;
|
||||
configuration.host = host;
|
||||
|
||||
|
@ -55,6 +55,15 @@ class DiscoverHostTest extends BaseTest {
|
|||
return;
|
||||
}
|
||||
|
||||
// run it twice...
|
||||
|
||||
host = Broadcast.discoverHost(udpPort, 2000);
|
||||
if (host == null) {
|
||||
stopEndPoints();
|
||||
fail("No servers found. Maybe you are behind a VPN service or your network is mis-configured?");
|
||||
return;
|
||||
}
|
||||
|
||||
Client client = new Client(configuration);
|
||||
addEndPoint(client);
|
||||
client.listeners()
|
||||
|
|
|
@ -19,10 +19,7 @@
|
|||
*/
|
||||
package dorkbox.network;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
@ -31,11 +28,7 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|||
import org.junit.Test;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import dorkbox.network.connection.Connection;
|
||||
import dorkbox.network.connection.ConnectionImpl;
|
||||
import dorkbox.network.connection.EndPoint;
|
||||
import dorkbox.network.connection.Listener;
|
||||
import dorkbox.network.connection.Listeners;
|
||||
import dorkbox.network.connection.*;
|
||||
import dorkbox.network.rmi.RmiBridge;
|
||||
import dorkbox.util.exceptions.InitializationException;
|
||||
import dorkbox.util.exceptions.SecurityException;
|
||||
|
@ -228,8 +221,7 @@ class ListenerTest extends BaseTest {
|
|||
void received(TestConnectionB connection, String string) {
|
||||
connection.check();
|
||||
System.err.println(string);
|
||||
connection.send()
|
||||
.TCP(string);
|
||||
connection.TCP(string);
|
||||
}
|
||||
});
|
||||
fail("Should not be able to ADD listeners that are NOT the basetype or the interface");
|
||||
|
|
|
@ -94,8 +94,7 @@ class MultipleThreadTest extends BaseTest {
|
|||
//System.err.println(dataClass.data);
|
||||
MultipleThreadTest.this.sentStringsToClientDebug.put(incrementAndGet, dataClass);
|
||||
connection.send()
|
||||
.TCP(dataClass)
|
||||
.flush();
|
||||
.TCP(dataClass);
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
|
|
|
@ -43,7 +43,7 @@ class ReconnectTest extends BaseTest {
|
|||
this.clientCount = new AtomicInteger(0);
|
||||
|
||||
Configuration configuration = new Configuration();
|
||||
configuration.tcpPort = tcpPort;
|
||||
// configuration.tcpPort = tcpPort;
|
||||
configuration.udpPort = udpPort;
|
||||
configuration.host = host;
|
||||
|
||||
|
@ -54,8 +54,8 @@ class ReconnectTest extends BaseTest {
|
|||
@Override
|
||||
public
|
||||
void connected(Connection connection) {
|
||||
connection.send()
|
||||
.TCP("-- TCP from server");
|
||||
// connection.send()
|
||||
// .TCP("-- TCP from server");
|
||||
connection.send()
|
||||
.UDP("-- UDP from server");
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ class ReconnectTest extends BaseTest {
|
|||
public
|
||||
void received(Connection connection, String object) {
|
||||
int incrementAndGet = ReconnectTest.this.serverCount.incrementAndGet();
|
||||
System.err.println("<S " + connection + "> " + incrementAndGet + " : " + object);
|
||||
System.out.println("----- <S " + connection + "> " + incrementAndGet + " : " + object);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -78,8 +78,8 @@ class ReconnectTest extends BaseTest {
|
|||
@Override
|
||||
public
|
||||
void connected(Connection connection) {
|
||||
connection.send()
|
||||
.TCP("-- TCP from client");
|
||||
// connection.send()
|
||||
// .TCP("-- TCP from client");
|
||||
connection.send()
|
||||
.UDP("-- UDP from client");
|
||||
}
|
||||
|
@ -89,18 +89,23 @@ class ReconnectTest extends BaseTest {
|
|||
public
|
||||
void received(Connection connection, String object) {
|
||||
int incrementAndGet = ReconnectTest.this.clientCount.incrementAndGet();
|
||||
System.err.println("<C " + connection + "> " + incrementAndGet + " : " + object);
|
||||
System.out.println("----- <C " + connection + "> " + incrementAndGet + " : " + object);
|
||||
}
|
||||
});
|
||||
|
||||
server.bind(false);
|
||||
int count = 10;
|
||||
|
||||
int count = 100;
|
||||
for (int i = 1; i < count + 1; i++) {
|
||||
client.connect(5000);
|
||||
|
||||
int target = i * 2;
|
||||
int waitingRetryCount = 10;
|
||||
// int target = i * 2;
|
||||
int target = i;
|
||||
while (this.serverCount.get() != target || this.clientCount.get() != target) {
|
||||
System.err.println("Waiting...");
|
||||
if (waitingRetryCount-- < 0) {
|
||||
throw new IOException("Unable to reconnect in 5000 ms");
|
||||
}
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException ignored) {
|
||||
|
@ -139,7 +144,7 @@ class ReconnectTest extends BaseTest {
|
|||
public
|
||||
void received(Connection connection, String object) {
|
||||
int incrementAndGet = ReconnectTest.this.serverCount.incrementAndGet();
|
||||
System.err.println("<S " + connection + "> " + incrementAndGet + " : " + object);
|
||||
System.out.println("----- <S " + connection + "> " + incrementAndGet + " : " + object);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -163,7 +168,7 @@ class ReconnectTest extends BaseTest {
|
|||
public
|
||||
void received(Connection connection, String object) {
|
||||
int incrementAndGet = ReconnectTest.this.clientCount.incrementAndGet();
|
||||
System.err.println("<C " + connection + "> " + incrementAndGet + " : " + object);
|
||||
System.out.println("----- <C " + connection + "> " + incrementAndGet + " : " + object);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -174,7 +179,7 @@ class ReconnectTest extends BaseTest {
|
|||
|
||||
int target = i;
|
||||
while (this.serverCount.get() != target || this.clientCount.get() != target) {
|
||||
System.err.println("Waiting...");
|
||||
System.out.println("----- Waiting...");
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException ex) {
|
||||
|
|
|
@ -171,8 +171,7 @@ class RmiGlobalTest extends BaseTest {
|
|||
m.text = "sometext";
|
||||
|
||||
connection.send()
|
||||
.TCP(m)
|
||||
.flush();
|
||||
.TCP(m);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -165,8 +165,7 @@ class RmiSendObjectOverrideMethodTest extends BaseTest {
|
|||
// that is where that object acutally exists.
|
||||
// we have to manually flush, since we are in a separate thread that does not auto-flush.
|
||||
connection.send()
|
||||
.TCP(otherObject)
|
||||
.flush();
|
||||
.TCP(otherObject);
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
|
|
@ -163,8 +163,7 @@ class RmiSendObjectTest extends BaseTest {
|
|||
// When a remote proxy object is sent, the other side receives its actual remote object.
|
||||
// we have to manually flush, since we are in a separate thread that does not auto-flush.
|
||||
connection.send()
|
||||
.TCP(otherObject)
|
||||
.flush();
|
||||
.TCP(otherObject);
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
|
|
@ -160,8 +160,7 @@ class RmiTest extends BaseTest {
|
|||
m.number = 678;
|
||||
m.text = "sometext";
|
||||
connection.send()
|
||||
.TCP(m)
|
||||
.flush();
|
||||
.TCP(m);
|
||||
|
||||
System.out.println("Finished tests");
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user