/* * 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. */ package net.jini.jeri.ssl; import com.sun.jini.jeri.internal.http.HttpClientConnection; import com.sun.jini.jeri.internal.http.HttpClientManager; import com.sun.jini.jeri.internal.http.HttpClientSocketFactory; import com.sun.jini.jeri.internal.http.HttpSettings; import com.sun.jini.logging.Levels; import com.sun.jini.thread.Executor; import com.sun.jini.thread.GetThreadPoolAction; import java.io.IOException; import java.io.InputStream; import java.io.InvalidObjectException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.ObjectStreamField; import java.io.OutputStream; import java.io.Serializable; import java.net.InetAddress; import java.net.Socket; import java.security.AccessController; import java.security.Principal; import java.security.PrivilegedAction; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import java.security.SecureRandom; import java.security.cert.CertPath; import java.security.cert.CertificateFactory; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.logging.Level; import java.util.logging.Logger; import javax.net.SocketFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLException; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; import javax.security.auth.Subject; import javax.security.auth.x500.X500Principal; import javax.security.auth.x500.X500PrivateCredential; import net.jini.core.constraint.ClientAuthentication; import net.jini.core.constraint.ClientMaxPrincipal; import net.jini.core.constraint.ClientMaxPrincipalType; import net.jini.core.constraint.ClientMinPrincipal; import net.jini.core.constraint.ClientMinPrincipalType; import net.jini.core.constraint.Confidentiality; import net.jini.core.constraint.ConnectionAbsoluteTime; import net.jini.core.constraint.ConnectionRelativeTime; import net.jini.core.constraint.ConstraintAlternatives; import net.jini.core.constraint.Delegation; import net.jini.core.constraint.DelegationAbsoluteTime; import net.jini.core.constraint.DelegationRelativeTime; import net.jini.core.constraint.Integrity; import net.jini.core.constraint.InvocationConstraints; import net.jini.core.constraint.ServerAuthentication; import net.jini.core.constraint.ServerMinPrincipal; import net.jini.io.context.AcknowledgmentSource; import net.jini.jeri.BasicJeriExporter; import net.jini.jeri.Endpoint; import net.jini.jeri.OutboundRequest; import net.jini.jeri.OutboundRequestIterator; import net.jini.jeri.ServerEndpoint; import net.jini.jeri.connection.Connection; import net.jini.jeri.connection.ConnectionManager; import net.jini.jeri.connection.OutboundRequestHandle; import net.jini.security.AuthenticationPermission; import net.jini.security.Security; import net.jini.security.proxytrust.TrustEquivalence; /** * An implementation of {@link Endpoint} that uses HTTPS (HTTP over TLS/SSL) to * support invocation constraints for communication through firewalls. <p> * * Instances of this class are intended to be created by the {@link * BasicJeriExporter} class when it calls {@link * ServerEndpoint#enumerateListenEndpoints enumerateListenEndpoints} on * instances of {@link HttpsServerEndpoint}. <p> * * The {@link SslTrustVerifier} trust verifier may be used for establishing * trust in remote proxies that use instances of this class. <p> * * This class supports at least the following constraints, possibly limited by * the available cipher suites: <p> * * <ul> * <li> {@link ClientAuthentication} * <li> {@link ClientMaxPrincipal}, when it contains an {@link X500Principal} * <li> {@link ClientMaxPrincipalType}, when it contains * <code>X500Principal</code> * <li> {@link ClientMinPrincipal}, when it contains a single * <code>X500Principal</code> only * <li> {@link ClientMinPrincipalType}, when it contains * <code>X500Principal</code> only * <li> {@link Confidentiality} * <li> {@link ConfidentialityStrength}, a provider-specific constraint for * specifying weak or strong confidentiality * <li> {@link ConnectionAbsoluteTime} * <li> {@link ConstraintAlternatives}, if the elements all have the same * actual class and at least one element is supported * <li> {@link Delegation#NO} * <li> {@link Delegation#YES}, trivially, for anonymous clients * <li> {@link DelegationAbsoluteTime}, trivially, when delegation is not * supported * <li> {@link Integrity#YES} * <li> {@link ServerAuthentication} * <li> {@link ServerMinPrincipal}, when it contains a single * <code>X500Principal</code> only * </ul> <p> * * Note that {@link ConnectionRelativeTime} and {@link DelegationRelativeTime} * constraints may be used at higher levels, but should be converted to the * associated absolute time constraints for use by this class. <p> * * This class authenticates as a single {@link Principal} if the following * items are present in the current {@link Subject}: <p> * * <ul> * <li> One or more principals of type <code>X500Principal</code> * <li> For each principal, one or more certificate chains, stored as public * credentials, and represented by instances of {@link CertPath}, whose * <code>getType</code> method returns "X.509", and for which calling * <code>getSubjectDN</code> on the certificate chain's first element * returns that principal's name * <li> For each certificate chain, an instance of {@link * X500PrivateCredential}, stored as a private credential, whose * <code>getCertificate</code> method returns a value equal to the first * element of the certificate chain, and whose <code>getPrivateKey</code> * method returns the associated private key * </ul> <p> * * In addition, this class's {@link #newRequest newRequest} method will only * authenticate as a given principal if the caller has been granted {@link * AuthenticationPermission} with that principal as the local principal, the * principal representing the authenticated identity of the server as the peer * principal, and the <code>connect</code> action. <p> * * This class supports remote connections between authenticated servers and * authenticated or anonymous clients, and between anonymous servers and * anonymous clients. Connections between anonymous servers and authenticated * clients are not supported. Because of the suites available in the TLS/SSL * protocol, support for {@link Confidentiality#NO} requires the server to * authenticate with an RSA public key. <p> * * This class permits specifying a {@link SocketFactory} for creating the * {@link Socket} instances that it uses to make remote connections. These * socket factories should not be instances of {@link SSLSocketFactory} or * return instances {@link SSLSocket}; it is the responsibility of the * implementation to establish a TLS/SSL connection over the socket it obtains * from the socket factory. <p> * * A <code>SocketFactory</code> used with instances of this class should be * serializable, and must implement {@link Object#equals Object.equals} to * obey the guidelines that are specified for <code>equals</code> methods of * {@link Endpoint} instances. <p> * * This class recognizes the following system properties: <p> * * <ul> * <li> https.proxyHost - The host name for the secure proxy server. The * default is to use no proxy server. * <li> https.proxyPort - The port for the secure proxy server. The default is * 443. * <li> http.nonProxyHosts - The names of hosts for which direct connections * should be made rather than using the proxy server. Each host name may * contain '<code>*</code>' wildcard characters in any position to match * zero or more of any characters within the name. Multiple host names may * be specified by separating the names with '<code>|</code>' * characters. The default is for all connections to use the proxy server * if one is specified. * </ul> * * @author Sun Microsystems, Inc. * @see HttpsServerEndpoint * @see ConfidentialityStrength * @see SslTrustVerifier * @since 2.0 * * @com.sun.jini.impl <!-- Implementation Specifics --> * * This implementation uses the <a * href="http://java.sun.com/j2se/1.4/docs/guide/security/jsse/JSSERefGuide.html" * target="_top">Java(TM) Secure Socket Extension (JSSE)</a>. <p> * * This implementation uses the {@link ConnectionManager} class to manage * connections. <p> * * This implementation uses the following {@link Logger} instances in the * <code>net.jini.jeri.ssl</code> namespace: <p> * * <ul> * <li> <a href="#init_logger">init</a> - problems during initialization * <li> <a href="#client_logger">client</a> - information about * client-side connections * </ul> <p> * * <a name="init_logger"></a> * <table border="1" cellpadding="5" summary="Describes logging to the init * logger performed by the HttpsEndpoint class at different logging * levels"> * * <caption halign="center" valign="top"><b><code> * net.jini.jeri.ssl.init</code></b></caption> * * <tr> <th scope="col"> Level <th scope="col"> Description * * <tr> <td> {@link Level#WARNING WARNING} <td> problems with initializing JSSE * * </table> <p> * * <a name="client_logger"></a> * <table border="1" cellpadding="5" summary="Describes logging to the client * logger performed by the HttpsEndpoint class at different logging * levels"> * * <caption halign="center" valign="top"><b><code> * net.jini.jeri.ssl.client</code></b></caption> * * <tr> <th scope="col"> Level <th scope="col"> Description * * <tr> <td> {@link Levels#FAILED FAILED} <td> problems with outbound requests * * <tr> <td> {@link Levels#HANDLED HANDLED} <td> exceptions caught involving * authentication * * <tr> <td> {@link Level#FINE FINE} <td> authentication decisions; creating, * choosing, expiring, or closing connections; or handling outbound requests * * <tr> <td> {@link Level#FINEST FINEST} <td> low level operation tracing * * </table> <p> * * This implementation uses the following security providers: <p> * * <ul> * <li> {@link SSLContext}, with the protocol specified by the * <code>com.sun.jini.jeri.ssl.sslProtocol</code> system property, or * <code>"TLS"</code> if that property is not defined, to provide the * TLS/SSL implementation. The {@link SSLContext#init SSLContext.init} * method is called with <code>null</code> for the <code>random</code> * parameter to use the default {@link SecureRandom} implementation. * <li> {@link CertificateFactory}, with type <code>"X.509"</code>, to generate * <code>CertPath</code> instances from X.509 certificate chains * <li> {@link TrustManagerFactory}, with the algorithm specified by the * <code>com.sun.jini.jeri.ssl.trustManagerFactoryAlgorithm</code> system * property, or the default algorithm if that property is not defined, to * implement trust management for the TLS/SSL implementation. The factory * must return trust managers that implement {@link X509TrustManager}. * </ul> <p> * * See the documentation on <a * href="http://java.sun.com/j2se/1.4/docs/guide/security/CryptoSpec.html#ProviderInstalling" * target="_top">installing security providers</a> and <a * href="http://java.sun.com/j2se/1.4/docs/guide/security/jsse/JSSERefGuide.html#ProviderCust" * target="_top">configuring JSSE</a> for information on configuring these * providers. <p> * * The <a * href="http://java.sun.com/j2se/1.4/docs/guide/security/jsse/JSSERefGuide.html#Customization" * target="_top">JSSE documentation</a> also describes the system properties * for configuring the location, type, and password of the truststore that this * implementation uses, through JSSE, to make decisions about what certificate * chains should be trusted. <p> * * This implementation recognizes the following system properties: <p> * * <ul> * <li> <code>com.sun.jini.jeri.ssl.maxClientSessionDuration</code> - The * maximum number of milliseconds a client-side TLS/SSL session should be * used. The default is 23.5 hours. The value should be smaller than the * maximum server session duration to allow the client to negotiate a new * session before the server timeout occurs. * <li> <code>com.sun.jini.jeri.ssl.sslProtocol</code> - The secure socket * protocol used when obtaining {@link SSLContext} instances. The default * is <code>"TLS"</code>. * <li> <code>com.sun.jini.jeri.ssl.trustManagerFactoryAlgorithm</code> - The * algorithm used when obtaining {@link TrustManagerFactory} * instances. The default is the value returned by {@link * TrustManagerFactory#getDefaultAlgorithm * TrustManagerFactory.getDefaultAlgorithm}. * <li> <code>com.sun.jini.jeri.ssl.cipherSuites</code> - The TLS/SSL cipher * suites that should be used for communication. The default is the list * of suites supported by the JSSE implementation. The value should * specify the suite names, separated by commas. The value will be ignored * if it contains no suites or specifies suites that are not supported by * the JSSE implementation. Suites appearing earlier in the list will be * preferred to ones appearing later for suites that support the same * requirements and preferences. * <li> <code>com.sun.jini.jeri.https.idleConnectionTimeout</code> - The number * of milliseconds to retain idle client-side HTTPS connections before * closing them. The default is <code>15000</code>. * <li> <code>com.sun.jini.jeri.https.responseAckTimeout</code> - The number of * milliseconds to keep track of acknowledgments that have not yet been * sent for {@link AcknowledgmentSource} instances. The default is * <code>15000</code>. * <li> <code>com.sun.jini.jeri.https.pingProxyConnections</code> - If * the value is case-insensitive equal to <code>true</code>, then if an * HTTP proxy is being used, ping the server endpoint to verify whether * it is alive and reachable. The ping occurs before the first request * and before each subsequent request which follows the expiration of * the ping proxy timeout period (below) following the previous ping. * When using an HTTP proxy it is often impossible to distinguish * between inability to reach the server endpoint (such as because the * server process refused a connection by the HTTP proxy) and the lack * of response from a delivered request (which might result in an * UnmarshalException). The ping increases the likelihood that the * inability to reach the server endpoint can be explicitly identified. * The default value is <code>false</code>, and no pings are done. * <li> <code>com.sun.jini.jeri.https.pingProxyConnectionTimeout</code> - The * number of milliseconds from the time a server endpoint was last * pinged before a ping will precede the next request. The default is * <code>Long.MAX_VALUE</code> (essentially meaning, ping only before * the first request). * </ul> */ public final class HttpsEndpoint implements Endpoint, Serializable, TrustEquivalence { /* -- Fields -- */ private static final long serialVersionUID = -3438786823613900804L; /** * @serialField serverHost String * The name of the server host. * @serialField port int * The server port. * @serialField socketFactory SocketFactory * The socket factory for creating sockets, or * <code>null</code> to use default sockets. */ private static final ObjectStreamField[] serialPersistentFields = { new ObjectStreamField("serverHost", String.class), new ObjectStreamField("port", int.class), new ObjectStreamField("socketFactory", SocketFactory.class) }; /** * Maps HttpsEndpointImpls to EndpointInfo instances, each of which * contains a representative endpoint equal to the key and a list of idle * connections. */ static final Map endpointMap = new HashMap(); /** How long to leave idle connections around before closing them. */ static final long IDLE_TIMEOUT; /** Executes a Runnable in a system thread. */ static final Executor systemExecutor = (Executor) AccessController.doPrivileged(new GetThreadPoolAction(false)); /** Manager for HTTP client connections. */ static final HttpClientManager httpClientManager; static { HttpSettings hs = getHttpSettings(); IDLE_TIMEOUT = hs.getConnectionTimeout(); httpClientManager = new HttpClientManager(hs.getResponseAckTimeout()); } /** Implementation delegate. */ private transient HttpsEndpointImpl impl; /* -- Methods -- */ /** * Returns an HTTPS endpoint for the specified server host and port. Uses a * <code>null</code> socket factory to create default sockets. * * @param serverHost the name of the server host * @param port the server port * @return an <code>HttpsEndpoint</code> instance * @throws IllegalArgumentException if <code>port</code> is less than or * equal to <code>0</code>, or greater than <code>65535</code> * @throws NullPointerException if <code>serverHost</code> is * <code>null</code> */ public static HttpsEndpoint getInstance(String serverHost, int port) { return new HttpsEndpoint(serverHost, port, null); } /** * Returns an HTTPS endpoint for the specified server host, port, and * socket factory. A <code>socketFactory</code> of <code>null</code> uses * default sockets. * * @param serverHost the name of the server host * @param port the server port * @param socketFactory the socket factory, or <code>null</code> * @return an <code>HttpsEndpoint</code> instance * @throws IllegalArgumentException if <code>port</code> is less than or * equal to <code>0</code>, or greater than <code>65535</code> * @throws NullPointerException if <code>serverHost</code> is * <code>null</code> */ public static HttpsEndpoint getInstance( String serverHost, int port, SocketFactory socketFactory) { return new HttpsEndpoint(serverHost, port, socketFactory); } /** Creates an instance of this class. */ private HttpsEndpoint( String serverHost, int port, SocketFactory socketFactory) { impl = new HttpsEndpointImpl(this, serverHost, port, socketFactory); } /** * Returns the server host that this endpoint connects to. * * @return the server host that this endpoint connects to */ public String getHost() { return impl.serverHost; } /** * Returns the TCP port that this endpoint connects to. * * @return the TCP port that this endpoint connects to */ public int getPort() { return impl.port; } /** * Returns the socket factory that this endpoint uses to create {@link * Socket} instances, or <code>null</code> if it uses default sockets. * * @return the socket factory that this endpoint uses to create sockets, or * <code>null</code> if it uses default sockets */ public SocketFactory getSocketFactory() { return impl.socketFactory; } /** Returns a string representation of this object. */ public String toString() { return "HttpsEndpoint" + impl.fieldsToString(); } /* -- Implement Endpoint -- */ /** Returns a hash code value for this object. */ public int hashCode() { return impl.hashCode(); } /** * Two instances of this class are equal if they have the same values for * server host and port; and have socket factories that are either both * <code>null</code>, or have the same actual class and are equal. */ public boolean equals(Object object) { if (this == object) { return true; } else { return object instanceof HttpsEndpoint && impl.equals(((HttpsEndpoint) object).impl); } } /** * {@inheritDoc} <p> * * <p>The returned <code>OutboundRequestIterator</code>'s {@link * OutboundRequestIterator#next next} method behaves as follows: * * <blockquote> * * Initiates an attempt to communicate the request to this remote * endpoint. * * <p>When the implementation of this method needs to create a new * <code>Socket</code>, it will do so by invoking one of the * <code>createSocket</code> methods on the * <code>SocketFactory</code> of this <code>HttpsEndpoint</code> * (which produced this iterator) if non-<code>null</code>, or it * will create a <code>Socket</code> directly otherwise. * * <p>When the implementation needs to connect a * <code>Socket</code>, if the host name to connect to (if an HTTP * proxy is to be used for the communication, the proxy's host * name; otherwise, this <code>HttpsEndpoint</code>'s host name) * resolves to multiple addresses (according to {@link * InetAddress#getAllByName InetAddress.getAllByName}), it * attempts to connect to the first resolved address; if that * attempt fails with an <code>IOException</code> or (as is * possible in the case that an HTTP proxy is not to be used) a * <code>SecurityException</code>, it then attempts to connect to * the next address; and this iteration continues as long as there * is another resolved address and the attempt to connect to the * previous address fails with an <code>IOException</code> or a * <code>SecurityException</code>. If the host name resolves to * just one address, the implementation makes one attempt to * connect to that address. If the host name does not resolve to * any addresses (<code>InetAddress.getAllByName</code> would * throw an <code>UnknownHostException</code>), the implementation * still makes an attempt to connect the <code>Socket</code> to * that host name, which could result in an * <code>UnknownHostException</code>. If the final connection * attempt fails with an <code>IOException</code> or a * <code>SecurityException</code>, then if any connection attempt * failed with an <code>IOException</code>, this method throws an * <code>IOException</code>, and otherwise (if all connection * attempts failed with a <code>SecurityException</code>), this * method throws a <code>SecurityException</code>. * * <p>If there is a security manager and an HTTP proxy is to be * used for the communication, the security manager's {@link * SecurityManager#checkConnect(String,int) checkConnect} method * is invoked with this <code>HttpsEndpoint</code>'s host and port; * if this results in a <code>SecurityException</code>, this * method throws that exception. * * <p>If there is a security manager and an HTTP proxy is not to * be used for the communication: * * <ul> * * <li>If a new connection is to be created, the security * manager's {@link SecurityManager#checkConnect(String,int) * checkConnect} method is invoked with this * <code>HttpsEndpoint</code>'s host and <code>-1</code> for the * port; if this results in a <code>SecurityException</code>, this * method throws that exception. <code>checkConnect</code> is * also invoked for each connection attempt, with the remote IP * address (or the host name, if it could not be resolved) and * port to connect to; this could result in a * <code>SecurityException</code> for that attempt. (Note that * the implementation may carry out these security checks * indirectly, such as through invocations of * <code>InetAddress.getAllByName</code> or <code>Socket</code>'s * constructors or <code>connect</code> method.) * * <li><p>In order to reuse an existing connection for the * communication, the current security context must have all of * the permissions that would be necessary if the connection were * being created. Specifically, it must be possible to invoke * <code>checkConnect</code> in the current security context with * this <code>HttpsEndpoint</code>'s host and <code>-1</code> for * the port without resulting in a <code>SecurityException</code>, * and it also must be possible to invoke * <code>checkConnect</code> with the remote IP address and port * of the <code>Socket</code> without resulting in a * <code>SecurityException</code> (if the remote socket address is * unresolved, its host name is used instead). If no existing * connection satisfies these requirements, then this method must * behave as if there are no existing connections. * * </ul> * * <p>Throws {@link NoSuchElementException} if this iterator does * not support making another attempt to communicate the request * (that is, if <code>hasNext</code> would return * <code>false</code>). * * <p>Throws {@link IOException} if an I/O exception occurs while * performing this operation, such as if a connection attempt * timed out or was refused or there are unsupported or conflicting * constraints. * * <p>Throws {@link SecurityException} if there is a security * manager and an invocation of its <code>checkConnect</code> * method fails, or if the caller does not have the appropriate * <code>AuthenticationPermission</code>. * * </blockquote> * * @throws NullPointerException {@inheritDoc} */ public OutboundRequestIterator newRequest( InvocationConstraints constraints) { return impl.newRequest(constraints); } /* -- Implement TrustEquivalence -- */ /** * Returns <code>true</code> if the argument is an instance of * <code>HttpsEndpoint</code> with the same values for server host and * port; and either both this instance and the argument have * <code>null</code> socket factories, or the factories have the same * actual class and are equal; and returns <code>false</code> otherwise. */ public boolean checkTrustEquivalence(Object obj) { if (this == obj) { return true; } else { return obj instanceof HttpsEndpoint && impl.equals(((HttpsEndpoint) obj).impl); } } /* -- Serialization -- */ /** * Writes the <code>serverHost</code>, <code>port</code>, and * <code>socketFactory</code> fields. */ private void writeObject(ObjectOutputStream out) throws IOException { ObjectOutputStream.PutField fields = out.putFields(); fields.put("serverHost", impl.serverHost); fields.put("port", impl.port); fields.put("socketFactory", impl.socketFactory); out.writeFields(); } /** * Reads the <code>serverHost</code>, <code>port</code>, and * <code>socketFactory</code> fields, checks that <code>serverHost</code> * is not <code>null</code> and <code>port</code> is in range, and sets * transient fields. * * @throws InvalidObjectException if <code>serverHost</code> is * <code>null</code> or <code>port</code> is out of range */ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { ObjectInputStream.GetField fields = in.readFields(); String serverHost = (String) fields.get("serverHost", null); int port = fields.get("port", 0); SocketFactory socketFactory = (SocketFactory) fields.get("socketFactory", null); if (serverHost == null) { throw new InvalidObjectException("serverHost cannot be null"); } else if (port <= 0 || port > 0xFFFF) { throw new InvalidObjectException("Invalid port: " + port); } impl = new HttpsEndpointImpl(this, serverHost, port, socketFactory); } /** Returns current HTTP system property settings. */ static HttpSettings getHttpSettings() { return (HttpSettings) AccessController.doPrivileged( new PrivilegedAction() { public Object run() { return HttpSettings.getHttpSettings(true); } }); } /* -- Classes -- */ /** Implementation delegate */ private static final class HttpsEndpointImpl extends SslEndpointImpl { /* -- Fields -- */ /** Time at which the server endpoint was last pinged. */ private long timeLastVerified; /* -- Constructors -- */ /** Creates an instance of this class. */ HttpsEndpointImpl(Endpoint endpoint, String serverHost, int port, SocketFactory socketFactory) { super(endpoint, serverHost, port, socketFactory); } /* -- Methods -- */ /** * Implements Endpoint.newRequest when the constraints are supported. */ OutboundRequestIterator newRequest(final CallContext callContext) { return new OutboundRequestIterator() { private boolean done; public synchronized boolean hasNext() { return !done; } public synchronized OutboundRequest next() throws IOException { if (!hasNext()) { throw new NoSuchElementException(); } done = true; return getOutboundRequest(callContext); } }; } /** Returns an outbound request for the specified call context. */ OutboundRequest getOutboundRequest(CallContext callContext) throws IOException { HttpsConnection connection = null; EndpointInfo info; synchronized (endpointMap) { info = (EndpointInfo) endpointMap.get(this); if (info == null) { info = new EndpointInfo(this); } } connection = info.connect(callContext); boolean ok = false; try { // If using a connection through a proxy, and if the // connectionTimeout has passed, then ping the server // endpoint to verify the connection. This reduces the // likelihood that a connection dropped by the server // won't be noticed until after some or all of the data // destined for the server has already been sent to the // proxy. if (connection.usesHttpProxy()) { HttpSettings settings = getHttpSettings(); if (settings.getPingProxyConnections()) { long timeout = settings.getPingProxyConnectionTimeout(); long now = System.currentTimeMillis(); if (now - timeLastVerified > timeout) { pingEndpoint(connection); // The ping succeeded, but the connection // cannot be reused as it was marked idle. connection = info.connect(callContext); timeLastVerified = System.currentTimeMillis(); } } } OutboundRequest request = connection.newRequest(callContext); ok = true; logger.log(Level.FINE, "using {0}", connection); return request; } finally { if (!ok) { info.noteClosed(connection); } } } /** * Ping the server endpoint to test the connection. Throw (or * pass) an IOException if the server endpoint doesn't * respond. If the ping succeeds, the connection will have been * returned to the idle pool. */ private void pingEndpoint(HttpsConnection connection) throws IOException { logger.log(Level.FINEST, "HTTP pinging {0}", connection); try{ if (!connection.ping()) { throw new IOException("HTTP ping via proxy failed."); } } catch (IOException e) { logger.log(Level.FINEST, "pinging HTTP endpoint failed."); throw e; } } public Connection connect(OutboundRequestHandle handle) { throw new AssertionError("wrong connect method called"); } } /* Implements Connection and HttpClientSocketFactory. */ private static final class HttpsConnection extends SslConnection implements HttpClientSocketFactory { /* -- Fields -- */ /** The associated endpoint. */ private final HttpsEndpointImpl endpoint; /** * The proxy host, or empty string if using a direct connection. */ final String proxyHost; /** The proxy port, ignored if using a direct connection. */ final int proxyPort; /** The current HTTP client connection. */ private HttpClientConnection httpClient; /** * The time this connection was found to be idle by the Reaper thread. * Set to zero each time a new request is initiated. */ private long idleTime = 0; /* -- Methods -- */ /** Creates a connection. */ HttpsConnection(HttpsEndpointImpl endpoint, CallContext context, String serverHost, int port, String proxyHost, int proxyPort, SocketFactory socketFactory) throws IOException { super(context, serverHost, port, socketFactory); this.endpoint = endpoint; this.proxyHost = proxyHost; this.proxyPort = proxyPort; } /** * Attempts to create a new socket for the specified call context and * cipher suites. * * @throws SSLException if the requested suites cannot be supported * @throws IOException if an I/O failure occurs */ void establishNewSocket() throws IOException { if (proxyHost.length() == 0) { httpClient = new HttpClient(serverHost, port, this); } else { SecurityManager sm = System.getSecurityManager(); if (sm != null) { sm.checkConnect(serverHost, port); } try { httpClient = (HttpClient) Security.doPrivileged( new PrivilegedExceptionAction() { public Object run() throws IOException { return new HttpClient(serverHost, port, proxyHost, proxyPort, HttpsConnection.this); } }); } catch (PrivilegedActionException e) { throw (IOException) e.getCause(); } } } /** * Uses the HTTPClientConnection to create an OutboundRequest object * with the specified call context, and sets the idle time to 0. */ OutboundRequest newRequest(CallContext callContext) throws IOException { OutboundRequest request = new HttpsOutboundRequest( httpClient.newRequest(), this, callContext); idleTime = 0; return request; } /* inherit javadoc */ public void close() throws IOException { synchronized (this) { if (closed) { return; } } super.close(); /* Close the associated HTTP connection. */ if (httpClient != null) { httpClient.shutdown(true); httpClient = null; } } /** * Returns true if the recorded idle time is more than IDLE_TIMEOUT * milliseconds before now. If the recorded idle time is zero, sets * the recorded idle time to now. */ boolean checkIdle(long now) { if (idleTime == 0) { idleTime = now; return false; } else { return now - idleTime > IDLE_TIMEOUT; } } /** * Adds this connection to the set of idle connections recorded for the * connection's endpoint. */ void noteIdle() { synchronized (endpointMap) { EndpointInfo info = ((EndpointInfo) endpointMap.get(endpoint)); if (info != null) { info.noteIdle(this); } idleTime = 0; } } /* -- Implement HttpClientSocketFactory -- */ /** * Creates a plain socket to use to talk to the proxy host, else * creates an SSL socket for a direct connection to the server. */ public Socket createSocket(String host, int port) throws IOException { Socket s = createPlainSocket(host, port); if (proxyHost.length() == 0) { s = setSSLSocket( (SSLSocket) sslSocketFactory.createSocket( s, host, port, true)); } return s; } /** * Creates an SSL socket on top of the one the HTTP code used to * connect through the proxy. */ public Socket createTunnelSocket(Socket s) throws IOException { return setSSLSocket( (SSLSocket) sslSocketFactory.createSocket( s, s.getInetAddress().getHostName(), s.getPort(), true /* autoClose */)); } /** * Stores the new socket in the sslSocket field, does a handshake on * it, and returns it. */ private Socket setSSLSocket(SSLSocket newSocket) throws IOException { sslSocket = newSocket; establishSuites(); return sslSocket; } /** * Return true if this connection is using an HTTP proxy. */ boolean usesHttpProxy() { return proxyHost.length() != 0; } /** * Forward a ping request to the underlying HttpClientConnection. */ private boolean ping() throws IOException { return httpClient.ping(); } /** * Return the proxy host name. */ protected String getProxyHost() { return proxyHost; } } /** * Subclass of HttpClientConnection that closes the associated connection * when it shuts down and moves it to the idle list when it becomes idle. */ private static final class HttpClient extends HttpClientConnection { /** The associated secure connection. */ private final HttpsConnection connection; HttpClient(String host, int port, HttpsConnection connection) throws IOException { super(host, port, connection, httpClientManager); this.connection = connection; } HttpClient(String targetHost, int targetPort, String proxyHost, int proxyPort, HttpsConnection connection) throws IOException { super(targetHost, targetPort, proxyHost, proxyPort, true /* tunnel */, true, connection, httpClientManager); this.connection = connection; } /** Tells the connection that it is idle. */ protected void idle() { connection.noteIdle(); } /** Closes the associated secure connection. */ public boolean shutdown(boolean force) { boolean result = super.shutdown(force); if (result) { try { connection.close(); } catch (IOException e) { } } return result; } } /** * Manages the open connections associated with endpoints equal to a * representative endpoint. */ private static final class EndpointInfo { /** * A representative endpoint that equals the endpoint for all the * connections. */ private final HttpsEndpointImpl endpoint; /** The proxy host, or empty string if using a direct connection. */ private String proxyHost = ""; /** The proxy port, ignored if using a direct connection. */ private int proxyPort = 0; /** The idle connections for the endpoint. */ private final List idle = new ArrayList(1); /** The connections that are in use for the endpoint. */ private final List inUse = new ArrayList(1); EndpointInfo(HttpsEndpointImpl endpoint) { this.endpoint = endpoint; } /** * Chooses and returns an idle connection that satisfies the * constraints, removing the connection from the list of idle * connections and adding it to the list of ones in use, else returns * null. */ HttpsConnection connect(CallContext context) throws IOException { HttpSettings settings = getHttpSettings(); String phost = settings.getProxyHost(endpoint.serverHost); int pport = (phost.length() == 0) ? 0 : settings.getProxyPort(); HttpsConnection result; synchronized (this) { List reap = new ArrayList(0); if (!(proxyHost.equals(phost) && proxyPort == pport)) { proxyHost = phost; proxyPort = pport; reap.addAll(idle); idle.clear(); } else { checkIdle(System.currentTimeMillis(), reap); } for (int i = reap.size(); --i >= 0; ) { try { ((HttpsConnection) reap.get(i)).close(); } catch (IOException e) { } } result = (HttpsConnection) endpoint.connect( context, Collections.EMPTY_SET, idle); if (result != null) { idle.remove(result); } else { result = new HttpsConnection(endpoint, context, endpoint.serverHost, endpoint.port, phost, pport, endpoint.socketFactory); result.establishCallContext(); } } synchronized (endpointMap) { EndpointInfo info = (EndpointInfo) endpointMap.get(endpoint); if (info == null) { if (endpointMap.isEmpty()) { systemExecutor.execute( new Reaper(), "HttpsEndpointReaper"); } endpointMap.put(endpoint, this); info = this; } info.noteInUse(result); } return result; } /** Adds a connection to the set of connections in use. */ synchronized void noteInUse(HttpsConnection connection) { inUse.add(connection); } /** Removes a connection from the set of connections in use. */ synchronized void noteClosed(HttpsConnection connection) { inUse.remove(connection); } /** * Removes a connection from the set of connections in use and adds it * to the set of idle connections. */ synchronized void noteIdle(HttpsConnection connection) { inUse.remove(connection); if (proxyHost.equals(connection.proxyHost) && proxyPort == connection.proxyPort) { idle.add(connection); } else { try { connection.close(); } catch (IOException e) { } } } /** * For each connection, calls checkIdle on the connection and, if that * returns true, removes the connection and adds it to the reap list. * Returns true if no in-use or idle connections remain. */ synchronized boolean checkIdle(long now, List reap) { for (int i = idle.size(); --i >= 0; ) { HttpsConnection connection = (HttpsConnection) idle.get(i); if (connection.checkIdle(now)) { reap.add(connection); idle.remove(connection); } } return idle.isEmpty() && inUse.isEmpty(); } } /** * Implements OutboundRequest using the specified OutboundRequest, * HttpsConnection, and OutboundRequestHandle. */ private static final class HttpsOutboundRequest implements OutboundRequest { private final OutboundRequest request; private final HttpsConnection connection; private final OutboundRequestHandle handle; HttpsOutboundRequest(OutboundRequest request, HttpsConnection connection, OutboundRequestHandle handle) { this.connection = connection; this.request = request; this.handle = handle; } /* Delegate to connection */ public void populateContext(Collection context) { connection.populateContext(handle, context); } /* Delegate to connection */ public InvocationConstraints getUnfulfilledConstraints() { return connection.getUnfulfilledConstraints(handle); } /* Delegate to underlying request */ public OutputStream getRequestOutputStream() { return request.getRequestOutputStream(); } /* Delegate to underlying request */ public InputStream getResponseInputStream() { return request.getResponseInputStream(); } /* Delegate to underlying request */ public boolean getDeliveryStatus() { return request.getDeliveryStatus(); } /* Delegate to underlying request */ public void abort() { request.abort(); } } /** * Records idle times in connections and shuts them down if they have been * idle for at least IDLE_TIMEOUT milliseconds. */ private static final class Reaper implements Runnable { /** Non-private constructor to avoid accessor methods. */ Reaper() { } /** * Sleep for IDLE_TIMEOUT milliseconds. Then call checkIdle on each * connection, and, if that returns true, remove the connection from * the list of idle connections. Then shutdown all of the idle * connections that have been collected. Terminate if no connections * remain, else wait for the next timeout. */ public void run() { List reap = new ArrayList(1); boolean done; do { try { Thread.sleep(IDLE_TIMEOUT); } catch (InterruptedException e) { return; } long now = System.currentTimeMillis(); synchronized (endpointMap) { for (Iterator iter = endpointMap.values().iterator(); iter.hasNext(); ) { EndpointInfo info = (EndpointInfo) iter.next(); if (info.checkIdle(now, reap)) { iter.remove(); } } done = endpointMap.isEmpty(); } for (int i = reap.size(); --i >= 0; ) { try { ((HttpsConnection) reap.get(i)).close(); } catch (IOException e) { } } reap.clear(); } while (!done); } } }