| /* |
| * 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 libcore.net.http; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.net.CacheResponse; |
| import java.net.HttpURLConnection; |
| import java.net.ProtocolException; |
| import java.net.Proxy; |
| import java.net.SecureCacheResponse; |
| import java.net.URL; |
| import java.security.Permission; |
| import java.security.Principal; |
| import java.security.cert.Certificate; |
| import java.security.cert.CertificateException; |
| import java.util.List; |
| import java.util.Map; |
| import javax.net.ssl.HttpsURLConnection; |
| import javax.net.ssl.SSLHandshakeException; |
| import javax.net.ssl.SSLPeerUnverifiedException; |
| import javax.net.ssl.SSLSocket; |
| import javax.net.ssl.SSLSocketFactory; |
| |
| final class HttpsURLConnectionImpl extends HttpsURLConnection { |
| |
| /** HttpUrlConnectionDelegate allows reuse of HttpURLConnectionImpl */ |
| private final HttpUrlConnectionDelegate delegate; |
| |
| protected HttpsURLConnectionImpl(URL url, int port) { |
| super(url); |
| delegate = new HttpUrlConnectionDelegate(url, port); |
| } |
| |
| protected HttpsURLConnectionImpl(URL url, int port, Proxy proxy) { |
| super(url); |
| delegate = new HttpUrlConnectionDelegate(url, port, proxy); |
| } |
| |
| private void checkConnected() { |
| if (delegate.getSSLSocket() == null) { |
| throw new IllegalStateException("Connection has not yet been established"); |
| } |
| } |
| |
| HttpEngine getHttpEngine() { |
| return delegate.getHttpEngine(); |
| } |
| |
| @Override |
| public String getCipherSuite() { |
| SecureCacheResponse cacheResponse = delegate.getCacheResponse(); |
| if (cacheResponse != null) { |
| return cacheResponse.getCipherSuite(); |
| } |
| checkConnected(); |
| return delegate.getSSLSocket().getSession().getCipherSuite(); |
| } |
| |
| @Override |
| public Certificate[] getLocalCertificates() { |
| SecureCacheResponse cacheResponse = delegate.getCacheResponse(); |
| if (cacheResponse != null) { |
| List<Certificate> result = cacheResponse.getLocalCertificateChain(); |
| return result != null ? result.toArray(new Certificate[result.size()]) : null; |
| } |
| checkConnected(); |
| return delegate.getSSLSocket().getSession().getLocalCertificates(); |
| } |
| |
| @Override |
| public Certificate[] getServerCertificates() throws SSLPeerUnverifiedException { |
| SecureCacheResponse cacheResponse = delegate.getCacheResponse(); |
| if (cacheResponse != null) { |
| List<Certificate> result = cacheResponse.getServerCertificateChain(); |
| return result != null ? result.toArray(new Certificate[result.size()]) : null; |
| } |
| checkConnected(); |
| return delegate.getSSLSocket().getSession().getPeerCertificates(); |
| } |
| |
| @Override |
| public Principal getPeerPrincipal() throws SSLPeerUnverifiedException { |
| SecureCacheResponse cacheResponse = delegate.getCacheResponse(); |
| if (cacheResponse != null) { |
| return cacheResponse.getPeerPrincipal(); |
| } |
| checkConnected(); |
| return delegate.getSSLSocket().getSession().getPeerPrincipal(); |
| } |
| |
| @Override |
| public Principal getLocalPrincipal() { |
| SecureCacheResponse cacheResponse = delegate.getCacheResponse(); |
| if (cacheResponse != null) { |
| return cacheResponse.getLocalPrincipal(); |
| } |
| checkConnected(); |
| return delegate.getSSLSocket().getSession().getLocalPrincipal(); |
| } |
| |
| @Override |
| public void disconnect() { |
| delegate.disconnect(); |
| } |
| |
| @Override |
| public InputStream getErrorStream() { |
| return delegate.getErrorStream(); |
| } |
| |
| @Override |
| public String getRequestMethod() { |
| return delegate.getRequestMethod(); |
| } |
| |
| @Override |
| public int getResponseCode() throws IOException { |
| return delegate.getResponseCode(); |
| } |
| |
| @Override |
| public String getResponseMessage() throws IOException { |
| return delegate.getResponseMessage(); |
| } |
| |
| @Override |
| public void setRequestMethod(String method) throws ProtocolException { |
| delegate.setRequestMethod(method); |
| } |
| |
| @Override |
| public boolean usingProxy() { |
| return delegate.usingProxy(); |
| } |
| |
| @Override |
| public boolean getInstanceFollowRedirects() { |
| return delegate.getInstanceFollowRedirects(); |
| } |
| |
| @Override |
| public void setInstanceFollowRedirects(boolean followRedirects) { |
| delegate.setInstanceFollowRedirects(followRedirects); |
| } |
| |
| @Override |
| public void connect() throws IOException { |
| connected = true; |
| delegate.connect(); |
| } |
| |
| @Override |
| public boolean getAllowUserInteraction() { |
| return delegate.getAllowUserInteraction(); |
| } |
| |
| @Override |
| public Object getContent() throws IOException { |
| return delegate.getContent(); |
| } |
| |
| @SuppressWarnings("unchecked") // Spec does not generify |
| @Override |
| public Object getContent(Class[] types) throws IOException { |
| return delegate.getContent(types); |
| } |
| |
| @Override |
| public String getContentEncoding() { |
| return delegate.getContentEncoding(); |
| } |
| |
| @Override |
| public int getContentLength() { |
| return delegate.getContentLength(); |
| } |
| |
| @Override |
| public String getContentType() { |
| return delegate.getContentType(); |
| } |
| |
| @Override |
| public long getDate() { |
| return delegate.getDate(); |
| } |
| |
| @Override |
| public boolean getDefaultUseCaches() { |
| return delegate.getDefaultUseCaches(); |
| } |
| |
| @Override |
| public boolean getDoInput() { |
| return delegate.getDoInput(); |
| } |
| |
| @Override |
| public boolean getDoOutput() { |
| return delegate.getDoOutput(); |
| } |
| |
| @Override |
| public long getExpiration() { |
| return delegate.getExpiration(); |
| } |
| |
| @Override |
| public String getHeaderField(int pos) { |
| return delegate.getHeaderField(pos); |
| } |
| |
| @Override |
| public Map<String, List<String>> getHeaderFields() { |
| return delegate.getHeaderFields(); |
| } |
| |
| @Override |
| public Map<String, List<String>> getRequestProperties() { |
| return delegate.getRequestProperties(); |
| } |
| |
| @Override |
| public void addRequestProperty(String field, String newValue) { |
| delegate.addRequestProperty(field, newValue); |
| } |
| |
| @Override |
| public String getHeaderField(String key) { |
| return delegate.getHeaderField(key); |
| } |
| |
| @Override |
| public long getHeaderFieldDate(String field, long defaultValue) { |
| return delegate.getHeaderFieldDate(field, defaultValue); |
| } |
| |
| @Override |
| public int getHeaderFieldInt(String field, int defaultValue) { |
| return delegate.getHeaderFieldInt(field, defaultValue); |
| } |
| |
| @Override |
| public String getHeaderFieldKey(int posn) { |
| return delegate.getHeaderFieldKey(posn); |
| } |
| |
| @Override |
| public long getIfModifiedSince() { |
| return delegate.getIfModifiedSince(); |
| } |
| |
| @Override |
| public InputStream getInputStream() throws IOException { |
| return delegate.getInputStream(); |
| } |
| |
| @Override |
| public long getLastModified() { |
| return delegate.getLastModified(); |
| } |
| |
| @Override |
| public OutputStream getOutputStream() throws IOException { |
| return delegate.getOutputStream(); |
| } |
| |
| @Override |
| public Permission getPermission() throws IOException { |
| return delegate.getPermission(); |
| } |
| |
| @Override |
| public String getRequestProperty(String field) { |
| return delegate.getRequestProperty(field); |
| } |
| |
| @Override |
| public URL getURL() { |
| return delegate.getURL(); |
| } |
| |
| @Override |
| public boolean getUseCaches() { |
| return delegate.getUseCaches(); |
| } |
| |
| @Override |
| public void setAllowUserInteraction(boolean newValue) { |
| delegate.setAllowUserInteraction(newValue); |
| } |
| |
| @Override |
| public void setDefaultUseCaches(boolean newValue) { |
| delegate.setDefaultUseCaches(newValue); |
| } |
| |
| @Override |
| public void setDoInput(boolean newValue) { |
| delegate.setDoInput(newValue); |
| } |
| |
| @Override |
| public void setDoOutput(boolean newValue) { |
| delegate.setDoOutput(newValue); |
| } |
| |
| @Override |
| public void setIfModifiedSince(long newValue) { |
| delegate.setIfModifiedSince(newValue); |
| } |
| |
| @Override |
| public void setRequestProperty(String field, String newValue) { |
| delegate.setRequestProperty(field, newValue); |
| } |
| |
| @Override |
| public void setUseCaches(boolean newValue) { |
| delegate.setUseCaches(newValue); |
| } |
| |
| @Override |
| public void setConnectTimeout(int timeoutMillis) { |
| delegate.setConnectTimeout(timeoutMillis); |
| } |
| |
| @Override |
| public int getConnectTimeout() { |
| return delegate.getConnectTimeout(); |
| } |
| |
| @Override |
| public void setReadTimeout(int timeoutMillis) { |
| delegate.setReadTimeout(timeoutMillis); |
| } |
| |
| @Override |
| public int getReadTimeout() { |
| return delegate.getReadTimeout(); |
| } |
| |
| @Override |
| public String toString() { |
| return delegate.toString(); |
| } |
| |
| @Override |
| public void setFixedLengthStreamingMode(int contentLength) { |
| delegate.setFixedLengthStreamingMode(contentLength); |
| } |
| |
| @Override |
| public void setChunkedStreamingMode(int chunkLength) { |
| delegate.setChunkedStreamingMode(chunkLength); |
| } |
| |
| private final class HttpUrlConnectionDelegate extends HttpURLConnectionImpl { |
| private HttpUrlConnectionDelegate(URL url, int port) { |
| super(url, port); |
| } |
| |
| private HttpUrlConnectionDelegate(URL url, int port, Proxy proxy) { |
| super(url, port, proxy); |
| } |
| |
| @Override protected HttpEngine newHttpEngine(String method, RawHeaders requestHeaders, |
| HttpConnection connection, RetryableOutputStream requestBody) throws IOException { |
| return new HttpsEngine(this, method, requestHeaders, connection, requestBody, |
| HttpsURLConnectionImpl.this); |
| } |
| |
| public SecureCacheResponse getCacheResponse() { |
| HttpsEngine engine = (HttpsEngine) httpEngine; |
| return engine != null ? (SecureCacheResponse) engine.getCacheResponse() : null; |
| } |
| |
| public SSLSocket getSSLSocket() { |
| HttpsEngine engine = (HttpsEngine) httpEngine; |
| return engine != null ? engine.sslSocket : null; |
| } |
| } |
| |
| private static class HttpsEngine extends HttpEngine { |
| |
| /** |
| * Local stash of HttpsEngine.connection.sslSocket for answering |
| * queries such as getCipherSuite even after |
| * httpsEngine.Connection has been recycled. It's presence is also |
| * used to tell if the HttpsURLConnection is considered connected, |
| * as opposed to the connected field of URLConnection or the a |
| * non-null connect in HttpURLConnectionImpl |
| */ |
| private SSLSocket sslSocket; |
| |
| private final HttpsURLConnectionImpl enclosing; |
| |
| /** |
| * @param policy the HttpURLConnectionImpl with connection configuration |
| * @param enclosing the HttpsURLConnection with HTTPS features |
| */ |
| private HttpsEngine(HttpURLConnectionImpl policy, String method, RawHeaders requestHeaders, |
| HttpConnection connection, RetryableOutputStream requestBody, |
| HttpsURLConnectionImpl enclosing) throws IOException { |
| super(policy, method, requestHeaders, connection, requestBody); |
| this.sslSocket = connection != null ? connection.getSecureSocketIfConnected() : null; |
| this.enclosing = enclosing; |
| } |
| |
| @Override protected void connect() throws IOException { |
| // first try an SSL connection with compression and |
| // various TLS extensions enabled, if it fails (and its |
| // not unheard of that it will) fallback to a more |
| // barebones connections |
| boolean connectionReused; |
| try { |
| connectionReused = makeSslConnection(true); |
| } catch (IOException e) { |
| // If the problem was a CertificateException from the X509TrustManager, |
| // do not retry, we didn't have an abrupt server initiated exception. |
| if (e instanceof SSLHandshakeException |
| && e.getCause() instanceof CertificateException) { |
| throw e; |
| } |
| release(false); |
| connectionReused = makeSslConnection(false); |
| } |
| |
| if (!connectionReused) { |
| sslSocket = connection.verifySecureSocketHostname(enclosing.getHostnameVerifier()); |
| } |
| } |
| |
| /** |
| * Attempt to make an https connection. Returns true if a |
| * connection was reused, false otherwise. |
| * |
| * @param tlsTolerant If true, assume server can handle common |
| * TLS extensions and SSL deflate compression. If false, use |
| * an SSL3 only fallback mode without compression. |
| */ |
| private boolean makeSslConnection(boolean tlsTolerant) throws IOException { |
| // make an SSL Tunnel on the first message pair of each SSL + proxy connection |
| if (connection == null) { |
| connection = openSocketConnection(); |
| if (connection.getAddress().getProxy() != null) { |
| makeTunnel(policy, connection, getRequestHeaders()); |
| } |
| } |
| |
| // if super.makeConnection returned a connection from the |
| // pool, sslSocket needs to be initialized here. If it is |
| // a new connection, it will be initialized by |
| // getSecureSocket below. |
| sslSocket = connection.getSecureSocketIfConnected(); |
| |
| // we already have an SSL connection, |
| if (sslSocket != null) { |
| return true; |
| } |
| |
| connection.setupSecureSocket(enclosing.getSSLSocketFactory(), tlsTolerant); |
| return false; |
| } |
| |
| /** |
| * To make an HTTPS connection over an HTTP proxy, send an unencrypted |
| * CONNECT request to create the proxy connection. This may need to be |
| * retried if the proxy requires authorization. |
| */ |
| private void makeTunnel(HttpURLConnectionImpl policy, HttpConnection connection, |
| RequestHeaders requestHeaders) throws IOException { |
| RawHeaders rawRequestHeaders = requestHeaders.getHeaders(); |
| while (true) { |
| HttpEngine connect = new ProxyConnectEngine(policy, rawRequestHeaders, connection); |
| connect.sendRequest(); |
| connect.readResponse(); |
| |
| int responseCode = connect.getResponseCode(); |
| switch (connect.getResponseCode()) { |
| case HTTP_OK: |
| return; |
| case HTTP_PROXY_AUTH: |
| rawRequestHeaders = new RawHeaders(rawRequestHeaders); |
| boolean credentialsFound = policy.processAuthHeader(HTTP_PROXY_AUTH, |
| connect.getResponseHeaders(), rawRequestHeaders); |
| if (credentialsFound) { |
| continue; |
| } else { |
| throw new IOException("Failed to authenticate with proxy"); |
| } |
| default: |
| throw new IOException("Unexpected response code for CONNECT: " + responseCode); |
| } |
| } |
| } |
| |
| @Override protected boolean acceptCacheResponseType(CacheResponse cacheResponse) { |
| return cacheResponse instanceof SecureCacheResponse; |
| } |
| |
| @Override protected boolean includeAuthorityInRequestLine() { |
| // Even if there is a proxy, it isn't involved. Always request just the file. |
| return false; |
| } |
| |
| @Override protected SSLSocketFactory getSslSocketFactory() { |
| return enclosing.getSSLSocketFactory(); |
| } |
| |
| @Override protected HttpURLConnection getHttpConnectionToCache() { |
| return enclosing; |
| } |
| } |
| |
| private static class ProxyConnectEngine extends HttpEngine { |
| public ProxyConnectEngine(HttpURLConnectionImpl policy, RawHeaders requestHeaders, |
| HttpConnection connection) throws IOException { |
| super(policy, HttpEngine.CONNECT, requestHeaders, connection, null); |
| } |
| |
| /** |
| * If we're establishing an HTTPS tunnel with CONNECT (RFC 2817 5.2), send |
| * only the minimum set of headers. This avoids sending potentially |
| * sensitive data like HTTP cookies to the proxy unencrypted. |
| */ |
| @Override protected RawHeaders getNetworkRequestHeaders() throws IOException { |
| RequestHeaders privateHeaders = getRequestHeaders(); |
| URL url = policy.getURL(); |
| |
| RawHeaders result = new RawHeaders(); |
| result.setStatusLine("CONNECT " + url.getHost() + ":" + url.getEffectivePort() |
| + " HTTP/1.1"); |
| |
| // Always set Host and User-Agent. |
| String host = privateHeaders.getHost(); |
| if (host == null) { |
| host = getOriginAddress(url); |
| } |
| result.set("Host", host); |
| |
| String userAgent = privateHeaders.getUserAgent(); |
| if (userAgent == null) { |
| userAgent = getDefaultUserAgent(); |
| } |
| result.set("User-Agent", userAgent); |
| |
| // Copy over the Proxy-Authorization header if it exists. |
| String proxyAuthorization = privateHeaders.getProxyAuthorization(); |
| if (proxyAuthorization != null) { |
| result.set("Proxy-Authorization", proxyAuthorization); |
| } |
| |
| // Always set the Proxy-Connection to Keep-Alive for the benefit of |
| // HTTP/1.0 proxies like Squid. |
| result.set("Proxy-Connection", "Keep-Alive"); |
| return result; |
| } |
| |
| @Override protected boolean requiresTunnel() { |
| return true; |
| } |
| } |
| } |