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 extends EndPoint> 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 extends EndPoint> type, final Configuration options) throws InitializationException, SecurityException, IOException {
this.type = (Class extends EndPoint>) 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