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:
nathan 2018-02-16 21:02:05 +01:00
parent 803f9c5fdb
commit f4e94f2562
56 changed files with 2649 additions and 2181 deletions

View File

@ -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

View File

@ -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.",
future.cause());
throw new IllegalArgumentException(errorMessage);
throw new IllegalArgumentException("Could not bind to address " + hostName + " UDP port " + udpPort + " on the server.",
future.cause());
}
logger.info("Listening on address {} at UDP port: {}", hostName, udpPort);

View File

@ -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()
.newPromise();
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);
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());
}
ConnectionPointWriter tcp = this.channelWrapper.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();
}
}
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);
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());
}
ConnectionPointWriter udp = this.channelWrapper.udp();
udp.write(message);
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

View File

@ -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();
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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!

View File

@ -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);

View File

@ -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,76 +126,73 @@ 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);
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);
}
else {
// COMPARE!
if (!CryptoECC.compare(publicKey, savedPublicKey)) {
String byAddress;
try {
byAddress = InetAddress.getByAddress(hostAddress)
.getHostAddress();
} catch (UnknownHostException e) {
byAddress = "Unknown Address";
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.endPoint.propertyStore.addRegisteredServerKey(hostAddress, publicKey);
}
else {
// COMPARE!
if (!CryptoECC.compare(publicKey, savedPublicKey)) {
String byAddress;
try {
byAddress = InetAddress.getByAddress(hostAddress)
.getHostAddress();
} catch (UnknownHostException e) {
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);
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);
return false;
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);
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();
}
}
/**
* 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
MetaChannel getChannel(final int channelHashCodeOrId) {
try {
IntMap<MetaChannel> channelMap = getAndLockChannelMap();
return channelMap.get(channelHashCodeOrId);
} finally {
releaseChannelMap();
}
MetaChannel createSessionClient(int sessionId) {
MetaChannel metaChannel = new MetaChannel(sessionId);
sessionMap.put(sessionId, metaChannel);
return metaChannel;
}
/**
* 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 createSessionServer() {
int sessionId = RandomUtil.int_();
while (sessionId == 0 && sessionMap.containsKey(sessionId)) {
sessionId = RandomUtil.int_();
}
MetaChannel metaChannel;
synchronized (sessionMap) {
// one final check, but slower...
while (sessionId == 0 && sessionMap.containsKey(sessionId)) {
sessionId = RandomUtil.int_();
}
channelMap.clear();
metaChannel = new MetaChannel(sessionId);
sessionMap.put(sessionId, metaChannel);
} finally {
releaseChannelMap();
// TODO: clean out sessions that are stale!
}
return metaChannel;
}
/**
* 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.
* 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 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;
MetaChannel getSession(final int sessionId) {
return sessionMap.get(sessionId);
}
if (metaChannel.localChannel == channel ||
metaChannel.tcpChannel == channel ||
metaChannel.udpChannel == channel) {
entries.remove();
metaChannel.close(maxShutdownWaitTimeInMilliSeconds);
return metaChannel;
}
}
} finally {
releaseChannelMap();
/**
* @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;
}
/**
* 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!!
* The SERVER AND CLIENT will stop tracking a session once the session is complete.
*/
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();
void removeSession(final MetaChannel metaChannel) {
int sessionId = metaChannel.sessionId;
if (sessionId != 0) {
sessionMap.remove(sessionId);
}
return registerServer;
}
/**
* The SERVER will stop tracking a session if there are errors
*/
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;
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();
}
}
} finally {
releaseChannelMap();
}
return success;
}
/**
* Remove all session associations (keeps the server/client running).
*/
public
MetaChannel getAssociatedChannel_UDP(final InetAddress remoteAddress) {
try {
MetaChannel metaChannel;
IntMap<MetaChannel> channelMap = getAndLockChannelMap();
IntMap.Entries<MetaChannel> entries = channelMap.entries();
void clearSessions() {
List<Channel> channels = new LinkedList<Channel>();
while (entries.hasNext()) {
metaChannel = entries.next().value;
// 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)) {
return metaChannel;
}
else {
return null;
}
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);
}
}
} finally {
releaseChannelMap();
// remote all session associations. Any session in progress will have to restart it's registration process
sessionMap.clear();
}
return null;
// close all "in progress" registrations as well
for (Channel channel : channels) {
channel.close();
}
}
}

View File

@ -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);
}

View File

@ -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 Channel tcpChannel = null;
public Channel udpChannel = null;
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) {
this.udpChannel.close();
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()) {
this.udpChannel.close()
.awaitUninterruptibly(maxShutdownWaitTimeInMilliSeconds);
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();
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -70,17 +70,17 @@ 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
pipeline.addLast(CONNECTION_HANDLER, connection);
if (connection != null) {
// have to setup connection handler
pipeline.addLast(CONNECTION_HANDLER, connection);
registrationWrapper.connectionConnected0(connection);
registrationWrapper.connectionConnected0(connection);
}
}
}
}

View File

@ -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();
///////////////////////
// DECODE (or upstream)
///////////////////////
pipeline.addFirst(FRAME_AND_KRYO_DECODER,
new KryoDecoder(this.serializationManager)); // cannot be shared because of possible fragmentation.
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));
}
/////////////////////////
// ENCODE (or downstream)
/////////////////////////
pipeline.addFirst(FRAME_AND_KRYO_ENCODER, this.registrationWrapper.getKryoEncoder()); // this is shared
if (isTcpChannel) {
/////////////////////////
// ENCODE (or downstream)
/////////////////////////
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,96 +203,18 @@ 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;
}
@ -299,37 +222,108 @@ class RegistrationRemoteHandler extends RegistrationHandler {
}
// 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 setupConnectionCrypto(final MetaChannel metaChannel, final InetSocketAddress remoteAddress) {
if (registerServer) {
// Only called if we have a UDP channel
setupServerUdpConnection(metaChannel);
}
if (this.logger.isDebugEnabled()) {
String type = "";
if (this.logger.isInfoEnabled()) {
String type = "TCP";
if (metaChannel.udpChannel != null) {
type += "/UDP";
if (metaChannel.tcpChannel != null) {
type = "TCP";
if (metaChannel.udpChannel != null) {
type += "/";
}
}
InetSocketAddress address = (InetSocketAddress) metaChannel.tcpChannel.remoteAddress();
this.logger.info("Created a {} connection with {}", type, address.getAddress());
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);
}
}
/**
* 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) {
// have to setup AFTER establish connection, data, as we don't want to enable AES until we're ready.
@SuppressWarnings("AutoUnboxing")
final
void setupConnection(final MetaChannel metaChannel, final Channel channel) {
// Now setup our meta-channel to migrate to the correct connection handler for all regular data.
// 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 = "";
if (metaChannel.tcpChannel != null) {
type = "TCP";
if (metaChannel.udpChannel != null) {
type += "/";
}
}
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;
}
}

View File

@ -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);
}
}

View File

@ -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());
Registration registration = (Registration) message;
//noinspection StatementWithEmptyBody
if (metaChannel != null) {
metaChannel.updateTcpRoundTripTime();
MetaChannel metaChannel;
int sessionId = registration.sessionID;
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();
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);
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);
}
}
}
if (sessionId == 0) {
logger.error("Invalid TCP channel session ID 0!");
shutdown(channel, 0);
return;
}
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);
}
}
readClient(channel, registration, "TCP client", metaChannel);
}
else {
logger2.error("Error registering TCP with remote server!");
shutdown(registrationWrapper2, channel);
logger.error("Error registering TCP with remote server!");
shutdown(channel, 0);
}
ReferenceCountUtil.release(message);
}
}

View File

@ -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)
if (message instanceof Registration) {
Registration registration = (Registration) message;
RegistrationWrapper registrationWrapper2 = this.registrationWrapper;
MetaChannel metaChannel = registrationWrapper2.getChannel(channel.hashCode());
MetaChannel metaChannel;
int sessionId = registration.sessionID;
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);
if (!OptimizeUtilsByteArray.canReadInt(payload)) {
this.logger.error("Invalid decryption of connection ID. Aborting.");
shutdown(registrationWrapper2, channel);
return;
}
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();
}
// 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;
}
if (sessionId == 0) {
logger.error("Invalid UDP channel session ID 0!");
return;
}
else {
metaChannel = registrationWrapper.getSession(sessionId);
if (metaChannel == null) {
metaChannel = registrationWrapper.createSessionClient(sessionId);
logger.debug("New UDP connection. Saving meta-channel id: {}", metaChannel.sessionId);
}
// in the event that we start with a TCP channel first, we still have to set the UDP channel
metaChannel.udpChannel = channel;
}
readClient(channel, registration, "UDP client", metaChannel);
}
else {
logger.error("Error registering UDP with remote server!");
shutdown(channel, 0);
}
// if we get here, there was an error!
this.logger.error("Error registering UDP with remote server!");
shutdown(registrationWrapper2, channel);
}
}

View File

@ -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.
*/
@Override
protected
void setupServerUdpConnection(MetaChannel metaChannel) {
registrationWrapper.registerServerUDP(metaChannel);
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
public
void run() {
logger.trace("Notify Connection");
notifyConnection(metaChannel);
}
}, delay, TimeUnit.MILLISECONDS);
}
// otherwise we have more registrations...
}
}

View File

@ -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();
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);
}
else {
metaChannel = registrationWrapper.getSession(sessionId);
// 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;
}
// 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
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);
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;
}
// 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");
}
notifyConnection(chan2);
}
}, metaChannel.getNanoSecBetweenTCP() * 2, TimeUnit.NANOSECONDS);
}
}
ReferenceCountUtil.release(message);
if (metaChannel == null) {
logger.error("Error getting invalid TCP channel session ID {}! MetaChannel is null!", sessionId);
shutdown(channel, sessionId);
return;
}
}
// this should NEVER happen!
logger2.error("Error registering TCP channel! MetaChannel is null!");
}
shutdown(registrationWrapper2, channel);
ReferenceCountUtil.release(message);
readServer(channel, registration, "TCP server", metaChannel);
}
else {
logger.error("Error registering TCP with remote client!");
shutdown(channel, 0);
}
}
}

View File

@ -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);
}
MetaChannel metaChannel;
int sessionId = 0;
sessionId = registration.sessionID;
if (sessionId == 0) {
metaChannel = registrationWrapper.createSessionServer();
metaChannel.udpChannel = channel;
logger.debug("New UDP connection. Saving meta-channel id: {}", metaChannel.sessionId);
}
else {
logger2.error("Invalid signature for 'Discover Host' from remote address: {}", remoteAddress);
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 {
// 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);
logger.error("Error registering UDP with remote client!");
shutdown(channel, 0);
}
}
// 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.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);
}
}
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);
}
}
else {
logger2.error("UDP attempting to spoof client! Unencrypted packet other than registration received.");
shutdown(null, channel);
}
}
}
/**
* 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;
}
}

View File

@ -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

View File

@ -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();
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
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() {
this.tcp.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;
this.tcp.close(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

View File

@ -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();
}
}

View File

@ -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

View File

@ -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;
}
}

View File

@ -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) {

View 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;
}
}

View File

@ -1,19 +1,19 @@
/*
* 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.
* You may obtain a copy of the License at
* 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
* 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.
* 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;
package dorkbox.network.pipeline.tcp;
import java.io.IOException;
import java.util.List;

View File

@ -1,19 +1,19 @@
/*
* 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.
* You may obtain a copy of the License at
* 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
* 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.
* 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;
package dorkbox.network.pipeline.tcp;
import java.io.IOException;

View File

@ -1,19 +1,19 @@
/*
* 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.
* You may obtain a copy of the License at
* 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
* 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.
* 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;
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;

View File

@ -1,19 +1,19 @@
/*
* 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.
* You may obtain a copy of the License at
* 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
* 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.
* 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;
package dorkbox.network.pipeline.tcp;
import java.io.IOException;

View File

@ -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);
}
}
if (data != null) {
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 msg = "Unable to deserialize object";
LoggerFactory.getLogger(this.getClass())
.error(msg, e);
throw new IOException(msg, e);
}
}
}

View File

@ -42,9 +42,9 @@ class KryoDecoderUdpCrypto extends MessageToMessageDecoder<DatagramPacket> {
@Override
public
void decode(ChannelHandlerContext ctx, DatagramPacket in, List<Object> out) throws Exception {
ChannelHandler last = ctx.pipeline()
.last();
void decode(ChannelHandlerContext context, DatagramPacket in, List<Object> out) throws Exception {
ChannelHandler last = context.pipeline()
.last();
try {
ByteBuf data = in.content();
@ -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);
}
}

View File

@ -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;
}
// 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) {
void encode(ChannelHandlerContext context, Object message, List<Object> out) throws Exception {
if (message != null) {
try {
ByteBuf outBuffer = Unpooled.buffer(maxSize);
ByteBuf outBuffer = context.alloc()
.buffer(maxSize);
// no size info, since this is UDP, it is not segmented
writeObject(this.serializationManager, ctx, msg, outBuffer);
writeObject(this.serializationManager, context, message, 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);
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) ctx.channel()
.remoteAddress());
(InetSocketAddress) context.channel()
.remoteAddress());
out.add(packet);
} catch (Exception e) {
String message = "Unable to serialize object of type: " + msg.getClass()
String msg = "Unable to serialize object of type: " + message.getClass()
.getName();
LoggerFactory.getLogger(this.getClass()).error(message, e);
throw new IOException(message, e);
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);
}
}

View File

@ -35,7 +35,7 @@ class KryoEncoderUdpCrypto extends KryoEncoderUdp {
@Override
void writeObject(CryptoSerializationManager serializationManager, ChannelHandlerContext ctx, Object msg, ByteBuf buffer)
throws IOException {
throws IOException {
ChannelHandler last = ctx.pipeline()
.last();

View 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();
}
}
}

View File

@ -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;
}
}

View 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";
}
}

View File

@ -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));
}
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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()

View File

@ -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");

View File

@ -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();

View File

@ -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) {

View File

@ -171,8 +171,7 @@ class RmiGlobalTest extends BaseTest {
m.text = "sometext";
connection.send()
.TCP(m)
.flush();
.TCP(m);
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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");
}