All Downloads are FREE. Search and download functionalities are using the official Maven repository.

dorkbox.network.connection.EndPoint Maven / Gradle / Ivy

/*
 * 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.Configuration;
import dorkbox.network.connection.bridge.ConnectionBridgeBase;
import dorkbox.network.connection.ping.PingSystemListener;
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.util.CryptoSerializationManager;
import dorkbox.network.util.store.NullSettingsStore;
import dorkbox.network.util.store.SettingsStore;
import dorkbox.util.OS;
import dorkbox.util.Property;
import dorkbox.util.crypto.CryptoECC;
import dorkbox.util.entropy.Entropy;
import dorkbox.util.exceptions.InitializationException;
import dorkbox.util.exceptions.SecurityException;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.util.NetUtil;
import io.netty.util.concurrent.EventExecutor;
import io.netty.util.concurrent.Future;
import io.netty.util.internal.PlatformDependent;
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.slf4j.Logger;

import java.io.IOException;
import java.security.AccessControlException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * represents the base of a client/server end point
 */
public abstract
class EndPoint {
    // If TCP and UDP both fill the pipe, THERE WILL BE FRAGMENTATION and dropped UDP packets!
    // it results in severe UDP packet loss and contention.
    //
    // http://www.isoc.org/INET97/proceedings/F3/F3_1.HTM
    // also, a google search on just "INET97/proceedings/F3/F3_1.HTM" turns up interesting problems.
    // Usually it's with ISPs.

    // TODO: will also want an UDP keepalive? (TCP is already there b/c of socket options, but might need a heartbeat to detect dead connections?)
    //          routers sometimes need a heartbeat to keep the connection
    // TODO: maybe some sort of STUN-like connection keep-alive??


    public static final String LOCAL_CHANNEL = "local_channel";
    protected static final String shutdownHookName = "::SHUTDOWN_HOOK::";
    protected static final String stopTreadName = "::STOP_THREAD::";

    /**
     * The HIGH and LOW watermark points for connections
     */
    @Property
    protected static final int WRITE_BUFF_HIGH = 32 * 1024;
    @Property
    protected static final int WRITE_BUFF_LOW = 8 * 1024;

    public static final String THREADGROUP_NAME = "(Netty)";

    /**
     * this can be changed to a more specialized value, if necessary
     */
    @Property
    public static int DEFAULT_THREAD_POOL_SIZE = Runtime.getRuntime()
                                                        .availableProcessors() * 2;
    /**
     * The amount of time in milli-seconds to wait for this endpoint to close all {@link Channel}s and shutdown gracefully.
     */
    @Property
    public static long maxShutdownWaitTimeInMilliSeconds = 2000L; // in milliseconds

    /**
     * The default size for UDP packets is 768 bytes.
     * 

* You could increase or decrease this value to avoid truncated packets * or to improve memory footprint respectively. *

* Please also note that a large UDP packet might be truncated or * dropped by your router no matter how you configured this option. * In UDP, a packet is truncated or dropped if it is larger than a * certain size, depending on router configuration. IPv4 routers * truncate and IPv6 routers drop a large packet. That's why it is * safe to send small packets in UDP. *

* To fit into that magic 576-byte MTU and avoid fragmentation, your * UDP payload should be restricted by 576-60-8=508 bytes. * * This can be set higher on an internal lan! (or use UDT to make UDP * transfers easy) * * DON'T go higher that 1400 over the internet, but 9k is possible * with jumbo frames on a local network (if it's supported) */ @Property public static int udpMaxSize = 508; // duplicated in DnsClient static { //noinspection Duplicates try { // doesn't work when running from inside eclipse. // Needed for NIO selectors on Android 2.2, and to force IPv4. System.setProperty("java.net.preferIPv4Stack", Boolean.TRUE.toString()); System.setProperty("java.net.preferIPv6Addresses", Boolean.FALSE.toString()); // java6 has stack overflow problems when loading certain classes in it's classloader. The result is a StackOverflow when // loading them normally if (OS.javaVersion == 6) { if (PlatformDependent.hasUnsafe()) { PlatformDependent.newFixedMpscQueue(8); } } } catch (AccessControlException ignored) { } } protected final org.slf4j.Logger logger; protected final ThreadGroup threadGroup; protected final Class> type; protected final ConnectionManager connectionManager; protected final CryptoSerializationManager serializationManager; protected final RegistrationWrapper registrationWrapper; protected final Object shutdownInProgress = new Object(); final ECPrivateKeyParameters privateKey; final ECPublicKeyParameters publicKey; final SecureRandom secureRandom; final RmiBridge globalRmiBridge; private final CountDownLatch blockUntilDone = new CountDownLatch(1); private final Executor rmiExecutor; private final boolean rmiEnabled; // the eventLoop groups are used to track and manage the event loops for startup/shutdown private final List eventLoopGroups = new ArrayList(8); private final List shutdownChannelList = new ArrayList(); // make sure that the endpoint is closed on JVM shutdown (if it's still open at that point in time) private Thread shutdownHook; private AtomicBoolean stopCalled = new AtomicBoolean(false); private AtomicBoolean isConnected = new AtomicBoolean(false); SettingsStore propertyStore; boolean disableRemoteKeyValidation; /** * in milliseconds. default is disabled! */ private volatile int idleTimeoutMs = 0; /** * @param type this is either "Client" or "Server", depending on who is creating this endpoint. * @param options these are the specific connection options * @throws InitializationException * @throws SecurityException */ @SuppressWarnings({"unchecked", "rawtypes"}) public EndPoint(Class type, final Configuration options) throws InitializationException, SecurityException, IOException { this.type = (Class>) type; // setup the thread group to easily ID what the following threads belong to (and their spawned threads...) SecurityManager s = System.getSecurityManager(); threadGroup = new ThreadGroup(s != null ? s.getThreadGroup() : Thread.currentThread() .getThreadGroup(), type.getSimpleName() + " " + THREADGROUP_NAME); threadGroup.setDaemon(true); this.logger = org.slf4j.LoggerFactory.getLogger(type.getSimpleName()); // make sure that 'localhost' is ALWAYS our specific loopback IP address if (options.host != null && (options.host.equals("localhost") || options.host.startsWith("127."))) { // localhost IP might not always be 127.0.0.1 options.host = NetUtil.LOCALHOST.getHostAddress(); } // serialization stuff this.serializationManager = KryoCryptoSerializationManager.DEFAULT; rmiEnabled = options.rmiEnabled; if (rmiEnabled) { // setup our RMI serialization managers. Can only be called once serializationManager.initRmiSerialization(); } rmiExecutor = options.rmiExecutor; // The registration wrapper permits the registration process to access protected/package fields/methods, that we don't want // to expose to external code. "this" escaping can be ignored, because it is benign. //noinspection ThisEscapedInObjectConstruction this.registrationWrapper = new RegistrationWrapper(this, this.logger, new KryoEncoder(this.serializationManager), new KryoEncoderCrypto(this.serializationManager)); // we have to be able to specify WHAT property store we want to use, since it can change! if (options.settingsStore == null) { this.propertyStore = new PropertyStore(); } else { this.propertyStore = options.settingsStore; } this.propertyStore.init(this.serializationManager, null); // null it out, since it is sensitive! options.settingsStore = null; if (!(this.propertyStore instanceof NullSettingsStore)) { // initialize the private/public keys used for negotiating ECC handshakes // these are ONLY used for IP connections. LOCAL connections do not need a handshake! ECPrivateKeyParameters privateKey = this.propertyStore.getPrivateKey(); ECPublicKeyParameters publicKey = this.propertyStore.getPublicKey(); if (privateKey == null || publicKey == null) { try { // seed our RNG based off of this and create our ECC keys byte[] seedBytes = Entropy.get("There are no ECC keys for the " + type.getSimpleName() + " yet"); SecureRandom secureRandom = new SecureRandom(seedBytes); secureRandom.nextBytes(seedBytes); this.logger.debug("Now generating ECC (" + CryptoECC.curve25519 + ") keys. Please wait!"); AsymmetricCipherKeyPair generateKeyPair = CryptoECC.generateKeyPair(CryptoECC.curve25519, secureRandom); privateKey = (ECPrivateKeyParameters) generateKeyPair.getPrivate(); publicKey = (ECPublicKeyParameters) generateKeyPair.getPublic(); // save to properties file this.propertyStore.savePrivateKey(privateKey); this.propertyStore.savePublicKey(publicKey); this.logger.debug("Done with ECC keys!"); } catch (Exception e) { String message = "Unable to initialize/generate ECC keys. FORCED SHUTDOWN."; this.logger.error(message); throw new InitializationException(message); } } this.privateKey = privateKey; this.publicKey = publicKey; } else { this.privateKey = null; this.publicKey = null; } this.secureRandom = new SecureRandom(this.propertyStore.getSalt()); this.shutdownHook = new Thread() { @Override public void run() { // connectionManager.shutdown accurately reflects the state of the app. Safe to use here if (EndPoint.this.connectionManager != null && !EndPoint.this.connectionManager.shutdown.get()) { EndPoint.this.stop(); } } }; this.shutdownHook.setName(shutdownHookName); try { Runtime.getRuntime() .addShutdownHook(this.shutdownHook); } catch (Throwable ignored) { // if we are in the middle of shutdown, we cannot do this. } // we don't care about un-instantiated/constructed members, since the class type is the only interest. this.connectionManager = new ConnectionManager(type.getSimpleName(), connection0(null).getClass()); // add the ping listener (internal use only!) this.connectionManager.add(new PingSystemListener()); if (this.rmiEnabled) { // these register the listener for registering a class implementation for RMI (internal use only) this.connectionManager.add(new RegisterRmiSystemListener()); this.globalRmiBridge = new RmiBridge(logger, options.rmiExecutor, true); } else { this.globalRmiBridge = null; } serializationManager.finishInit(); } /** * Disables remote endpoint public key validation when the connection is established. This is not recommended as it is a security risk */ public void disableRemoteKeyValidation() { Logger logger2 = this.logger; if (isConnected()) { logger2.error("Cannot disable the remote key validation after this endpoint is connected!"); } else { logger2.info("WARNING: Disabling remote key validation is a security risk!!"); this.disableRemoteKeyValidation = true; } } /** * Returns the property store used by this endpoint. The property store can store via properties, * a database, etc, or can be a "null" property store, which does nothing */ @SuppressWarnings("unchecked") public S getPropertyStore() { return (S) this.propertyStore; } /** * 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; } /** * The amount of milli-seconds that must elapse with no read or write before {@link Listener.OnIdle#idle(Connection)} } * will be triggered */ public int getIdleTimeout() { return this.idleTimeoutMs; } /** * The {@link Listener:idle()} will be triggered when neither read nor write * has happened for the specified period of time (in milli-seconds) *
* Specify {@code 0} to disable (default). */ public void setIdleTimeout(int idleTimeoutMs) { this.idleTimeoutMs = idleTimeoutMs; } /** * Return the connection status of this endpoint. *

* Once a server has connected to ANY client, it will always return true until server.close() is called */ public final boolean isConnected() { return this.isConnected.get(); } /** * Add a channel future to be tracked and managed for shutdown. */ protected final void manageForShutdown(ChannelFuture future) { synchronized (this.shutdownChannelList) { this.shutdownChannelList.add(future); } } /** * Add an eventloop group to be tracked & managed for shutdown */ protected final void manageForShutdown(EventLoopGroup loopGroup) { synchronized (this.eventLoopGroups) { this.eventLoopGroups.add(loopGroup); } } /** * Returns the serialization wrapper if there is an object type that needs to be added outside of the basics. */ public CryptoSerializationManager getSerialization() { return this.serializationManager; } /** * This method allows the connections used by the client/server to be subclassed (custom implementations). *

* As this is for the network stack, the new connection MUST subclass {@link ConnectionImpl} *

* The parameters are ALL NULL when getting the base class, as this instance is just thrown away. * * @return a new network connection */ protected ConnectionImpl newConnection(final Logger logger, final EndPoint endPoint, final RmiBridge rmiBridge) { return new ConnectionImpl(logger, endPoint, rmiBridge); } /** * Internal call by the pipeline when: * - creating a new network connection * - when determining the baseClass for listeners * * @param metaChannel can be NULL (when getting the baseClass) */ @SuppressWarnings("unchecked") protected final Connection connection0(MetaChannel metaChannel) { ConnectionImpl connection; RmiBridge rmiBridge = null; if (metaChannel != null && rmiEnabled) { rmiBridge = new RmiBridge(logger, rmiExecutor, false); } // setup the extras needed by the network connection. // These properties are ASSIGNED in the same thread that CREATED the object. Only the AES info needs to be // volatile since it is the only thing that changes. if (metaChannel != null) { ChannelWrapper wrapper; connection = newConnection(logger, this, rmiBridge); metaChannel.connection = connection; if (metaChannel.localChannel != null) { wrapper = new ChannelLocalWrapper(metaChannel); } else { if (this instanceof EndPointServer) { wrapper = new ChannelNetworkWrapper(metaChannel, this.registrationWrapper); } else { wrapper = new ChannelNetworkWrapper(metaChannel, null); } } // now initialize the connection channels with whatever extra info they might need. connection.init(wrapper, (ConnectionManager) this.connectionManager); if (rmiBridge != null) { // notify our remote object space that it is able to receive method calls. connection.listeners() .add(rmiBridge.getListener()); } } else { // getting the connection baseClass // have to add the networkAssociate to a map of "connected" computers connection = newConnection(null, null, null); } return connection; } /** * Internal call by the pipeline to notify the "Connection" object that it has "connected", meaning that modifications * to the pipeline are finished. *

* Only the CLIENT injects in front of this) */ @SuppressWarnings("unchecked") void connectionConnected0(ConnectionImpl connection) { this.isConnected.set(true); // prep the channel wrapper connection.prep(); this.connectionManager.connectionConnected((C) connection); } /** * Expose methods to modify the listeners (connect/disconnect/idle/receive events). */ public final ListenerBridge listeners() { return this.connectionManager; } /** * Returns a non-modifiable list of active connections */ public List getConnections() { return this.connectionManager.getConnections(); } /** * Returns a non-modifiable list of active connections */ @SuppressWarnings("unchecked") public Collection getConnectionsAs() { return this.connectionManager.getConnections(); } /** * Expose methods to send objects to a destination. */ public abstract ConnectionBridgeBase send(); /** * Closes all connections ONLY (keeps the server/client running). To STOP the client/server, use stop(). *

* This is used, for example, when reconnecting to a server. *

* The server should ALWAYS use STOP. */ public void closeConnections() { // give a chance to other threads. Thread.yield(); // stop does the same as this + more this.connectionManager.closeConnections(); // Sometimes there might be "lingering" connections (ie, halfway though registration) that need to be closed. this.registrationWrapper.closeChannels(maxShutdownWaitTimeInMilliSeconds); this.isConnected.set(false); } // server only does this on stop. Client does this on closeConnections protected void shutdownChannels() { synchronized (shutdownChannelList) { // now we stop all of our channels for (ChannelFuture f : this.shutdownChannelList) { Channel channel = f.channel(); channel.close() .awaitUninterruptibly(maxShutdownWaitTimeInMilliSeconds); Thread.yield(); } // we have to clear the shutdown list. ( this.shutdownChannelList.clear(); } } protected final String stopWithErrorMessage(Logger logger2, String errorMessage, Throwable throwable) { if (logger2.isDebugEnabled() && throwable != null) { // extra info if debug is enabled logger2.error(errorMessage, throwable.getCause()); } else { logger2.error(errorMessage); } stop(); return errorMessage; } /** * Safely closes all associated resources/threads/connections. *

* If we want to WAIT for this endpoint to shutdown, we must explicitly call waitForShutdown() *

* Override stopExtraActions() if you want to provide extra behavior while stopping the endpoint */ public final void stop() { // only permit us to "stop" once! if (!this.stopCalled.compareAndSet(false, true)) { return; } // check to make sure we are in our OWN thread, otherwise, this thread will never exit -- because it will wait indefinitely // for itself to finish (since it blocks itself). // This occurs when calling stop from within a listener callback. Thread currentThread = Thread.currentThread(); String threadName = currentThread.getName(); boolean inShutdownThread = !threadName.equals(shutdownHookName) && !threadName.equals(stopTreadName); // used to check the event groups to see if we are running from one of them. NOW we force to // ALWAYS shutdown inside a NEW thread if (!inShutdownThread) { stopInThread(); } else { // we have to make sure always run this from within it's OWN thread -- because if it's run from within // a client/server thread executor, it will deadlock while waiting for the threadpool to terminate. boolean isInEventLoop = false; for (EventLoopGroup loopGroup : this.eventLoopGroups) { for (EventExecutor child : loopGroup.children()) { if (child.inEventLoop()) { isInEventLoop = true; break; } } } if (!isInEventLoop) { EndPoint.this.stopInThread(); } else { Thread thread = new Thread(new Runnable() { @Override public void run() { EndPoint.this.stopInThread(); } }); thread.setDaemon(false); thread.setName(stopTreadName); thread.start(); } } } // This actually does the "stopping", since there is some logic to making sure we don't deadlock, this is important private void stopInThread() { // make sure we are not trying to stop during a startup procedure. // This will wait until we have finished starting up/shutting down. synchronized (this.shutdownInProgress) { // we want to WAIT until after the event executors have completed shutting down. List> shutdownThreadList = new LinkedList>(); for (EventLoopGroup loopGroup : this.eventLoopGroups) { shutdownThreadList.add(loopGroup.shutdownGracefully(maxShutdownWaitTimeInMilliSeconds, maxShutdownWaitTimeInMilliSeconds * 4, TimeUnit.MILLISECONDS)); Thread.yield(); } // now wait for them to finish! // It can take a few seconds to shut down the executor. This will affect unit testing, where connections are quickly created/stopped for (Future f : shutdownThreadList) { f.syncUninterruptibly(); Thread.yield(); } closeConnections(); // this does a closeConnections + clear_listeners this.connectionManager.stop(); shutdownChannels(); this.logger.info("Stopping endpoint"); // there is no need to call "stop" again if we close the connection. // however, if this is called WHILE from the shutdown hook, blammo! problems! // Also, you can call client/server.stop from another thread, which is run when the JVM is shutting down // (as there is nothing left to do), and also have problems. if (!Thread.currentThread() .getName() .equals(shutdownHookName)) { try { Runtime.getRuntime() .removeShutdownHook(this.shutdownHook); } catch (Exception e) { // ignore } } // shutdown the database store this.propertyStore.close(); // when the eventloop closes, the associated selectors are ALSO closed! stopExtraActions(); // we also want to stop the thread group threadGroup.interrupt(); } // tell the blocked "bind" method that it may continue (and exit) this.blockUntilDone.countDown(); } /** * Extra EXTERNAL actions to perform when stopping this endpoint. */ public void stopExtraActions() { } /** * Blocks the current thread until the endpoint has been stopped. If the endpoint is already stopped, this do nothing. */ public final void waitForShutdown() { // we now BLOCK until the stop method is called. try { this.blockUntilDone.await(); } catch (InterruptedException e) { this.logger.error("Thread interrupted while waiting for stop!"); } } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + (this.privateKey == null ? 0 : this.privateKey.hashCode()); result = prime * result + (this.publicKey == null ? 0 : this.publicKey.hashCode()); return result; } @SuppressWarnings("rawtypes") @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } EndPoint other = (EndPoint) obj; if (this.privateKey == null) { if (other.privateKey != null) { return false; } } else if (!CryptoECC.compare(this.privateKey, other.privateKey)) { return false; } if (this.publicKey == null) { if (other.publicKey != null) { return false; } } else if (!CryptoECC.compare(this.publicKey, other.publicKey)) { return false; } return true; } @Override public String toString() { return "EndPoint [" + getName() + "]"; } public String getName() { return this.type.getSimpleName(); } /** * Creates a "global" RMI object for use by multiple connections. * @return the ID assigned to this RMI object */ public int createGlobalObject(final T globalObject) { int globalObjectId = globalRmiBridge.nextObjectId(); globalRmiBridge.register(globalObjectId, globalObject); return globalObjectId; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy