/* * Copyright (c) 2001-2007 Sun Microsystems, Inc. All rights reserved. * * The Sun Project JXTA(TM) Software License * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. The end-user documentation included with the redistribution, if any, must * include the following acknowledgment: "This product includes software * developed by Sun Microsystems, Inc. for JXTA(TM) technology." * Alternately, this acknowledgment may appear in the software itself, if * and wherever such third-party acknowledgments normally appear. * * 4. The names "Sun", "Sun Microsystems, Inc.", "JXTA" and "Project JXTA" must * not be used to endorse or promote products derived from this software * without prior written permission. For written permission, please contact * Project JXTA at http://www.jxta.org. * * 5. Products derived from this software may not be called "JXTA", nor may * "JXTA" appear in their name, without prior written permission of Sun. * * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SUN * MICROSYSTEMS OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * JXTA is a registered trademark of Sun Microsystems, Inc. in the United * States and other countries. * * Please see the license information page at : * <http://www.jxta.org/project/www/license.html> for instructions on use of * the license in source files. * * ==================================================================== * * This software consists of voluntary contributions made by many individuals * on behalf of Project JXTA. For more information on Project JXTA, please see * http://www.jxta.org. * * This license is based on the BSD license adopted by the Apache Foundation. */ package net.jxta.impl.endpoint.tls; import net.jxta.document.Advertisement; import net.jxta.endpoint.EndpointAddress; import net.jxta.endpoint.EndpointService; import net.jxta.endpoint.Message; import net.jxta.endpoint.MessageReceiver; import net.jxta.endpoint.MessageSender; import net.jxta.endpoint.Messenger; import net.jxta.exception.PeerGroupException; import net.jxta.id.ID; import net.jxta.id.IDFactory; import net.jxta.impl.endpoint.LoopbackMessenger; import net.jxta.impl.membership.pse.PSECredential; import net.jxta.impl.membership.pse.PSEMembershipService; import net.jxta.impl.peergroup.GenericPeerGroup; import net.jxta.impl.util.TimeUtils; import net.jxta.logging.Logging; import net.jxta.membership.MembershipService; import net.jxta.peer.PeerID; import net.jxta.peergroup.PeerGroup; import net.jxta.platform.Module; import net.jxta.protocol.ModuleImplAdvertisement; import javax.security.auth.x500.X500Principal; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.security.InvalidKeyException; import java.security.KeyStoreException; import java.security.SignatureException; import java.security.cert.X509Certificate; import java.util.Collections; import java.util.Enumeration; import java.util.Iterator; import java.util.MissingResourceException; import java.util.ResourceBundle; import java.util.logging.Level; import java.util.logging.Logger; /** * A JXTA {@link net.jxta.endpoint.MessageTransport} implementation which * uses TLS sockets. */ public class TlsTransport implements Module, MessageSender, MessageReceiver { /** * Logger */ private final static transient Logger LOG = Logger.getLogger(TlsTransport.class.getName()); /** * If true then we can accept incoming connections. Eventually this should * be coming out of the transport advertisement. */ static final boolean ACT_AS_SERVER = true; private PeerGroup group = null; ID assignedID = null; ModuleImplAdvertisement implAdvertisement = null; EndpointService endpoint = null; PSEMembershipService membership = null; private membershipPCL membershipListener = null; X509Certificate[] serviceCert = null; PSECredential credential = null; private credentialPCL credentialListener = null; EndpointAddress localPeerAddr = null; EndpointAddress localTlsPeerAddr = null; /** * local peerID */ PeerID localPeerId = null; /** * Amount of a connection must be idle before a reconnection attempt will * be considered. */ long MIN_IDLE_RECONNECT = 1 * TimeUtils.AMINUTE; /** * Amount of time after which a connection is considered idle and may be * scavenged. */ long CONNECTION_IDLE_TIMEOUT = 5 * TimeUtils.AMINUTE; /** * Amount if time which retries may remain queued for retransmission. If * still unACKed after this amount of time then the connection is * considered dead. */ long RETRMAXAGE = 2 * TimeUtils.AMINUTE; /** * Will manage connections to remote peers. */ private TlsManager manager = null; /** * This is the thread group into which we will place all of the threads * we create. THIS HAS NO EFFECT ON SCHEDULING. Java thread groups are * only for organization and naming. */ ThreadGroup myThreadGroup = null; /** * Extends LoopbackMessenger to add a message property to passed messages * so that TLS pipes and other users can be sure that the message * originate with the local TLS transport. */ class TlsLoopbackMessenger extends LoopbackMessenger { TlsLoopbackMessenger(EndpointService ep, EndpointAddress src, EndpointAddress dest, EndpointAddress logicalDest) { super(group, ep, src, dest, logicalDest); } /** * {@inheritDoc} **/ @Override public void sendMessageBImpl(Message message, String service, String serviceParam) throws IOException { // add a property to the message to indicate it came from us. message.setMessageProperty(TlsTransport.class, TlsTransport.this); super.sendMessageBImpl(message, service, serviceParam); } } /** * Default constructor **/ public TlsTransport() { // initialize connection timeout try { ResourceBundle jxtaRsrcs = ResourceBundle.getBundle("net.jxta.user"); try { String override_str = jxtaRsrcs.getString("impl.endpoint.tls.connection.idletimeout"); if (null != override_str) { long override_long = Long.parseLong(override_str.trim()); if (override_long >= 1) { CONNECTION_IDLE_TIMEOUT = override_long * TimeUtils.AMINUTE; if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) { LOG.info("Adjusting TLS connection idle timeout to " + CONNECTION_IDLE_TIMEOUT + " millis."); } } } } catch (NumberFormatException badvalue) { ; } try { String override_str = jxtaRsrcs.getString("impl.endpoint.tls.connection.minidlereconnect"); if (null != override_str) { long override_long = Long.parseLong(override_str.trim()); if (override_long >= 1) { MIN_IDLE_RECONNECT = override_long * TimeUtils.AMINUTE; if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) { LOG.info("Adjusting TLS min reconnection idle to " + MIN_IDLE_RECONNECT + " millis."); } } } } catch (NumberFormatException badvalue) { ; } try { String override_str = jxtaRsrcs.getString("impl.endpoint.tls.connection.maxretryage"); if (null != override_str) { long override_long = Long.parseLong(override_str.trim()); if (override_long >= 1) { RETRMAXAGE = override_long * TimeUtils.AMINUTE; if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) { LOG.info("Adjusting TLS maximum retry queue age to " + RETRMAXAGE + " millis."); } } } } catch (NumberFormatException badvalue) { ; } // reconnect must be less the idle interval. MIN_IDLE_RECONNECT = Math.min(MIN_IDLE_RECONNECT, CONNECTION_IDLE_TIMEOUT); // max retry queue age must be less the idle interval. RETRMAXAGE = Math.min(RETRMAXAGE, CONNECTION_IDLE_TIMEOUT); } catch (MissingResourceException notthere) { ; } } /** * {@inheritDoc} */ @Override public boolean equals(Object target) { if (this == target) { return true; } if (null == target) { return false; } if (target instanceof TlsTransport) { TlsTransport likeMe = (TlsTransport) target; if (!getProtocolName().equals(likeMe.getProtocolName())) { return false; } return localTlsPeerAddr.equals(likeMe.localTlsPeerAddr); } return false; } /** * Get the PeerGroup this service is running in. * * @return PeerGroup instance */ PeerGroup getPeerGroup() { return group; } /** * {@inheritDoc} */ public void init(PeerGroup group, ID assignedID, Advertisement impl) throws PeerGroupException { this.group = group; this.assignedID = assignedID; this.implAdvertisement = (ModuleImplAdvertisement) impl; localPeerId = group.getPeerID(); localPeerAddr = mkAddress(group.getPeerID(), null, null); localTlsPeerAddr = new EndpointAddress(JTlsDefs.tlsPName, localPeerId.getUniqueValue().toString(), null, null); myThreadGroup = new ThreadGroup(group.getHomeThreadGroup(), "TLSTransport " + localTlsPeerAddr); if (Logging.SHOW_CONFIG && LOG.isLoggable(Level.CONFIG)) { StringBuilder configInfo = new StringBuilder("Configuring TLS Transport : " + assignedID); if (null != implAdvertisement) { configInfo.append("\n\tImplementation:"); configInfo.append("\n\t\tModule Spec ID: ").append(implAdvertisement.getModuleSpecID()); configInfo.append("\n\t\tImpl Description : ").append(implAdvertisement.getDescription()); configInfo.append("\n\t\tImpl URI : ").append(implAdvertisement.getUri()); configInfo.append("\n\t\tImpl Code : ").append(implAdvertisement.getCode()); } configInfo.append("\n\tGroup Params:"); configInfo.append("\n\t\tGroup: ").append(group.getPeerGroupName()); configInfo.append("\n\t\tGroup ID: ").append(group.getPeerGroupID()); configInfo.append("\n\t\tPeer ID: ").append(group.getPeerID()); configInfo.append("\n\tConfiguration :"); configInfo.append("\n\t\tProtocol: ").append(JTlsDefs.tlsPName); configInfo.append("\n\t\tOutgoing Connections Enabled: ").append(Boolean.TRUE); configInfo.append("\n\t\tIncoming Connections Enabled: " + ACT_AS_SERVER); configInfo.append("\n\t\tMinimum idle for reconnect : ").append(MIN_IDLE_RECONNECT).append("ms"); configInfo.append("\n\t\tConnection idle timeout : ").append(CONNECTION_IDLE_TIMEOUT).append("ms"); configInfo.append("\n\t\tRetry queue maximum age : ").append(RETRMAXAGE).append("ms"); configInfo.append("\n\t\tPeerID : ").append(localPeerId); configInfo.append("\n\t\tRoute through : ").append(localPeerAddr); configInfo.append("\n\t\tPublic Address : ").append(localTlsPeerAddr); LOG.config(configInfo.toString()); } } /** * {@inheritDoc} */ public synchronized int startApp(String[] args) { endpoint = group.getEndpointService(); if (null == endpoint) { if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) { LOG.warning("Stalled until there is an endpoint service"); } return START_AGAIN_STALLED; } MembershipService groupMembership = group.getMembershipService(); if (null == groupMembership) { if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) { LOG.warning("Stalled until there is a membership service"); } return START_AGAIN_STALLED; } if (!(groupMembership instanceof PSEMembershipService)) { if (Logging.SHOW_SEVERE && LOG.isLoggable(Level.SEVERE)) { LOG.severe("TLS Transport requires PSE Membership Service"); } return -1; } if (endpoint.addMessageTransport(this) == null) { if (Logging.SHOW_SEVERE && LOG.isLoggable(Level.SEVERE)) { LOG.severe("Transport registration refused"); } return -1; } membership = (PSEMembershipService) groupMembership; PropertyChangeListener mpcl = new membershipPCL(); membership.addPropertyChangeListener(mpcl); try { serviceCert = membership.getPSEConfig().getTrustedCertificateChain(assignedID); Enumeration eachCred = membership.getCurrentCredentials(); while (eachCred.hasMoreElements()) { PSECredential aCred = (PSECredential) eachCred.nextElement(); // send a fake property change event. mpcl.propertyChange(new PropertyChangeEvent(membership, "addCredential", null, aCred)); } } catch (IOException failed) { serviceCert = null; } catch (KeyStoreException failed) { serviceCert = null; } // Create the TLS Manager manager = new TlsManager(this); // Connect ourself to the EndpointService try { endpoint.addIncomingMessageListener(manager, JTlsDefs.ServiceName, null); } catch (Throwable e2) { if (Logging.SHOW_SEVERE && LOG.isLoggable(Level.SEVERE)) { LOG.log(Level.SEVERE, "TLS could not register listener...as good as dead", e2); } return -1; } return 0; } /** * {@inheritDoc} */ public synchronized void stopApp() { if (null != endpoint) { endpoint.removeIncomingMessageListener(JTlsDefs.ServiceName, null); endpoint.removeMessageTransport(this); endpoint = null; } if (null != manager) { manager.close(); manager = null; } if (null != membership) { membership.removePropertyChangeListener(membershipListener); membershipListener = null; membership = null; } PSECredential temp = credential; if (null != temp) { temp.removePropertyChangeListener(credentialListener); credentialListener = null; credential = null; } } /** * {@inheritDoc} **/ public boolean isConnectionOriented() { return true; } /** * {@inheritDoc} */ public boolean allowsRouting() { // The TLS connection should not be used for default routing return false; } /** * {@inheritDoc} */ public Object transportControl(Object operation, Object Value) { return null; } /** * {@inheritDoc} */ public EndpointAddress getPublicAddress() { return localTlsPeerAddr; } /** * {@inheritDoc} */ public EndpointService getEndpointService() { return endpoint; } /** * {@inheritDoc} */ public Iterator<EndpointAddress> getPublicAddresses() { return Collections.singletonList(getPublicAddress()).iterator(); } /** * {@inheritDoc} */ public String getProtocolName() { return JTlsDefs.tlsPName; } /** * {@inheritDoc} * * XXX bondolo 20040522 The hint could be used in request for the * underlying messenger. */ public Messenger getMessenger(EndpointAddress addr, Object hintIgnored) { if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("getMessenger for " + addr); } EndpointAddress plainAddress = new EndpointAddress(addr, null, null); // If the dest is the local peer, just loop it back without going // through the TLS. Local communication do not use TLS. if (plainAddress.equals(localTlsPeerAddr)) { if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("returning LoopbackMessenger"); } return new TlsLoopbackMessenger(endpoint, plainAddress, addr, localPeerAddr); } // Create a Peer EndpointAddress EndpointAddress dstPAddr = mkAddress(ID.URIEncodingName + ":" + ID.URNNamespace + ":" + addr.getProtocolAddress(), null , null); TlsConn conn = manager.getTlsConn(dstPAddr); if (conn == null) { if (Logging.SHOW_SEVERE && LOG.isLoggable(Level.SEVERE)) { LOG.severe("Cannot get a TLS connection for " + dstPAddr); } // No connection was either available or created. Cannot do TLS // with the destination address. return null; } if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("TlsMessanger with TlsConn DONE"); } // Build a TlsMessenger around it that will add our header. // Right now we do not want to "announce" outgoing messengers because they get pooled and so must // not be grabbed by a listener. If "announcing" is to be done, that should be by the endpoint // and probably with a subtely different interface. return new TlsMessenger(addr, conn, this); } /** * processReceivedMessage is invoked by the TLS Manager when a message has been * completely received and is ready to be delivered to the service/application */ void processReceivedMessage(final Message msg) { if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("processReceivedMessage starts"); } // add a property to the message to indicate it came from us. msg.setMessageProperty(TlsTransport.class, this); // let the message continue to its final destination. try { ((GenericPeerGroup)group).getExecutor().execute( new Runnable() { public void run() { try { endpoint.processIncomingMessage(msg); } catch(Throwable uncaught) { if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) { LOG.log(Level.WARNING, "Failure demuxing an incoming message", uncaught); } } } }); } catch (Throwable e) { if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) { LOG.log(Level.WARNING, "Failure demuxing an incoming message", e); } } } /** * Convenience method for constructing an endpoint address from an id * * @param destPeer peer id * @param serv the service name (if any) * @param parm the service param (if any) * @return endpointAddress for this peer id. */ private static EndpointAddress mkAddress(String destPeer, String serv, String parm) { ID asID = null; try { asID = IDFactory.fromURI(new URI(destPeer)); } catch (URISyntaxException caught) { throw new IllegalArgumentException(caught.getMessage()); } return mkAddress(asID, serv, parm); } /** * Convenience method for constructing an endpoint address from an id * * @param destPeer peer id * @param serv the service name (if any) * @param parm the service param (if any) * @return endpointAddress for this peer id. */ private final static EndpointAddress mkAddress(ID destPeer, String serv, String parm) { EndpointAddress addr = new EndpointAddress("jxta", destPeer.getUniqueValue().toString(), serv, parm); return addr; } /** * Listener for Property Changed Events on our credential **/ class credentialPCL implements PropertyChangeListener { /** * {@inheritDoc} * * <p/>Handle events on our active credential. **/ public synchronized void propertyChange(PropertyChangeEvent evt) { if (credential == evt.getSource()) { if (!credential.isValid()) { if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) { LOG.info("Clearing credential/certfile "); } credential.removePropertyChangeListener(this); credential = null; } } } } /** * Listener for Property Changed Events on membership service **/ class membershipPCL implements PropertyChangeListener { /** * {@inheritDoc} **/ public synchronized void propertyChange(PropertyChangeEvent evt) { String evtProp = evt.getPropertyName(); PSECredential cred = (PSECredential) evt.getNewValue(); boolean validCertificate = true; if (null != serviceCert) { try { serviceCert[0].checkValidity(); } catch (Exception notValidException) { validCertificate = false; } } if ("addCredential".equals(evtProp) && ((null == serviceCert) || !validCertificate)) { // no service Cert or Non-valid Cert? Make one. Exception failure = null; try { X509Certificate peerCert = membership.getPSEConfig().getTrustedCertificate(group.getPeerID()); X500Principal credSubjectDN = cred.getCertificate().getSubjectX500Principal(); X500Principal peerCertSubjectDN = peerCert.getSubjectX500Principal(); if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine( "Checking credential cert for match to peer cert" + "\n\tcred subject=" + credSubjectDN + "\n\tpeer subject=" + peerCertSubjectDN); } if (peerCertSubjectDN.equals(credSubjectDN)) { serviceCert = cred.generateServiceCertificate(assignedID); } } catch (IOException failed) { failure = failed; } catch (KeyStoreException failed) { failure = failed; } catch (InvalidKeyException failed) { failure = failed; } catch (SignatureException failed) { failure = failed; } if (null != failure) { if (Logging.SHOW_SEVERE && LOG.isLoggable(Level.SEVERE)) { LOG.log(Level.SEVERE, "Failure building service certificate", failure); } return; } } if ("addCredential".equals(evtProp)) { Exception failure = null; try { X509Certificate credCert = cred.getCertificate(); X500Principal credSubjectDN = credCert.getSubjectX500Principal(); X500Principal serviceIssuerDN = serviceCert[0].getIssuerX500Principal(); if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine( "Checking credential cert for match to service issuer cert" + "\n\tcred subject=" + credSubjectDN + "\n\t svc issuer=" + serviceIssuerDN); } if (credSubjectDN.equals(serviceIssuerDN)) { if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) { LOG.info("Setting credential/certfile "); } credential = cred.getServiceCredential(assignedID); if (null != credential) { credentialListener = new credentialPCL(); credential.addPropertyChangeListener(credentialListener); } } } catch (IOException failed) { failure = failed; } catch (PeerGroupException failed) { failure = failed; } catch (InvalidKeyException failed) { failure = failed; } catch (SignatureException failed) { failure = failed; } if (null != failure) { if (Logging.SHOW_SEVERE && LOG.isLoggable(Level.SEVERE)) { LOG.log(Level.SEVERE, "Failure building service credential", failure); } } } } } }