diff --git a/src/dorkbox/network/Client.java b/src/dorkbox/network/Client.java index 840536cf..f3020a50 100644 --- a/src/dorkbox/network/Client.java +++ b/src/dorkbox/network/Client.java @@ -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 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 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 diff --git a/src/dorkbox/network/Server.java b/src/dorkbox/network/Server.java index a543610c..0ed0ff52 100644 --- a/src/dorkbox/network/Server.java +++ b/src/dorkbox/network/Server.java @@ -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 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 extends EndPointServer { } if (udpPort > 0) { - udpBootstrap = new Bootstrap(); + udpBootstrap = new ServerBootstrap(); } else { udpBootstrap = null; @@ -148,25 +138,25 @@ class Server 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 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 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 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 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 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 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); diff --git a/src/dorkbox/network/connection/ConnectionImpl.java b/src/dorkbox/network/connection/ConnectionImpl.java index ee55b1e8..fac58214 100644 --- a/src/dorkbox/network/connection/ConnectionImpl.java +++ b/src/dorkbox/network/connection/ConnectionImpl.java @@ -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 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> newPromise = this.channelWrapper.getEventLoop() - .newPromise(); + Promise> 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 diff --git a/src/dorkbox/network/connection/ConnectionPoint.java b/src/dorkbox/network/connection/ConnectionPoint.java index 93255ac1..a4d81c3c 100644 --- a/src/dorkbox/network/connection/ConnectionPoint.java +++ b/src/dorkbox/network/connection/ConnectionPoint.java @@ -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. DOES NOT FLUSH the pipe to the wire! */ - void flush(); + void write(Object object) throws Exception; + + /** + * Creates a new promise associated with this connection type + */ + Promise newPromise(); } diff --git a/src/dorkbox/network/connection/ConnectionPointWriter.java b/src/dorkbox/network/connection/ConnectionPointWriter.java deleted file mode 100644 index c72d4484..00000000 --- a/src/dorkbox/network/connection/ConnectionPointWriter.java +++ /dev/null @@ -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. DOES NOT FLUSH the pipe to the wire! - */ - void write(Object object); -} diff --git a/src/dorkbox/network/connection/EndPoint.java b/src/dorkbox/network/connection/EndPoint.java index 07486269..b3687949 100644 --- a/src/dorkbox/network/connection/EndPoint.java +++ b/src/dorkbox/network/connection/EndPoint.java @@ -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 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 getGlobalObject(final int objectRmiId) { + return (T) globalRmiBridge.getRegisteredObject(objectRmiId); } } diff --git a/src/dorkbox/network/connection/EndPointClient.java b/src/dorkbox/network/connection/EndPointClient.java index 6359bc5c..7987c910 100644 --- a/src/dorkbox/network/connection/EndPointClient.java +++ b/src/dorkbox/network/connection/EndPointClient.java @@ -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(); } } diff --git a/src/dorkbox/network/connection/ISessionManager.java b/src/dorkbox/network/connection/ISessionManager.java index cc8d14df..a36ebb30 100644 --- a/src/dorkbox/network/connection/ISessionManager.java +++ b/src/dorkbox/network/connection/ISessionManager.java @@ -20,24 +20,32 @@ import java.util.Collection; public interface ISessionManager { /** - * Called when a message is received + * Called when a message is received. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * 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! diff --git a/src/dorkbox/network/connection/KryoExtra.java b/src/dorkbox/network/connection/KryoExtra.java index 3ab4c50f..9217e3c0 100644 --- a/src/dorkbox/network/connection/KryoExtra.java +++ b/src/dorkbox/network/connection/KryoExtra.java @@ -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 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 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 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 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 extends Kryo { } // write out the "magic" byte. - buffer.writeByte(crypto); + buffer.writeByte(MagicBytes.crypto); // write out our GCM counter OptimizeUtilsByteBuf.writeLong(buffer, nextGcmSequence, true); diff --git a/src/dorkbox/network/connection/RegistrationWrapper.java b/src/dorkbox/network/connection/RegistrationWrapper.java index 80facfbc..e384e858 100644 --- a/src/dorkbox/network/connection/RegistrationWrapper.java +++ b/src/dorkbox/network/connection/RegistrationWrapper.java @@ -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. *

- * 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 channelMap = new IntMap(); - - // keeps track of connections (UDP-server) - @SuppressWarnings({"FieldCanBeLocal", "unused"}) - private volatile ObjectMap 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 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 sessionMap = new LockFreeIntMap(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(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. - *

- * Make SURE to use this in a try/finally block with releaseChannelMap in the finally block! - */ - private - IntMap 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 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 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 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 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 channelMap = getAndLockChannelMap(); - return channelMap.remove(channelHashCodeOrId); - } finally { - releaseChannelMap(); - } - } + + + + + + /** + * MetaChannel allow access to the same "session" across TCP/UDP/etc + *

+ * The connection ID '0' is reserved to mean "no channel ID yet" + */ public - MetaChannel getChannel(final int channelHashCodeOrId) { - try { - IntMap 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. + *

+ * The connection ID '0' is reserved to mean "no channel ID yet" */ public - void closeChannels(final long maxShutdownWaitTimeInMilliSeconds) { - try { - IntMap channelMap = getAndLockChannelMap(); - IntMap.Entries 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 + *

+ * The session ID '0' is reserved to mean "no session ID yet" */ public - MetaChannel closeChannel(final Channel channel, final long maxShutdownWaitTimeInMilliSeconds) { - try { - IntMap channelMap = getAndLockChannelMap(); - IntMap.Entries 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 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 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 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 channelMap = getAndLockChannelMap(); - IntMap.Entries 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 channelMap = getAndLockChannelMap(); - IntMap.Entries entries = channelMap.entries(); + void clearSessions() { + List channels = new LinkedList(); - 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 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(); + } } } diff --git a/src/dorkbox/network/connection/UdpServer.java b/src/dorkbox/network/connection/UdpServer.java deleted file mode 100644 index 3889b243..00000000 --- a/src/dorkbox/network/connection/UdpServer.java +++ /dev/null @@ -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); -} diff --git a/src/dorkbox/network/connection/registration/MetaChannel.java b/src/dorkbox/network/connection/registration/MetaChannel.java index c86029aa..80cdd7f4 100644 --- a/src/dorkbox/network/connection/registration/MetaChannel.java +++ b/src/dorkbox/network/connection/registration/MetaChannel.java @@ -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(); } } diff --git a/src/dorkbox/network/connection/registration/Registration.java b/src/dorkbox/network/connection/registration/Registration.java index 97f77b11..68babda8 100644 --- a/src/dorkbox/network/connection/registration/Registration.java +++ b/src/dorkbox/network/connection/registration/Registration.java @@ -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; + } } diff --git a/src/dorkbox/network/connection/registration/RegistrationHandler.java b/src/dorkbox/network/connection/registration/RegistrationHandler.java index ae7db67e..238cee71 100644 --- a/src/dorkbox/network/connection/registration/RegistrationHandler.java +++ b/src/dorkbox/network/connection/registration/RegistrationHandler.java @@ -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; } } diff --git a/src/dorkbox/network/connection/registration/local/RegistrationLocalHandler.java b/src/dorkbox/network/connection/registration/local/RegistrationLocalHandler.java index dd70536b..4a550861 100644 --- a/src/dorkbox/network/connection/registration/local/RegistrationLocalHandler.java +++ b/src/dorkbox/network/connection/registration/local/RegistrationLocalHandler.java @@ -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 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); } } diff --git a/src/dorkbox/network/connection/registration/local/RegistrationLocalHandlerClient.java b/src/dorkbox/network/connection/registration/local/RegistrationLocalHandlerClient.java index e7bf64a7..d45eba9c 100644 --- a/src/dorkbox/network/connection/registration/local/RegistrationLocalHandlerClient.java +++ b/src/dorkbox/network/connection/registration/local/RegistrationLocalHandlerClient.java @@ -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); } } } diff --git a/src/dorkbox/network/connection/registration/local/RegistrationLocalHandlerServer.java b/src/dorkbox/network/connection/registration/local/RegistrationLocalHandlerServer.java index 9bbea74e..1826732b 100644 --- a/src/dorkbox/network/connection/registration/local/RegistrationLocalHandlerServer.java +++ b/src/dorkbox/network/connection/registration/local/RegistrationLocalHandlerServer.java @@ -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); + } } } } diff --git a/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandler.java b/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandler.java index 8ac921b6..681f1ae4 100644 --- a/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandler.java +++ b/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandler.java @@ -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 aesEngine = new FastThreadLocal() { - @Override - public - GCMBlockCipher initialValue() { - return new GCMBlockCipher(new AESFastEngine()); - } - }; - - final - FastThreadLocal eccEngineLocal = new FastThreadLocal() { - @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 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. - *

- * 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; } } diff --git a/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerClient.java b/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerClient.java index 8a2195b9..396a62dc 100644 --- a/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerClient.java +++ b/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerClient.java @@ -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); + } } diff --git a/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerClientTCP.java b/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerClientTCP.java index fac0b8a0..72330771 100644 --- a/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerClientTCP.java +++ b/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerClientTCP.java @@ -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); } } diff --git a/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerClientUDP.java b/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerClientUDP.java index 4ad07eba..d8f30b73 100644 --- a/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerClientUDP.java +++ b/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerClientUDP.java @@ -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); } } diff --git a/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerServer.java b/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerServer.java index 3bf76438..13fec36b 100644 --- a/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerServer.java +++ b/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerServer.java @@ -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... } } diff --git a/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerServerTCP.java b/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerServerTCP.java index ebd863cd..2c7b92e3 100644 --- a/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerServerTCP.java +++ b/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerServerTCP.java @@ -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); + } } } diff --git a/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerServerUDP.java b/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerServerUDP.java index 19c5de09..604d0c19 100644 --- a/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerServerUDP.java +++ b/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerServerUDP.java @@ -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 { - - // 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 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 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; - } } diff --git a/src/dorkbox/network/connection/wrapper/ChannelLocalWrapper.java b/src/dorkbox/network/connection/wrapper/ChannelLocalWrapper.java index 68f1b04b..9c09211b 100644 --- a/src/dorkbox/network/connection/wrapper/ChannelLocalWrapper.java +++ b/src/dorkbox/network/connection/wrapper/ChannelLocalWrapper.java @@ -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(); + Promise newPromise() { + return channel.eventLoop() + .newPromise(); } @Override diff --git a/src/dorkbox/network/connection/wrapper/ChannelNetwork.java b/src/dorkbox/network/connection/wrapper/ChannelNetwork.java index ebda3ef1..103fe371 100644 --- a/src/dorkbox/network/connection/wrapper/ChannelNetwork.java +++ b/src/dorkbox/network/connection/wrapper/ChannelNetwork.java @@ -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 + Promise newPromise() { + return channel.eventLoop().newPromise(); + } + public void close(long maxShutdownWaitTimeInMilliSeconds) { shouldFlush.set(false); channel.close() .awaitUninterruptibly(maxShutdownWaitTimeInMilliSeconds); } - - public - int id() { - return channel.hashCode(); - } } diff --git a/src/dorkbox/network/connection/wrapper/ChannelNetworkUdp.java b/src/dorkbox/network/connection/wrapper/ChannelNetworkUdp.java deleted file mode 100644 index 3c0498dd..00000000 --- a/src/dorkbox/network/connection/wrapper/ChannelNetworkUdp.java +++ /dev/null @@ -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); - } - } -} diff --git a/src/dorkbox/network/connection/wrapper/ChannelNetworkWrapper.java b/src/dorkbox/network/connection/wrapper/ChannelNetworkWrapper.java index 4729e238..694889da 100644 --- a/src/dorkbox/network/connection/wrapper/ChannelNetworkWrapper.java +++ b/src/dorkbox/network/connection/wrapper/ChannelNetworkWrapper.java @@ -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 diff --git a/src/dorkbox/network/connection/wrapper/ChannelNull.java b/src/dorkbox/network/connection/wrapper/ChannelNull.java index 263739aa..75b0882d 100644 --- a/src/dorkbox/network/connection/wrapper/ChannelNull.java +++ b/src/dorkbox/network/connection/wrapper/ChannelNull.java @@ -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() { + Promise newPromise() { + return ImmediateEventExecutor.INSTANCE.newPromise(); } } diff --git a/src/dorkbox/network/connection/wrapper/ChannelWrapper.java b/src/dorkbox/network/connection/wrapper/ChannelWrapper.java index f3135f5f..cd7e4f4b 100644 --- a/src/dorkbox/network/connection/wrapper/ChannelWrapper.java +++ b/src/dorkbox/network/connection/wrapper/ChannelWrapper.java @@ -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 diff --git a/src/dorkbox/network/connection/wrapper/UdpWrapper.java b/src/dorkbox/network/connection/wrapper/UdpWrapper.java deleted file mode 100644 index 10684e29..00000000 --- a/src/dorkbox/network/connection/wrapper/UdpWrapper.java +++ /dev/null @@ -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; - } -} diff --git a/src/dorkbox/network/pipeline/MagicBytes.java b/src/dorkbox/network/pipeline/MagicBytes.java index 37313c39..13e04e4f 100644 --- a/src/dorkbox/network/pipeline/MagicBytes.java +++ b/src/dorkbox/network/pipeline/MagicBytes.java @@ -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) { diff --git a/src/dorkbox/network/pipeline/discovery/BroadcastServer.java b/src/dorkbox/network/pipeline/discovery/BroadcastServer.java new file mode 100644 index 00000000..a961dfdb --- /dev/null +++ b/src/dorkbox/network/pipeline/discovery/BroadcastServer.java @@ -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; + } +} diff --git a/src/dorkbox/network/pipeline/KryoDecoder.java b/src/dorkbox/network/pipeline/tcp/KryoDecoder.java similarity index 91% rename from src/dorkbox/network/pipeline/KryoDecoder.java rename to src/dorkbox/network/pipeline/tcp/KryoDecoder.java index d8de58ef..0a7fa2d8 100644 --- a/src/dorkbox/network/pipeline/KryoDecoder.java +++ b/src/dorkbox/network/pipeline/tcp/KryoDecoder.java @@ -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; diff --git a/src/dorkbox/network/pipeline/KryoDecoderCrypto.java b/src/dorkbox/network/pipeline/tcp/KryoDecoderCrypto.java similarity index 61% rename from src/dorkbox/network/pipeline/KryoDecoderCrypto.java rename to src/dorkbox/network/pipeline/tcp/KryoDecoderCrypto.java index 676b43d8..19f08b97 100644 --- a/src/dorkbox/network/pipeline/KryoDecoderCrypto.java +++ b/src/dorkbox/network/pipeline/tcp/KryoDecoderCrypto.java @@ -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; diff --git a/src/dorkbox/network/pipeline/KryoEncoder.java b/src/dorkbox/network/pipeline/tcp/KryoEncoder.java similarity index 70% rename from src/dorkbox/network/pipeline/KryoEncoder.java rename to src/dorkbox/network/pipeline/tcp/KryoEncoder.java index 50a603c0..0ade5a6c 100644 --- a/src/dorkbox/network/pipeline/KryoEncoder.java +++ b/src/dorkbox/network/pipeline/tcp/KryoEncoder.java @@ -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 { - // 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 { 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 { 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; diff --git a/src/dorkbox/network/pipeline/KryoEncoderCrypto.java b/src/dorkbox/network/pipeline/tcp/KryoEncoderCrypto.java similarity index 60% rename from src/dorkbox/network/pipeline/KryoEncoderCrypto.java rename to src/dorkbox/network/pipeline/tcp/KryoEncoderCrypto.java index 40339dca..5759e446 100644 --- a/src/dorkbox/network/pipeline/KryoEncoderCrypto.java +++ b/src/dorkbox/network/pipeline/tcp/KryoEncoderCrypto.java @@ -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; diff --git a/src/dorkbox/network/pipeline/udp/KryoDecoderUdp.java b/src/dorkbox/network/pipeline/udp/KryoDecoderUdp.java index f7209e9e..f65be01c 100644 --- a/src/dorkbox/network/pipeline/udp/KryoDecoderUdp.java +++ b/src/dorkbox/network/pipeline/udp/KryoDecoderUdp.java @@ -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 { +class KryoDecoderUdp extends MessageToMessageDecoder { private final CryptoSerializationManager serializationManager; @@ -39,31 +41,59 @@ class KryoDecoderUdp extends MessageToMessageDecoder { 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 out) throws Exception { - if (msg != null) { - ByteBuf data = msg.content(); + void decode(ChannelHandlerContext context, Object message, List 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); } } } diff --git a/src/dorkbox/network/pipeline/udp/KryoDecoderUdpCrypto.java b/src/dorkbox/network/pipeline/udp/KryoDecoderUdpCrypto.java index e18ed3bd..fa79c7c1 100644 --- a/src/dorkbox/network/pipeline/udp/KryoDecoderUdpCrypto.java +++ b/src/dorkbox/network/pipeline/udp/KryoDecoderUdpCrypto.java @@ -42,9 +42,9 @@ class KryoDecoderUdpCrypto extends MessageToMessageDecoder { @Override public - void decode(ChannelHandlerContext ctx, DatagramPacket in, List out) throws Exception { - ChannelHandler last = ctx.pipeline() - .last(); + void decode(ChannelHandlerContext context, DatagramPacket in, List out) throws Exception { + ChannelHandler last = context.pipeline() + .last(); try { ByteBuf data = in.content(); @@ -52,7 +52,8 @@ class KryoDecoderUdpCrypto extends MessageToMessageDecoder { 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); } } diff --git a/src/dorkbox/network/pipeline/udp/KryoEncoderUdp.java b/src/dorkbox/network/pipeline/udp/KryoEncoderUdp.java index 3524ff94..e73c778d 100644 --- a/src/dorkbox/network/pipeline/udp/KryoEncoderUdp.java +++ b/src/dorkbox/network/pipeline/udp/KryoEncoderUdp.java @@ -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 { @@ -46,42 +43,45 @@ class KryoEncoderUdp extends MessageToMessageEncoder { 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 out) throws Exception { - if (msg != null) { + void encode(ChannelHandlerContext context, Object message, List 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); + } } diff --git a/src/dorkbox/network/pipeline/udp/KryoEncoderUdpCrypto.java b/src/dorkbox/network/pipeline/udp/KryoEncoderUdpCrypto.java index f0cbeecb..8373e0f1 100644 --- a/src/dorkbox/network/pipeline/udp/KryoEncoderUdpCrypto.java +++ b/src/dorkbox/network/pipeline/udp/KryoEncoderUdpCrypto.java @@ -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(); diff --git a/src/io/netty/channel/socket/nio/DatagramSessionChannel.java b/src/io/netty/channel/socket/nio/DatagramSessionChannel.java new file mode 100644 index 00000000..66315ec5 --- /dev/null +++ b/src/io/netty/channel/socket/nio/DatagramSessionChannel.java @@ -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(); + } + } +} diff --git a/src/io/netty/channel/socket/nio/DatagramSessionChannelConfig.java b/src/io/netty/channel/socket/nio/DatagramSessionChannelConfig.java new file mode 100644 index 00000000..af5c88ae --- /dev/null +++ b/src/io/netty/channel/socket/nio/DatagramSessionChannelConfig.java @@ -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, Object> getOptions() { + return null; + } + + @Override + public + boolean setOptions(final Map, ?> options) { + return false; + } + + @Override + public + T getOption(final ChannelOption option) { + return serverDatagramSessionChannel.config().getOption(option); + } + + @Override + public + boolean setOption(final ChannelOption 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 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; + } +} diff --git a/src/io/netty/channel/socket/nio/NioServerDatagramChannel.java b/src/io/netty/channel/socket/nio/NioServerDatagramChannel.java new file mode 100644 index 00000000..6e1e2760 --- /dev/null +++ b/src/io/netty/channel/socket/nio/NioServerDatagramChannel.java @@ -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}. + * + * @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 #2308. + */ + 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 datagramChannels = new LongObjectHashMap(); + 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 channels = new ArrayList(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 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 envelope = (AddressedEnvelope) 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 e = (AddressedEnvelope) msg; + if (e.content() instanceof ByteBuf) { + ByteBuf content = (ByteBuf) e.content(); + if (isSingleDirectBuffer(content)) { + return e; + } + return new DefaultAddressedEnvelope(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"; + } +} diff --git a/src/io/netty/channel/socket/nio/NioServerDatagramChannel7.java b/src/io/netty/channel/socket/nio/NioServerDatagramChannel7.java new file mode 100644 index 00000000..87a16110 --- /dev/null +++ b/src/io/netty/channel/socket/nio/NioServerDatagramChannel7.java @@ -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)); + } +} diff --git a/src/io/netty/channel/socket/nio/NioServerDatagramChannelConfig.java b/src/io/netty/channel/socket/nio/NioServerDatagramChannelConfig.java new file mode 100644 index 00000000..d37e469e --- /dev/null +++ b/src/io/netty/channel/socket/nio/NioServerDatagramChannelConfig.java @@ -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; + } +} diff --git a/test/dorkbox/network/ChunkedDataIdleTest.java b/test/dorkbox/network/ChunkedDataIdleTest.java index ec715830..4846ce50 100644 --- a/test/dorkbox/network/ChunkedDataIdleTest.java +++ b/test/dorkbox/network/ChunkedDataIdleTest.java @@ -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; diff --git a/test/dorkbox/network/ConnectionTest.java b/test/dorkbox/network/ConnectionTest.java index d82d94a2..e567f8d7 100644 --- a/test/dorkbox/network/ConnectionTest.java +++ b/test/dorkbox/network/ConnectionTest.java @@ -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() { - 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() { + @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; } diff --git a/test/dorkbox/network/DiscoverHostTest.java b/test/dorkbox/network/DiscoverHostTest.java index df83b3cd..31c2603d 100644 --- a/test/dorkbox/network/DiscoverHostTest.java +++ b/test/dorkbox/network/DiscoverHostTest.java @@ -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() diff --git a/test/dorkbox/network/ListenerTest.java b/test/dorkbox/network/ListenerTest.java index a94b1f73..500e88d2 100644 --- a/test/dorkbox/network/ListenerTest.java +++ b/test/dorkbox/network/ListenerTest.java @@ -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"); diff --git a/test/dorkbox/network/MultipleThreadTest.java b/test/dorkbox/network/MultipleThreadTest.java index c93fc252..7f4f8d31 100644 --- a/test/dorkbox/network/MultipleThreadTest.java +++ b/test/dorkbox/network/MultipleThreadTest.java @@ -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(); diff --git a/test/dorkbox/network/ReconnectTest.java b/test/dorkbox/network/ReconnectTest.java index cb611f91..cb709fb6 100644 --- a/test/dorkbox/network/ReconnectTest.java +++ b/test/dorkbox/network/ReconnectTest.java @@ -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(" " + incrementAndGet + " : " + object); + System.out.println("----- " + 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(" " + incrementAndGet + " : " + object); + System.out.println("----- " + 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(" " + incrementAndGet + " : " + object); + System.out.println("----- " + 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(" " + incrementAndGet + " : " + object); + System.out.println("----- " + 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) { diff --git a/test/dorkbox/network/rmi/RmiGlobalTest.java b/test/dorkbox/network/rmi/RmiGlobalTest.java index e49200a6..2576f4d7 100644 --- a/test/dorkbox/network/rmi/RmiGlobalTest.java +++ b/test/dorkbox/network/rmi/RmiGlobalTest.java @@ -171,8 +171,7 @@ class RmiGlobalTest extends BaseTest { m.text = "sometext"; connection.send() - .TCP(m) - .flush(); + .TCP(m); } diff --git a/test/dorkbox/network/rmi/RmiSendObjectOverrideMethodTest.java b/test/dorkbox/network/rmi/RmiSendObjectOverrideMethodTest.java index 3f12f843..d7f28a50 100644 --- a/test/dorkbox/network/rmi/RmiSendObjectOverrideMethodTest.java +++ b/test/dorkbox/network/rmi/RmiSendObjectOverrideMethodTest.java @@ -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(); } diff --git a/test/dorkbox/network/rmi/RmiSendObjectTest.java b/test/dorkbox/network/rmi/RmiSendObjectTest.java index 59d0fbc2..1660be4d 100644 --- a/test/dorkbox/network/rmi/RmiSendObjectTest.java +++ b/test/dorkbox/network/rmi/RmiSendObjectTest.java @@ -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(); } diff --git a/test/dorkbox/network/rmi/RmiTest.java b/test/dorkbox/network/rmi/RmiTest.java index a5ae1cb0..9915f745 100644 --- a/test/dorkbox/network/rmi/RmiTest.java +++ b/test/dorkbox/network/rmi/RmiTest.java @@ -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"); }