jdk/src/share/classes/sun/net/www/protocol/https/HttpsClient.java
changeset 2 90ce3da70b43
child 98 4087c83cfab8
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/src/share/classes/sun/net/www/protocol/https/HttpsClient.java	Sat Dec 01 00:00:00 2007 +0000
@@ -0,0 +1,636 @@
+/*
+ * Copyright 2001-2007 Sun Microsystems, Inc.  All Rights Reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Sun designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Sun in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
+ * CA 95054 USA or visit www.sun.com if you need additional information or
+ * have any questions.
+ */
+
+
+package sun.net.www.protocol.https;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.FileInputStream;
+import java.io.PrintStream;
+import java.io.BufferedOutputStream;
+import java.net.Socket;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.CookieHandler;
+import java.net.Authenticator;
+import java.net.PasswordAuthentication;
+import java.security.Principal;
+import java.security.KeyStore;
+import java.security.PrivateKey;
+import java.security.cert.*;
+import java.util.StringTokenizer;
+import java.util.Vector;
+import java.util.Collection;
+import java.util.List;
+import java.util.Iterator;
+import java.security.AccessController;
+
+import javax.security.auth.x500.X500Principal;
+import javax.security.auth.kerberos.KerberosPrincipal;
+
+import javax.net.ssl.*;
+import sun.security.x509.X500Name;
+import sun.misc.Regexp;
+import sun.misc.RegexpPool;
+import sun.net.www.HeaderParser;
+import sun.net.www.MessageHeader;
+import sun.net.www.http.HttpClient;
+import sun.security.action.*;
+
+import sun.security.util.HostnameChecker;
+import sun.security.ssl.SSLSocketImpl;
+
+
+/**
+ * This class provides HTTPS client URL support, building on the standard
+ * "sun.net.www" HTTP protocol handler.  HTTPS is the same protocol as HTTP,
+ * but differs in the transport layer which it uses:  <UL>
+ *
+ *      <LI>There's a <em>Secure Sockets Layer</em> between TCP
+ *      and the HTTP protocol code.
+ *
+ *      <LI>It uses a different default TCP port.
+ *
+ *      <LI>It doesn't use application level proxies, which can see and
+ *      manipulate HTTP user level data, compromising privacy.  It uses
+ *      low level tunneling instead, which hides HTTP protocol and data
+ *      from all third parties.  (Traffic analysis is still possible).
+ *
+ *      <LI>It does basic server authentication, to protect
+ *      against "URL spoofing" attacks.  This involves deciding
+ *      whether the X.509 certificate chain identifying the server
+ *      is trusted, and verifying that the name of the server is
+ *      found in the certificate.  (The application may enable an
+ *      anonymous SSL cipher suite, and such checks are not done
+ *      for anonymous ciphers.)
+ *
+ *      <LI>It exposes key SSL session attributes, specifically the
+ *      cipher suite in use and the server's X509 certificates, to
+ *      application software which knows about this protocol handler.
+ *
+ *      </UL>
+ *
+ * <P> System properties used include:  <UL>
+ *
+ *      <LI><em>https.proxyHost</em> ... the host supporting SSL
+ *      tunneling using the conventional CONNECT syntax
+ *
+ *      <LI><em>https.proxyPort</em> ... port to use on proxyHost
+ *
+ *      <LI><em>https.cipherSuites</em> ... comma separated list of
+ *      SSL cipher suite names to enable.
+ *
+ *      <LI><em>http.nonProxyHosts</em> ...
+ *
+ *      </UL>
+ *
+ * @author David Brownell
+ * @author Bill Foote
+ */
+
+// final for export control reasons (access to APIs); remove with care
+final class HttpsClient extends HttpClient
+    implements HandshakeCompletedListener
+{
+    // STATIC STATE and ACCESSORS THERETO
+
+    // HTTPS uses a different default port number than HTTP.
+    private static final int    httpsPortNumber = 443;
+
+    /** Returns the default HTTPS port (443) */
+    protected int getDefaultPort() { return httpsPortNumber; }
+
+    private HostnameVerifier hv;
+    private SSLSocketFactory sslSocketFactory;
+
+    // HttpClient.proxyDisabled will always be false, because we don't
+    // use an application-level HTTP proxy.  We might tunnel through
+    // our http proxy, though.
+
+
+    // INSTANCE DATA
+
+    // last negotiated SSL session
+    private SSLSession  session;
+
+    private String [] getCipherSuites() {
+        //
+        // If ciphers are assigned, sort them into an array.
+        //
+        String ciphers [];
+        String cipherString = AccessController.doPrivileged(
+                new GetPropertyAction("https.cipherSuites"));
+
+        if (cipherString == null || "".equals(cipherString)) {
+            ciphers = null;
+        } else {
+            StringTokenizer     tokenizer;
+            Vector<String>      v = new Vector<String>();
+
+            tokenizer = new StringTokenizer(cipherString, ",");
+            while (tokenizer.hasMoreTokens())
+                v.addElement(tokenizer.nextToken());
+            ciphers = new String [v.size()];
+            for (int i = 0; i < ciphers.length; i++)
+                ciphers [i] = v.elementAt(i);
+        }
+        return ciphers;
+    }
+
+    private String [] getProtocols() {
+        //
+        // If protocols are assigned, sort them into an array.
+        //
+        String protocols [];
+        String protocolString = AccessController.doPrivileged(
+                new GetPropertyAction("https.protocols"));
+
+        if (protocolString == null || "".equals(protocolString)) {
+            protocols = null;
+        } else {
+            StringTokenizer     tokenizer;
+            Vector<String>      v = new Vector<String>();
+
+            tokenizer = new StringTokenizer(protocolString, ",");
+            while (tokenizer.hasMoreTokens())
+                v.addElement(tokenizer.nextToken());
+            protocols = new String [v.size()];
+            for (int i = 0; i < protocols.length; i++) {
+                protocols [i] = v.elementAt(i);
+            }
+        }
+        return protocols;
+    }
+
+    private String getUserAgent() {
+        String userAgent = java.security.AccessController.doPrivileged(
+                new sun.security.action.GetPropertyAction("https.agent"));
+        if (userAgent == null || userAgent.length() == 0) {
+            userAgent = "JSSE";
+        }
+        return userAgent;
+    }
+
+    // should remove once HttpClient.newHttpProxy is putback
+    private static Proxy newHttpProxy(String proxyHost, int proxyPort) {
+        InetSocketAddress saddr = null;
+        final String phost = proxyHost;
+        final int pport = proxyPort < 0 ? httpsPortNumber : proxyPort;
+        try {
+            saddr = java.security.AccessController.doPrivileged(new
+                java.security.PrivilegedExceptionAction<InetSocketAddress>() {
+                public InetSocketAddress run() {
+                    return new InetSocketAddress(phost, pport);
+                }});
+        } catch (java.security.PrivilegedActionException pae) {
+        }
+        return new Proxy(Proxy.Type.HTTP, saddr);
+    }
+
+    // CONSTRUCTOR, FACTORY
+
+
+    /**
+     * Create an HTTPS client URL.  Traffic will be tunneled through any
+     * intermediate nodes rather than proxied, so that confidentiality
+     * of data exchanged can be preserved.  However, note that all the
+     * anonymous SSL flavors are subject to "person-in-the-middle"
+     * attacks against confidentiality.  If you enable use of those
+     * flavors, you may be giving up the protection you get through
+     * SSL tunneling.
+     *
+     * Use New to get new HttpsClient. This constructor is meant to be
+     * used only by New method. New properly checks for URL spoofing.
+     *
+     * @param URL https URL with which a connection must be established
+     */
+    private HttpsClient(SSLSocketFactory sf, URL url)
+    throws IOException
+    {
+        // HttpClient-level proxying is always disabled,
+        // because we override doConnect to do tunneling instead.
+        this(sf, url, (String)null, -1);
+    }
+
+    /**
+     *  Create an HTTPS client URL.  Traffic will be tunneled through
+     * the specified proxy server.
+     */
+    HttpsClient(SSLSocketFactory sf, URL url, String proxyHost, int proxyPort)
+        throws IOException {
+        this(sf, url, proxyHost, proxyPort, -1);
+    }
+
+    /**
+     *  Create an HTTPS client URL.  Traffic will be tunneled through
+     * the specified proxy server, with a connect timeout
+     */
+    HttpsClient(SSLSocketFactory sf, URL url, String proxyHost, int proxyPort,
+                int connectTimeout)
+        throws IOException {
+        this(sf, url,
+             (proxyHost == null? null:
+                HttpsClient.newHttpProxy(proxyHost, proxyPort)),
+                connectTimeout);
+    }
+
+    /**
+     *  Same as previous constructor except using a Proxy
+     */
+    HttpsClient(SSLSocketFactory sf, URL url, Proxy proxy,
+                int connectTimeout)
+        throws IOException {
+        this.proxy = proxy;
+        setSSLSocketFactory(sf);
+        this.proxyDisabled = true;
+
+        this.host = url.getHost();
+        this.url = url;
+        port = url.getPort();
+        if (port == -1) {
+            port = getDefaultPort();
+        }
+        setConnectTimeout(connectTimeout);
+        // get the cookieHandler if there is any
+        cookieHandler = java.security.AccessController.doPrivileged(
+            new java.security.PrivilegedAction<CookieHandler>() {
+                public CookieHandler run() {
+                    return CookieHandler.getDefault();
+                }
+            });
+        openServer();
+    }
+
+
+    // This code largely ripped off from HttpClient.New, and
+    // it uses the same keepalive cache.
+
+    static HttpClient New(SSLSocketFactory sf, URL url, HostnameVerifier hv)
+            throws IOException {
+        return HttpsClient.New(sf, url, hv, true);
+    }
+
+    /** See HttpClient for the model for this method. */
+    static HttpClient New(SSLSocketFactory sf, URL url,
+            HostnameVerifier hv, boolean useCache) throws IOException {
+        return HttpsClient.New(sf, url, hv, (String)null, -1, useCache);
+    }
+
+    /**
+     * Get a HTTPS client to the URL.  Traffic will be tunneled through
+     * the specified proxy server.
+     */
+    static HttpClient New(SSLSocketFactory sf, URL url, HostnameVerifier hv,
+                           String proxyHost, int proxyPort) throws IOException {
+        return HttpsClient.New(sf, url, hv, proxyHost, proxyPort, true);
+    }
+
+    static HttpClient New(SSLSocketFactory sf, URL url, HostnameVerifier hv,
+                           String proxyHost, int proxyPort, boolean useCache)
+        throws IOException {
+        return HttpsClient.New(sf, url, hv, proxyHost, proxyPort, useCache, -1);
+    }
+
+    static HttpClient New(SSLSocketFactory sf, URL url, HostnameVerifier hv,
+                          String proxyHost, int proxyPort, boolean useCache,
+                          int connectTimeout)
+        throws IOException {
+
+        return HttpsClient.New(sf, url, hv,
+                               (proxyHost == null? null :
+                                HttpsClient.newHttpProxy(proxyHost, proxyPort)),
+                               useCache, connectTimeout);
+    }
+
+    static HttpClient New(SSLSocketFactory sf, URL url, HostnameVerifier hv,
+                          Proxy p, boolean useCache,
+                          int connectTimeout)
+        throws IOException {
+        HttpsClient ret = null;
+        if (useCache) {
+            /* see if one's already around */
+            ret = (HttpsClient) kac.get(url, sf);
+            if (ret != null) {
+                ret.cachedHttpClient = true;
+            }
+        }
+        if (ret == null) {
+            ret = new HttpsClient(sf, url, p, connectTimeout);
+        } else {
+            SecurityManager security = System.getSecurityManager();
+            if (security != null) {
+                security.checkConnect(url.getHost(), url.getPort());
+            }
+            ret.url = url;
+        }
+        ret.setHostnameVerifier(hv);
+
+        return ret;
+    }
+
+    // METHODS
+    void setHostnameVerifier(HostnameVerifier hv) {
+        this.hv = hv;
+    }
+
+    void setSSLSocketFactory(SSLSocketFactory sf) {
+        sslSocketFactory = sf;
+    }
+
+    SSLSocketFactory getSSLSocketFactory() {
+        return sslSocketFactory;
+    }
+
+    public boolean needsTunneling() {
+        return (proxy != null && proxy.type() != Proxy.Type.DIRECT
+                && proxy.type() != Proxy.Type.SOCKS);
+    }
+
+    public void afterConnect() throws IOException, UnknownHostException {
+        if (!isCachedConnection()) {
+            SSLSocket s = null;
+            SSLSocketFactory factory = sslSocketFactory;
+            try {
+                if (!(serverSocket instanceof SSLSocket)) {
+                    s = (SSLSocket)factory.createSocket(serverSocket,
+                                                        host, port, true);
+                } else {
+                    s = (SSLSocket)serverSocket;
+                }
+            } catch (IOException ex) {
+                // If we fail to connect through the tunnel, try it
+                // locally, as a last resort.  If this doesn't work,
+                // throw the original exception.
+                try {
+                    s = (SSLSocket)factory.createSocket(host, port);
+                } catch (IOException ignored) {
+                    throw ex;
+                }
+            }
+
+            //
+            // Force handshaking, so that we get any authentication.
+            // Register a handshake callback so our session state tracks any
+            // later session renegotiations.
+            //
+            String [] protocols = getProtocols();
+            String [] ciphers = getCipherSuites();
+            if (protocols != null) {
+                s.setEnabledProtocols(protocols);
+            }
+            if (ciphers != null) {
+                s.setEnabledCipherSuites(ciphers);
+            }
+            s.addHandshakeCompletedListener(this);
+
+            // if the HostnameVerifier is not set, try to enable endpoint
+            // identification during handshaking
+            boolean enabledIdentification = false;
+            if (hv instanceof DefaultHostnameVerifier &&
+                (s instanceof SSLSocketImpl) &&
+                ((SSLSocketImpl)s).trySetHostnameVerification("HTTPS")) {
+                enabledIdentification = true;
+            }
+
+            s.startHandshake();
+            session = s.getSession();
+            // change the serverSocket and serverOutput
+            serverSocket = s;
+            try {
+                serverOutput = new PrintStream(
+                    new BufferedOutputStream(serverSocket.getOutputStream()),
+                    false, encoding);
+            } catch (UnsupportedEncodingException e) {
+                throw new InternalError(encoding+" encoding not found");
+            }
+
+            // check URL spoofing if it has not been checked under handshaking
+            if (!enabledIdentification) {
+                checkURLSpoofing(hv);
+            }
+        } else {
+            // if we are reusing a cached https session,
+            // we don't need to do handshaking etc. But we do need to
+            // set the ssl session
+            session = ((SSLSocket)serverSocket).getSession();
+        }
+    }
+
+    // Server identity checking is done according to RFC 2818: HTTP over TLS
+    // Section 3.1 Server Identity
+    private void checkURLSpoofing(HostnameVerifier hostnameVerifier)
+            throws IOException
+    {
+        //
+        // Get authenticated server name, if any
+        //
+        boolean done = false;
+        String host = url.getHost();
+
+        // if IPv6 strip off the "[]"
+        if (host != null && host.startsWith("[") && host.endsWith("]")) {
+            host = host.substring(1, host.length()-1);
+        }
+
+        Certificate[] peerCerts = null;
+        try {
+            HostnameChecker checker = HostnameChecker.getInstance(
+                                                HostnameChecker.TYPE_TLS);
+
+            Principal principal = getPeerPrincipal();
+            if (principal instanceof KerberosPrincipal) {
+                if (!checker.match(host, (KerberosPrincipal)principal)) {
+                    throw new SSLPeerUnverifiedException("Hostname checker" +
+                                " failed for Kerberos");
+                }
+            } else {
+                // get the subject's certificate
+                peerCerts = session.getPeerCertificates();
+
+                X509Certificate peerCert;
+                if (peerCerts[0] instanceof
+                        java.security.cert.X509Certificate) {
+                    peerCert = (java.security.cert.X509Certificate)peerCerts[0];
+                } else {
+                    throw new SSLPeerUnverifiedException("");
+                }
+                checker.match(host, peerCert);
+            }
+
+            // if it doesn't throw an exception, we passed. Return.
+            return;
+
+        } catch (SSLPeerUnverifiedException e) {
+
+            //
+            // client explicitly changed default policy and enabled
+            // anonymous ciphers; we can't check the standard policy
+            //
+            // ignore
+        } catch (java.security.cert.CertificateException cpe) {
+            // ignore
+        }
+
+        String cipher = session.getCipherSuite();
+        if ((cipher != null) && (cipher.indexOf("_anon_") != -1)) {
+            return;
+        } else if ((hostnameVerifier != null) &&
+                   (hostnameVerifier.verify(host, session))) {
+            return;
+        }
+
+        serverSocket.close();
+        session.invalidate();
+
+        throw new IOException("HTTPS hostname wrong:  should be <"
+                              + url.getHost() + ">");
+    }
+
+    protected void putInKeepAliveCache() {
+        kac.put(url, sslSocketFactory, this);
+    }
+
+    /**
+     * Returns the cipher suite in use on this connection.
+     */
+    String getCipherSuite() {
+        return session.getCipherSuite();
+    }
+
+    /**
+     * Returns the certificate chain the client sent to the
+     * server, or null if the client did not authenticate.
+     */
+    public java.security.cert.Certificate [] getLocalCertificates() {
+        return session.getLocalCertificates();
+    }
+
+    /**
+     * Returns the certificate chain with which the server
+     * authenticated itself, or throw a SSLPeerUnverifiedException
+     * if the server did not authenticate.
+     */
+    java.security.cert.Certificate [] getServerCertificates()
+            throws SSLPeerUnverifiedException
+    {
+        return session.getPeerCertificates();
+    }
+
+    /**
+     * Returns the X.509 certificate chain with which the server
+     * authenticated itself, or null if the server did not authenticate.
+     */
+    javax.security.cert.X509Certificate [] getServerCertificateChain()
+            throws SSLPeerUnverifiedException
+    {
+        return session.getPeerCertificateChain();
+    }
+
+    /**
+     * Returns the principal with which the server authenticated
+     * itself, or throw a SSLPeerUnverifiedException if the
+     * server did not authenticate.
+     */
+    Principal getPeerPrincipal()
+            throws SSLPeerUnverifiedException
+    {
+        Principal principal;
+        try {
+            principal = session.getPeerPrincipal();
+        } catch (AbstractMethodError e) {
+            // if the provider does not support it, fallback to peer certs.
+            // return the X500Principal of the end-entity cert.
+            java.security.cert.Certificate[] certs =
+                        session.getPeerCertificates();
+            principal = (X500Principal)
+                ((X509Certificate)certs[0]).getSubjectX500Principal();
+        }
+        return principal;
+    }
+
+    /**
+     * Returns the principal the client sent to the
+     * server, or null if the client did not authenticate.
+     */
+    Principal getLocalPrincipal()
+    {
+        Principal principal;
+        try {
+            principal = session.getLocalPrincipal();
+        } catch (AbstractMethodError e) {
+            principal = null;
+            // if the provider does not support it, fallback to local certs.
+            // return the X500Principal of the end-entity cert.
+            java.security.cert.Certificate[] certs =
+                        session.getLocalCertificates();
+            if (certs != null) {
+                principal = (X500Principal)
+                    ((X509Certificate)certs[0]).getSubjectX500Principal();
+            }
+        }
+        return principal;
+    }
+
+    /**
+     * This method implements the SSL HandshakeCompleted callback,
+     * remembering the resulting session so that it may be queried
+     * for the current cipher suite and peer certificates.  Servers
+     * sometimes re-initiate handshaking, so the session in use on
+     * a given connection may change.  When sessions change, so may
+     * peer identities and cipher suites.
+     */
+    public void handshakeCompleted(HandshakeCompletedEvent event)
+    {
+        session = event.getSession();
+    }
+
+    /**
+     * @return the proxy host being used for this client, or null
+     *          if we're not going through a proxy
+     */
+    public String getProxyHostUsed() {
+        if (!needsTunneling()) {
+            return null;
+        } else {
+            return ((InetSocketAddress)proxy.address()).getHostName();
+        }
+    }
+
+    /**
+     * @return the proxy port being used for this client.  Meaningless
+     *          if getProxyHostUsed() gives null.
+     */
+    public int getProxyPortUsed() {
+        return (proxy == null || proxy.type() == Proxy.Type.DIRECT ||
+                proxy.type() == Proxy.Type.SOCKS)? -1:
+            ((InetSocketAddress)proxy.address()).getPort();
+    }
+}