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

org.apache.commons.ssl.RMISocketFactoryImpl Maven / Gradle / Ivy

The newest version!
/*
 * $HeadURL: file:///opt/dev/not-yet-commons-ssl-SVN-repo/tags/commons-ssl-0.3.17/src/java/org/apache/commons/ssl/RMISocketFactoryImpl.java $
 * $Revision: 166 $
 * $Date: 2014-04-28 11:40:25 -0700 (Mon, 28 Apr 2014) $
 *
 * ====================================================================
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * .
 *
 */

package org.apache.commons.ssl;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLProtocolException;
import javax.net.ssl.SSLSocket;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.io.EOFException;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.rmi.server.RMISocketFactory;
import java.security.GeneralSecurityException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;


/**
 * An RMISocketFactory ideal for using RMI over SSL.  The server secures both
 * the registry and the remote objects.  The client assumes that either both
 * the registry and the remote objects will use SSL, or both will use
 * plain-socket.  The client is able to auto detect plain-socket registries
 * and downgrades itself to accomodate those.
 * 

* Unlike most existing RMI over SSL solutions in use (including Java 5's * javax.rmi.ssl.SslRMIClientSocketFactory), this one does proper SSL hostname * verification. From the client perspective this is straighforward. From * the server perspective we introduce a clever trick: we perform an initial * "hostname verification" by trying the current value of * "java.rmi.server.hostname" against our server certificate. If the * "java.rmi.server.hostname" System Property isn't set, we set it ourselves * using the CN value we extract from our server certificate! (Some * complications arise should a wildcard certificate show up, but we try our * best to deal with those). *

* An SSL server cannot be started without a private key. We have defined some * default behaviour for trying to find a private key to use that we believe * is convenient and sensible: *

* If running from inside Tomcat, we try to re-use Tomcat's private key and * certificate chain (assuming Tomcat-SSL on port 8443 is enabled). If this * isn't available, we look for the "javax.net.ssl.keyStore" System property. * Finally, if that isn't available, we look for "~/.keystore" and assume * a password of "changeit". *

* If after all these attempts we still failed to find a private key, the * RMISocketFactoryImpl() constructor will throw an SSLException. * * @author Credit Union Central of British Columbia * @author www.cucbc.com * @author [email protected] * @since 22-Apr-2005 */ public class RMISocketFactoryImpl extends RMISocketFactory { public final static String RMI_HOSTNAME_KEY = "java.rmi.server.hostname"; private final static Log log = LogFactory.getLog(RMISocketFactoryImpl.class); private volatile SocketFactory defaultClient; private volatile ServerSocketFactory sslServer; private volatile String localBindAddress = null; private volatile int anonymousPort = 31099; private Map clientMap = new TreeMap(); private Map serverSockets = new HashMap(); private final SocketFactory plainClient = SocketFactory.getDefault(); public RMISocketFactoryImpl() throws GeneralSecurityException, IOException { this(true); } /** * @param createDefaultServer If false, then we only set the default * client, and the default server is set to null. * If true, then a default server is also created. * @throws GeneralSecurityException bad things * @throws IOException bad things */ public RMISocketFactoryImpl(boolean createDefaultServer) throws GeneralSecurityException, IOException { SSLServer defaultServer = createDefaultServer ? new SSLServer() : null; SSLClient defaultClient = new SSLClient(); // RMI calls to localhost will not check that host matches CN in // certificate. Hopefully this is acceptable. (The registry server // will followup the registry lookup with the proper DNS name to get // the remote object, anyway). HostnameVerifier verifier = HostnameVerifier.DEFAULT_AND_LOCALHOST; defaultClient.setHostnameVerifier(verifier); if (defaultServer != null) { defaultServer.setHostnameVerifier(verifier); // The RMI server will try to re-use Tomcat's "port 8443" SSL // Certificate if possible. defaultServer.useTomcatSSLMaterial(); X509Certificate[] x509 = defaultServer.getAssociatedCertificateChain(); if (x509 == null || x509.length < 1) { throw new SSLException("Cannot initialize RMI-SSL Server: no KeyMaterial!"); } setServer(defaultServer); } setDefaultClient(defaultClient); } public void setServer(ServerSocketFactory f) throws GeneralSecurityException, IOException { this.sslServer = f; if (f instanceof SSLServer) { final HostnameVerifier VERIFIER; VERIFIER = HostnameVerifier.DEFAULT_AND_LOCALHOST; final SSLServer ssl = (SSLServer) f; final X509Certificate[] chain = ssl.getAssociatedCertificateChain(); String[] cns = Certificates.getCNs(chain[0]); String[] subjectAlts = Certificates.getDNSSubjectAlts(chain[0]); LinkedList names = new LinkedList(); if (cns != null && cns.length > 0) { // Only first CN is used. Not going to get into the IE6 nonsense // where all CN values are used. names.add(cns[0]); } if (subjectAlts != null && subjectAlts.length > 0) { names.addAll(Arrays.asList(subjectAlts)); } String rmiHostName = System.getProperty(RMI_HOSTNAME_KEY); // If "java.rmi.server.hostname" is already set, don't mess with it. // But blowup if it's not going to work with our SSL Server // Certificate! if (rmiHostName != null) { try { VERIFIER.check(rmiHostName, cns, subjectAlts); } catch (SSLException ssle) { String s = ssle.toString(); throw new SSLException(RMI_HOSTNAME_KEY + " of " + rmiHostName + " conflicts with SSL Server Certificate: " + s); } } else { // If SSL Cert only contains one non-wild name, just use that and // hope for the best. boolean hopingForBest = false; if (names.size() == 1) { String name = (String) names.get(0); if (!name.startsWith("*")) { System.setProperty(RMI_HOSTNAME_KEY, name); log.warn("commons-ssl '" + RMI_HOSTNAME_KEY + "' set to '" + name + "' as found in my SSL Server Certificate."); hopingForBest = true; } } if (!hopingForBest) { // Help me, Obi-Wan Kenobi; you're my only hope. All we can // do now is grab our internet-facing addresses, reverse-lookup // on them, and hope that one of them validates against our // server cert. Set s = getMyInternetFacingIPs(); Iterator it = s.iterator(); while (it.hasNext()) { String name = (String) it.next(); try { VERIFIER.check(name, cns, subjectAlts); System.setProperty(RMI_HOSTNAME_KEY, name); log.warn("commons-ssl '" + RMI_HOSTNAME_KEY + "' set to '" + name + "' as found by reverse-dns against my own IP."); hopingForBest = true; break; } catch (SSLException ssle) { // next! } } } if (!hopingForBest) { throw new SSLException("'" + RMI_HOSTNAME_KEY + "' not present. Must work with my SSL Server Certificate's CN field: " + names); } } } trustOurself(); } public void setLocalBindAddress(String localBindAddress) { this.localBindAddress = localBindAddress; } public void setAnonymousPort(int port) { this.anonymousPort = port; } public void setDefaultClient(SocketFactory f) throws GeneralSecurityException, IOException { this.defaultClient = f; trustOurself(); } public void setClient(String host, SocketFactory f) throws GeneralSecurityException, IOException { if (f != null && sslServer != null) { boolean clientIsCommonsSSL = f instanceof SSLClient; boolean serverIsCommonsSSL = sslServer instanceof SSLServer; if (clientIsCommonsSSL && serverIsCommonsSSL) { SSLClient c = (SSLClient) f; SSLServer s = (SSLServer) sslServer; trustEachOther(c, s); } } Set names = hostnamePossibilities(host); Iterator it = names.iterator(); synchronized (this) { while (it.hasNext()) { clientMap.put(it.next(), f); } } } public void removeClient(String host) { Set names = hostnamePossibilities(host); Iterator it = names.iterator(); synchronized (this) { while (it.hasNext()) { clientMap.remove(it.next()); } } } public synchronized void removeClient(SocketFactory sf) { Iterator it = clientMap.entrySet().iterator(); while (it.hasNext()) { Map.Entry entry = (Map.Entry) it.next(); Object o = entry.getValue(); if (sf.equals(o)) { it.remove(); } } } private Set hostnamePossibilities(String host) { host = host != null ? host.toLowerCase().trim() : ""; if ("".equals(host)) { return Collections.EMPTY_SET; } TreeSet names = new TreeSet(); names.add(host); InetAddress[] addresses; try { // If they gave us "hostname.com", this will give us the various // IP addresses: addresses = InetAddress.getAllByName(host); for (int i = 0; i < addresses.length; i++) { String name1 = addresses[i].getHostName(); String name2 = addresses[i].getHostAddress(); names.add(name1.trim().toLowerCase()); names.add(name2.trim().toLowerCase()); } } catch (UnknownHostException uhe) { /* oh well, nothing found, nothing to add for this client */ } try { host = InetAddress.getByName(host).getHostAddress(); // If they gave us "1.2.3.4", this will hopefully give us // "hostname.com" so that we can then try and find any other // IP addresses associated with that name. host = InetAddress.getByName(host).getHostName(); names.add(host.trim().toLowerCase()); addresses = InetAddress.getAllByName(host); for (int i = 0; i < addresses.length; i++) { String name1 = addresses[i].getHostName(); String name2 = addresses[i].getHostAddress(); names.add(name1.trim().toLowerCase()); names.add(name2.trim().toLowerCase()); } } catch (UnknownHostException uhe) { /* oh well, nothing found, nothing to add for this client */ } return names; } private void trustOurself() throws GeneralSecurityException, IOException { if (defaultClient == null || sslServer == null) { return; } boolean clientIsCommonsSSL = defaultClient instanceof SSLClient; boolean serverIsCommonsSSL = sslServer instanceof SSLServer; if (clientIsCommonsSSL && serverIsCommonsSSL) { SSLClient c = (SSLClient) defaultClient; SSLServer s = (SSLServer) sslServer; trustEachOther(c, s); } } private void trustEachOther(SSLClient client, SSLServer server) throws GeneralSecurityException, IOException { if (client != null && server != null) { // Our own client should trust our own server. X509Certificate[] certs = server.getAssociatedCertificateChain(); if (certs != null && certs[0] != null) { TrustMaterial tm = new TrustMaterial(certs[0]); client.addTrustMaterial(tm); } // Our own server should trust our own client. certs = client.getAssociatedCertificateChain(); if (certs != null && certs[0] != null) { TrustMaterial tm = new TrustMaterial(certs[0]); server.addTrustMaterial(tm); } } } public ServerSocketFactory getServer() { return sslServer; } public SocketFactory getDefaultClient() { return defaultClient; } public synchronized SocketFactory getClient(String host) { host = host != null ? host.trim().toLowerCase() : ""; return (SocketFactory) clientMap.get(host); } public synchronized ServerSocket createServerSocket(int port) throws IOException { // Re-use existing ServerSocket if possible. if (port == 0) { port = anonymousPort; } Integer key = new Integer(port); ServerSocket ss = (ServerSocket) serverSockets.get(key); if (ss == null || ss.isClosed()) { if (ss != null && ss.isClosed()) { System.out.println("found closed server on port: " + port); } log.debug("commons-ssl RMI server-socket: listening on port " + port); ss = sslServer.createServerSocket(port); serverSockets.put(key, ss); } return ss; } public Socket createSocket(String host, int port) throws IOException { host = host != null ? host.trim().toLowerCase() : ""; InetAddress local = null; String bindAddress = localBindAddress; if (bindAddress == null) { bindAddress = System.getProperty(RMI_HOSTNAME_KEY); if (bindAddress != null) { local = InetAddress.getByName(bindAddress); if (!local.isLoopbackAddress()) { String ip = local.getHostAddress(); Set myInternetIps = getMyInternetFacingIPs(); if (!myInternetIps.contains(ip)) { log.warn("Cannot bind to " + ip + " since it doesn't exist on this machine."); // Not going to be able to bind as this. Our RMI_HOSTNAME_KEY // must be set to some kind of proxy in front of us. So we // still want to use it, but we can't bind to it. local = null; bindAddress = null; } } } } if (bindAddress == null) { // Our last resort - let's make sure we at least use something that's // internet facing! bindAddress = getMyDefaultIP(); } if (local == null && bindAddress != null) { local = InetAddress.getByName(bindAddress); localBindAddress = local.getHostName(); } SocketFactory sf; synchronized (this) { sf = (SocketFactory) clientMap.get(host); } if (sf == null) { sf = defaultClient; } Socket s = null; SSLSocket ssl = null; int soTimeout = Integer.MIN_VALUE; IOException reasonForPlainSocket = null; boolean tryPlain = false; try { s = sf.createSocket(host, port, local, 0); soTimeout = s.getSoTimeout(); if (!(s instanceof SSLSocket)) { // Someone called setClient() or setDefaultClient() and passed in // a plain socket factory. Okay, nothing to see, move along. return s; } else { ssl = (SSLSocket) s; } // If we don't get the peer certs in 15 seconds, revert to plain // socket. ssl.setSoTimeout(15000); ssl.getSession().getPeerCertificates(); // Everything worked out okay, so go back to original soTimeout. ssl.setSoTimeout(soTimeout); return ssl; } catch (IOException ioe) { // SSL didn't work. Let's analyze the IOException to see if maybe // we're accidentally attempting to talk to a plain-socket RMI // server. Throwable t = ioe; while (!tryPlain && t != null) { tryPlain = tryPlain || t instanceof EOFException; tryPlain = tryPlain || t instanceof InterruptedIOException; tryPlain = tryPlain || t instanceof SSLProtocolException; t = t.getCause(); } if (!tryPlain && ioe instanceof SSLPeerUnverifiedException) { try { if (ssl != null) { ssl.startHandshake(); } } catch (IOException ioe2) { // Stacktrace from startHandshake() will be more descriptive // then the one we got from getPeerCertificates(). ioe = ioe2; t = ioe2; while (!tryPlain && t != null) { tryPlain = tryPlain || t instanceof EOFException; tryPlain = tryPlain || t instanceof InterruptedIOException; tryPlain = tryPlain || t instanceof SSLProtocolException; t = t.getCause(); } } } if (!tryPlain) { log.debug("commons-ssl RMI-SSL failed: " + ioe); throw ioe; } else { reasonForPlainSocket = ioe; } } finally { // Some debug logging: boolean isPlain = tryPlain || (s != null && ssl == null); String socket = isPlain ? "RMI plain-socket " : "RMI ssl-socket "; String localIP = local != null ? local.getHostAddress() : "ANY"; StringBuffer buf = new StringBuffer(64); buf.append(socket); buf.append(localIP); buf.append(" --> "); buf.append(host); buf.append(":"); buf.append(port); log.debug(buf.toString()); } // SSL didn't work. Remote server either timed out, or sent EOF, or // there was some kind of SSLProtocolException. (Any other problem // would have caused an IOException to be thrown, so execution wouldn't // have made it this far). Maybe plain socket will work in these three // cases. sf = plainClient; s = JavaImpl.connect(null, sf, host, port, local, 0, 15000, null); if (soTimeout != Integer.MIN_VALUE) { s.setSoTimeout(soTimeout); } try { // Plain socket worked! Let's remember that for next time an RMI call // against this host happens. setClient(host, plainClient); String msg = "RMI downgrading from SSL to plain-socket for " + host + " because of " + reasonForPlainSocket; log.warn(msg, reasonForPlainSocket); } catch (GeneralSecurityException gse) { throw new RuntimeException("can't happen because we're using plain socket", gse); // won't happen because we're using plain socket, not SSL. } return s; } public static String getMyDefaultIP() { String anInternetIP = "64.111.122.211"; String ip = null; try { DatagramSocket dg = new DatagramSocket(); dg.setSoTimeout(250); // 64.111.122.211 is juliusdavies.ca. // This code doesn't actually send any packets (so no firewalls can // get in the way). It's just a neat trick for getting our // internet-facing interface card. InetAddress addr = Util.toInetAddress(anInternetIP); dg.connect(addr, 12345); InetAddress localAddr = dg.getLocalAddress(); ip = localAddr.getHostAddress(); // log.debug( "Using bogus UDP socket (" + anInternetIP + ":12345), I think my IP address is: " + ip ); dg.close(); if (localAddr.isLoopbackAddress() || "0.0.0.0".equals(ip)) { ip = null; } } catch (IOException ioe) { log.debug("Bogus UDP didn't work: " + ioe); } return ip; } public static SortedSet getMyInternetFacingIPs() throws SocketException { TreeSet set = new TreeSet(); Enumeration en = NetworkInterface.getNetworkInterfaces(); while (en.hasMoreElements()) { NetworkInterface ni = (NetworkInterface) en.nextElement(); Enumeration en2 = ni.getInetAddresses(); while (en2.hasMoreElements()) { InetAddress addr = (InetAddress) en2.nextElement(); if (!addr.isLoopbackAddress()) { String ip = addr.getHostAddress(); String reverse = addr.getHostName(); // IP: set.add(ip); // Reverse-Lookup: set.add(reverse); } } } return set; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy