Java tutorial
/* * 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 java.net.InetAddress; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.security.SecureRandom; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import org.bouncycastle.crypto.AsymmetricCipherKeyPair; import org.bouncycastle.crypto.params.ECPrivateKeyParameters; import org.bouncycastle.crypto.params.ECPublicKeyParameters; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import dorkbox.network.Configuration; import dorkbox.network.connection.bridge.ConnectionBridgeBase; 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.rmi.RmiBridge; import dorkbox.network.serialization.NetworkSerializationManager; import dorkbox.network.serialization.Serialization; import dorkbox.network.store.NullSettingsStore; import dorkbox.network.store.SettingsStore; import dorkbox.util.Property; import dorkbox.util.crypto.CryptoECC; import dorkbox.util.entropy.Entropy; import dorkbox.util.exceptions.SecurityException; import io.netty.channel.local.LocalAddress; import io.netty.handler.codec.ByteToMessageDecoder; import io.netty.handler.timeout.IdleStateHandler; import io.netty.resolver.DefaultNameResolver; import io.netty.resolver.InetSocketAddressResolver; import io.netty.util.NetUtil; /** * represents the base of a client/server end point */ public abstract class EndPoint extends Shutdownable { // 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?? static { // have to load some classes early to prevent stack overflow issues on windows ConnectionImpl.isTcpChannel(null); ConnectionImpl.isUdpChannel(null); Object clazz = ByteToMessageDecoder.class; clazz = IdleStateHandler.class; try { // this class is a private, inner class to IdleStateHandler... clazz = Class.forName("io.netty.handler.timeout.IdleStateHandler$AbstractIdleTask"); } catch (ClassNotFoundException e) { e.printStackTrace(); } clazz = DefaultNameResolver.class; clazz = InetSocketAddressResolver.class; } public static String getHostDetails(final SocketAddress socketAddress) { StringBuilder builder = new StringBuilder(); getHostDetails(builder, socketAddress); return builder.toString(); } public static void getHostDetails(StringBuilder stringBuilder, final SocketAddress socketAddress) { if (socketAddress instanceof InetSocketAddress) { InetSocketAddress address = (InetSocketAddress) socketAddress; InetAddress address1 = address.getAddress(); String hostName = address1.getHostName(); String hostAddress = address1.getHostAddress(); if (!hostName.equals(hostAddress)) { stringBuilder.append(hostName).append('/').append(hostAddress); } else { stringBuilder.append(hostAddress); } stringBuilder.append(':').append(address.getPort()); } else if (socketAddress instanceof LocalAddress) { stringBuilder.append(socketAddress.toString()); } } /** * Defines if we are allowed to use the native OS-specific network interface (non-native to java) for boosted networking performance. */ @Property public static boolean enableNativeLibrary = false; public static final String LOCAL_CHANNEL = "local_channel"; /** * The default size for UDP packets is 768 bytes. * <p/> * You could increase or decrease this value to avoid truncated packets * or to improve memory footprint respectively. * <p/> * 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. * <p/> * 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! * * 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; protected final Configuration config; protected final ConnectionManager connectionManager; protected final NetworkSerializationManager serializationManager; protected final RegistrationWrapper registrationWrapper; final ECPrivateKeyParameters privateKey; final ECPublicKeyParameters publicKey; final SecureRandom secureRandom; final boolean rmiEnabled; // we only want one instance of these created. These will be called appropriately final RmiBridge rmiGlobalBridge; SettingsStore propertyStore; boolean disableRemoteKeyValidation; /** * in milliseconds. default is disabled! */ private volatile int idleTimeoutMs = 0; // the connection status of this endpoint. Once a server has connected to ANY client, it will always return true until server.close() is called protected final AtomicBoolean isConnected = new AtomicBoolean(false); /** * @param type this is either "Client" or "Server", depending on who is creating this endpoint. * @param config these are the specific connection options * * @throws SecurityException if unable to initialize/generate ECC keys */ public EndPoint(Class<? extends EndPoint> type, final Configuration config) throws SecurityException { super(type); this.config = config; // make sure that 'localhost' is ALWAYS our specific loopback IP address if (config.host != null && (config.host.equals("localhost") || config.host.startsWith("127."))) { // localhost IP might not always be 127.0.0.1 config.host = NetUtil.LOCALHOST.getHostAddress(); } // serialization stuff if (config.serialization != null) { serializationManager = config.serialization; } else { serializationManager = Serialization.DEFAULT(); } // setup our RMI serialization managers. Can only be called once rmiEnabled = serializationManager.initRmiSerialization(); // 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 registrationWrapper = new RegistrationWrapper(this, logger); // we have to be able to specify WHAT property store we want to use, since it can change! if (config.settingsStore == null) { propertyStore = new PropertyStore(); } else { propertyStore = config.settingsStore; } propertyStore.init(serializationManager, null); // null it out, since it is sensitive! config.settingsStore = null; if (!(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 = propertyStore.getPrivateKey(); ECPublicKeyParameters publicKey = 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); 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 propertyStore.savePrivateKey(privateKey); propertyStore.savePublicKey(publicKey); logger.debug("Done with ECC keys!"); } catch (Exception e) { String message = "Unable to initialize/generate ECC keys. FORCED SHUTDOWN."; logger.error(message); throw new SecurityException(message); } } this.privateKey = privateKey; this.publicKey = publicKey; } else { this.privateKey = null; this.publicKey = null; } secureRandom = new SecureRandom(propertyStore.getSalt()); // 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, null).getClass()); if (rmiEnabled) { rmiGlobalBridge = new RmiBridge(logger, true); } else { rmiGlobalBridge = null; } Logger readLogger = LoggerFactory.getLogger(type.getSimpleName() + ".READ"); Logger writeLogger = LoggerFactory.getLogger(type.getSimpleName() + ".WRITE"); serializationManager.finishInit(readLogger, writeLogger); } /** * Disables remote endpoint public key validation when the connection is established. This is not recommended as it is a security risk */ public void disableRemoteKeyValidation() { if (isConnected.get()) { logger.error("Cannot disable the remote key validation after this endpoint is connected!"); } else { logger.info("WARNING: Disabling remote key validation is a security risk!!"); 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 extends SettingsStore> S getPropertyStore() { 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 void startNextProtocolRegistration() { } /** * 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 idleTimeoutMs; } /** * The {@link Listener:idle()} will be triggered when neither read nor write * has happened for the specified period of time (in milli-seconds) * <br> * Specify {@code 0} to disable (default). */ public void setIdleTimeout(int idleTimeoutMs) { this.idleTimeoutMs = idleTimeoutMs; } /** * Returns the serialization wrapper if there is an object type that needs to be added outside of the basics. */ public NetworkSerializationManager getSerialization() { return serializationManager; } /** * This method allows the connections used by the client/server to be subclassed (custom implementations). * <p/> * As this is for the network stack, the new connection MUST subclass {@link ConnectionImpl} * <p/> * The parameters are ALL NULL when getting the base class, as this instance is just thrown away. * * @return a new network connection */ protected <E extends EndPoint> ConnectionImpl newConnection(final E endPoint, final ChannelWrapper wrapper) { return new ConnectionImpl(endPoint, wrapper); } /** * 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) * @param remoteAddress be NULL (when getting the baseClass or when creating a local channel) */ final ConnectionImpl connection0(final MetaChannel metaChannel, final InetSocketAddress remoteAddress) { ConnectionImpl connection; // 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; if (metaChannel.localChannel != null) { wrapper = new ChannelLocalWrapper(metaChannel); } else { wrapper = new ChannelNetworkWrapper(metaChannel, remoteAddress); } connection = newConnection(this, wrapper); isConnected.set(true); connectionManager.addConnection(connection); } else { // getting the connection baseClass // have to add the networkAssociate to a map of "connected" computers connection = newConnection(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. * <p/> * Only the CLIENT injects in front of this */ void connectionConnected0(ConnectionImpl connection) { connectionManager.onConnected(connection); } /** * Expose methods to modify the listeners (connect/disconnect/idle/receive events). */ public final Listeners listeners() { return connectionManager; } /** * Returns a non-modifiable list of active connections */ public <C extends Connection> List<C> getConnections() { return connectionManager.getConnections(); } /** * Expose methods to send objects to a destination. */ public abstract ConnectionBridgeBase send(); /** * Safely sends objects to a destination (such as a custom object or a standard ping). This will automatically choose which protocol * is available to use. If you want specify the protocol, use {@link #send()}, followed by the protocol you wish to use. */ public abstract ConnectionPoint send(final Object message); /** * Closes all connections ONLY (keeps the server/client running). To STOP the client/server, use stop(). * <p/> * This is used, for example, when reconnecting to a server. * <p/> * The server should ALWAYS use STOP. */ void closeConnections(boolean shouldKeepListeners) { } /** * Starts the shutdown process during JVM shutdown, if necessary. * </p> * By default, we always can shutdown via the JVM shutdown hook. */ @Override protected boolean shouldShutdownHookRun() { // connectionManager.shutdown accurately reflects the state of the app. Safe to use here return (connectionManager != null && !connectionManager.shutdown.get()); } @Override protected void shutdownChannelsPre() { // this does a closeConnections + clear_listeners connectionManager.stop(); } @Override protected void stopExtraActionsInternal() { // shutdown the database store propertyStore.close(); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + (privateKey == null ? 0 : privateKey.hashCode()); result = prime * result + (publicKey == null ? 0 : publicKey.hashCode()); return result; } @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 (privateKey == null) { if (other.privateKey != null) { return false; } } else if (!CryptoECC.compare(privateKey, other.privateKey)) { return false; } if (publicKey == null) { if (other.publicKey != null) { return false; } } else if (!CryptoECC.compare(publicKey, other.publicKey)) { return false; } return true; } /** * Creates a "global" RMI object for use by multiple connections. * * @return the ID assigned to this RMI object */ public <T> int createGlobalObject(final T globalObject) { return rmiGlobalBridge.register(globalObject); } /** * Gets a previously created "global" RMI object * * @param objectRmiId the ID of the RMI object to get * * @return null if the object doesn't exist or the ID is invalid. */ @SuppressWarnings("unchecked") public <T> T getGlobalObject(final int objectRmiId) { return (T) rmiGlobalBridge.getRegisteredObject(objectRmiId); } }