/*
* Copyright (c) 2001, 2011, Oracle and/or its affiliates. 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle 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 Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.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.PrintStream;
import java.io.BufferedOutputStream;
import java.net.Socket;
import java.net.SocketException;
import java.net.URL;
import java.net.UnknownHostException;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.security.Principal;
import java.security.cert.*;
import java.util.StringTokenizer;
import java.util.Vector;
import java.security.AccessController;
import javax.security.auth.x500.X500Principal;
import javax.security.auth.kerberos.KerberosPrincipal;
import javax.net.ssl.*;
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:
- There's a Secure Sockets Layer between TCP
and the HTTP protocol code.
- It uses a different default TCP port.
- 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).
- 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.)
- 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.
System properties used include:
- https.proxyHost ... the host supporting SSL
tunneling using the conventional CONNECT syntax
- https.proxyPort ... port to use on proxyHost
- https.cipherSuites ... comma separated list of
SSL cipher suite names to enable.
- http.nonProxyHosts ...
Author: David Brownell, Bill Foote
/**
* 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) /** Returns the default HTTPS port (443) */
@Override
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.
Params: - URL – https URL with which a connection must be established
/**
* 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.
/**
* 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
/**
* 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
/**
* 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);
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. /** 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.
/**
* 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;
}
The following method, createSocket, is defined in NetworkClient
and overridden here so that the socket facroty is used to create
new sockets.
/**
* The following method, createSocket, is defined in NetworkClient
* and overridden here so that the socket facroty is used to create
* new sockets.
*/
@Override
protected Socket createSocket() throws IOException {
try {
return sslSocketFactory.createSocket();
} catch (SocketException se) {
//
// bug 6771432
// javax.net.SocketFactory throws a SocketException with an
// UnsupportedOperationException as its cause to indicate that
// unconnected sockets have not been implemented.
//
Throwable t = se.getCause();
if (t != null && t instanceof UnsupportedOperationException) {
return super.createSocket();
} else {
throw se;
}
}
}
@Override
public boolean needsTunneling() {
return (proxy != null && proxy.type() != Proxy.Type.DIRECT
&& proxy.type() != Proxy.Type.SOCKS);
}
@Override
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;
if (s instanceof SSLSocketImpl) {
((SSLSocketImpl)s).setHost(host);
}
}
} 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
//
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;
String cipher = session.getCipherSuite();
try {
HostnameChecker checker = HostnameChecker.getInstance(
HostnameChecker.TYPE_TLS);
Principal principal = getPeerPrincipal();
// X.500 principal or Kerberos principal.
// (Use ciphersuite check to determine whether Kerberos is present.)
if (cipher.startsWith("TLS_KRB5") &&
principal instanceof KerberosPrincipal) {
if (!HostnameChecker.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
}
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() + ">");
}
@Override
protected void putInKeepAliveCache() {
kac.put(url, sslSocketFactory, this);
}
Returns the cipher suite in use on this connection.
/**
* 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.
/**
* 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.
/**
* 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.
/**
* 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.
/**
* 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 = ((X509Certificate)certs[0]).getSubjectX500Principal();
}
return principal;
}
Returns the principal the client sent to the
server, or null if the client did not authenticate.
/**
* 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 = ((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.
/**
* 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.
*/
@Override
public void handshakeCompleted(HandshakeCompletedEvent event)
{
session = event.getSession();
}
Returns: the proxy host being used for this client, or null
if we're not going through a proxy
/**
* @return the proxy host being used for this client, or null
* if we're not going through a proxy
*/
@Override
public String getProxyHostUsed() {
if (!needsTunneling()) {
return null;
} else {
return super.getProxyHostUsed();
}
}
Returns: the proxy port being used for this client. Meaningless
if getProxyHostUsed() gives null.
/**
* @return the proxy port being used for this client. Meaningless
* if getProxyHostUsed() gives null.
*/
@Override
public int getProxyPortUsed() {
return (proxy == null || proxy.type() == Proxy.Type.DIRECT ||
proxy.type() == Proxy.Type.SOCKS)? -1:
((InetSocketAddress)proxy.address()).getPort();
}
}