| /* |
| * Copyright (C) 2007 The Android Open Source Project |
| * |
| * Licensed 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 android.net.http; |
| |
| import android.content.Context; |
| import android.util.Log; |
| import com.android.org.conscrypt.Conscrypt; |
| import com.android.org.conscrypt.FileClientSessionCache; |
| import com.android.org.conscrypt.OpenSSLContextImpl; |
| import com.android.org.conscrypt.SSLClientSessionCache; |
| import org.apache.http.Header; |
| import org.apache.http.HttpException; |
| import org.apache.http.HttpHost; |
| import org.apache.http.HttpStatus; |
| import org.apache.http.ParseException; |
| import org.apache.http.ProtocolVersion; |
| import org.apache.http.StatusLine; |
| import org.apache.http.message.BasicHttpRequest; |
| import org.apache.http.params.BasicHttpParams; |
| import org.apache.http.params.HttpConnectionParams; |
| import org.apache.http.params.HttpParams; |
| |
| import javax.net.ssl.SSLException; |
| import javax.net.ssl.SSLSocket; |
| import javax.net.ssl.SSLSocketFactory; |
| import javax.net.ssl.TrustManager; |
| import javax.net.ssl.X509TrustManager; |
| import java.io.File; |
| import java.io.IOException; |
| import java.net.Socket; |
| import java.security.KeyManagementException; |
| import java.security.cert.X509Certificate; |
| import java.util.Locale; |
| |
| /** |
| * A Connection connecting to a secure http server or tunneling through |
| * a http proxy server to a https server. |
| */ |
| public class HttpsConnection extends Connection { |
| |
| /** |
| * SSL socket factory |
| */ |
| private static SSLSocketFactory mSslSocketFactory = null; |
| |
| static { |
| // This initialization happens in the zygote. It triggers some |
| // lazy initialization that can will benefit later invocations of |
| // initializeEngine(). |
| initializeEngine(null); |
| } |
| |
| /** |
| * @param sessionDir directory to cache SSL sessions |
| */ |
| public static void initializeEngine(File sessionDir) { |
| try { |
| SSLClientSessionCache cache = null; |
| if (sessionDir != null) { |
| Log.d("HttpsConnection", "Caching SSL sessions in " |
| + sessionDir + "."); |
| cache = FileClientSessionCache.usingDirectory(sessionDir); |
| } |
| |
| OpenSSLContextImpl sslContext = (OpenSSLContextImpl) Conscrypt.newPreferredSSLContextSpi(); |
| |
| // here, trust managers is a single trust-all manager |
| TrustManager[] trustManagers = new TrustManager[] { |
| new X509TrustManager() { |
| public X509Certificate[] getAcceptedIssuers() { |
| return null; |
| } |
| |
| public void checkClientTrusted( |
| X509Certificate[] certs, String authType) { |
| } |
| |
| public void checkServerTrusted( |
| X509Certificate[] certs, String authType) { |
| } |
| } |
| }; |
| |
| sslContext.engineInit(null, trustManagers, null); |
| sslContext.engineGetClientSessionContext().setPersistentCache(cache); |
| |
| synchronized (HttpsConnection.class) { |
| mSslSocketFactory = sslContext.engineGetSocketFactory(); |
| } |
| } catch (KeyManagementException e) { |
| throw new RuntimeException(e); |
| } catch (IOException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| private synchronized static SSLSocketFactory getSocketFactory() { |
| return mSslSocketFactory; |
| } |
| |
| /** |
| * Object to wait on when suspending the SSL connection |
| */ |
| private Object mSuspendLock = new Object(); |
| |
| /** |
| * True if the connection is suspended pending the result of asking the |
| * user about an error. |
| */ |
| private boolean mSuspended = false; |
| |
| /** |
| * True if the connection attempt should be aborted due to an ssl |
| * error. |
| */ |
| private boolean mAborted = false; |
| |
| // Used when connecting through a proxy. |
| private HttpHost mProxyHost; |
| |
| /** |
| * Contructor for a https connection. |
| */ |
| HttpsConnection(Context context, HttpHost host, HttpHost proxy, |
| RequestFeeder requestFeeder) { |
| super(context, host, requestFeeder); |
| mProxyHost = proxy; |
| } |
| |
| /** |
| * Sets the server SSL certificate associated with this |
| * connection. |
| * @param certificate The SSL certificate |
| */ |
| /* package */ void setCertificate(SslCertificate certificate) { |
| mCertificate = certificate; |
| } |
| |
| /** |
| * Opens the connection to a http server or proxy. |
| * |
| * @return the opened low level connection |
| * @throws IOException if the connection fails for any reason. |
| */ |
| @Override |
| AndroidHttpClientConnection openConnection(Request req) throws IOException { |
| SSLSocket sslSock = null; |
| |
| if (mProxyHost != null) { |
| // If we have a proxy set, we first send a CONNECT request |
| // to the proxy; if the proxy returns 200 OK, we negotiate |
| // a secure connection to the target server via the proxy. |
| // If the request fails, we drop it, but provide the event |
| // handler with the response status and headers. The event |
| // handler is then responsible for cancelling the load or |
| // issueing a new request. |
| AndroidHttpClientConnection proxyConnection = null; |
| Socket proxySock = null; |
| try { |
| proxySock = new Socket |
| (mProxyHost.getHostName(), mProxyHost.getPort()); |
| |
| proxySock.setSoTimeout(60 * 1000); |
| |
| proxyConnection = new AndroidHttpClientConnection(); |
| HttpParams params = new BasicHttpParams(); |
| HttpConnectionParams.setSocketBufferSize(params, 8192); |
| |
| proxyConnection.bind(proxySock, params); |
| } catch(IOException e) { |
| if (proxyConnection != null) { |
| proxyConnection.close(); |
| } |
| |
| String errorMessage = e.getMessage(); |
| if (errorMessage == null) { |
| errorMessage = |
| "failed to establish a connection to the proxy"; |
| } |
| |
| throw new IOException(errorMessage); |
| } |
| |
| StatusLine statusLine = null; |
| int statusCode = 0; |
| Headers headers = new Headers(); |
| try { |
| BasicHttpRequest proxyReq = new BasicHttpRequest |
| ("CONNECT", mHost.toHostString()); |
| |
| // add all 'proxy' headers from the original request, we also need |
| // to add 'host' header unless we want proxy to answer us with a |
| // 400 Bad Request |
| for (Header h : req.mHttpRequest.getAllHeaders()) { |
| String headerName = h.getName().toLowerCase(Locale.ROOT); |
| if (headerName.startsWith("proxy") || headerName.equals("keep-alive") |
| || headerName.equals("host")) { |
| proxyReq.addHeader(h); |
| } |
| } |
| |
| proxyConnection.sendRequestHeader(proxyReq); |
| proxyConnection.flush(); |
| |
| // it is possible to receive informational status |
| // codes prior to receiving actual headers; |
| // all those status codes are smaller than OK 200 |
| // a loop is a standard way of dealing with them |
| do { |
| statusLine = proxyConnection.parseResponseHeader(headers); |
| statusCode = statusLine.getStatusCode(); |
| } while (statusCode < HttpStatus.SC_OK); |
| } catch (ParseException e) { |
| String errorMessage = e.getMessage(); |
| if (errorMessage == null) { |
| errorMessage = |
| "failed to send a CONNECT request"; |
| } |
| |
| throw new IOException(errorMessage); |
| } catch (HttpException e) { |
| String errorMessage = e.getMessage(); |
| if (errorMessage == null) { |
| errorMessage = |
| "failed to send a CONNECT request"; |
| } |
| |
| throw new IOException(errorMessage); |
| } catch (IOException e) { |
| String errorMessage = e.getMessage(); |
| if (errorMessage == null) { |
| errorMessage = |
| "failed to send a CONNECT request"; |
| } |
| |
| throw new IOException(errorMessage); |
| } |
| |
| if (statusCode == HttpStatus.SC_OK) { |
| try { |
| sslSock = (SSLSocket) getSocketFactory().createSocket( |
| proxySock, mHost.getHostName(), mHost.getPort(), true); |
| } catch(IOException e) { |
| if (sslSock != null) { |
| sslSock.close(); |
| } |
| |
| String errorMessage = e.getMessage(); |
| if (errorMessage == null) { |
| errorMessage = |
| "failed to create an SSL socket"; |
| } |
| throw new IOException(errorMessage); |
| } |
| } else { |
| // if the code is not OK, inform the event handler |
| ProtocolVersion version = statusLine.getProtocolVersion(); |
| |
| req.mEventHandler.status(version.getMajor(), |
| version.getMinor(), |
| statusCode, |
| statusLine.getReasonPhrase()); |
| req.mEventHandler.headers(headers); |
| req.mEventHandler.endData(); |
| |
| proxyConnection.close(); |
| |
| // here, we return null to indicate that the original |
| // request needs to be dropped |
| return null; |
| } |
| } else { |
| // if we do not have a proxy, we simply connect to the host |
| try { |
| sslSock = (SSLSocket) getSocketFactory().createSocket( |
| mHost.getHostName(), mHost.getPort()); |
| sslSock.setSoTimeout(SOCKET_TIMEOUT); |
| } catch(IOException e) { |
| if (sslSock != null) { |
| sslSock.close(); |
| } |
| |
| String errorMessage = e.getMessage(); |
| if (errorMessage == null) { |
| errorMessage = "failed to create an SSL socket"; |
| } |
| |
| throw new IOException(errorMessage); |
| } |
| } |
| |
| // do handshake and validate server certificates |
| SslError error = CertificateChainValidator.getInstance(). |
| doHandshakeAndValidateServerCertificates(this, sslSock, mHost.getHostName()); |
| |
| // Inform the user if there is a problem |
| if (error != null) { |
| // handleSslErrorRequest may immediately unsuspend if it wants to |
| // allow the certificate anyway. |
| // So we mark the connection as suspended, call handleSslErrorRequest |
| // then check if we're still suspended and only wait if we actually |
| // need to. |
| synchronized (mSuspendLock) { |
| mSuspended = true; |
| } |
| // don't hold the lock while calling out to the event handler |
| boolean canHandle = req.getEventHandler().handleSslErrorRequest(error); |
| if(!canHandle) { |
| throw new IOException("failed to handle "+ error); |
| } |
| synchronized (mSuspendLock) { |
| if (mSuspended) { |
| try { |
| // Put a limit on how long we are waiting; if the timeout |
| // expires (which should never happen unless you choose |
| // to ignore the SSL error dialog for a very long time), |
| // we wake up the thread and abort the request. This is |
| // to prevent us from stalling the network if things go |
| // very bad. |
| mSuspendLock.wait(10 * 60 * 1000); |
| if (mSuspended) { |
| // mSuspended is true if we have not had a chance to |
| // restart the connection yet (ie, the wait timeout |
| // has expired) |
| mSuspended = false; |
| mAborted = true; |
| if (HttpLog.LOGV) { |
| HttpLog.v("HttpsConnection.openConnection():" + |
| " SSL timeout expired and request was cancelled!!!"); |
| } |
| } |
| } catch (InterruptedException e) { |
| // ignore |
| } |
| } |
| if (mAborted) { |
| // The user decided not to use this unverified connection |
| // so close it immediately. |
| sslSock.close(); |
| throw new SSLConnectionClosedByUserException("connection closed by the user"); |
| } |
| } |
| } |
| |
| // All went well, we have an open, verified connection. |
| AndroidHttpClientConnection conn = new AndroidHttpClientConnection(); |
| BasicHttpParams params = new BasicHttpParams(); |
| params.setIntParameter(HttpConnectionParams.SOCKET_BUFFER_SIZE, 8192); |
| conn.bind(sslSock, params); |
| |
| return conn; |
| } |
| |
| /** |
| * Closes the low level connection. |
| * |
| * If an exception is thrown then it is assumed that the connection will |
| * have been closed (to the extent possible) anyway and the caller does not |
| * need to take any further action. |
| * |
| */ |
| @Override |
| void closeConnection() { |
| // if the connection has been suspended due to an SSL error |
| if (mSuspended) { |
| // wake up the network thread |
| restartConnection(false); |
| } |
| |
| try { |
| if (mHttpClientConnection != null && mHttpClientConnection.isOpen()) { |
| mHttpClientConnection.close(); |
| } |
| } catch (IOException e) { |
| if (HttpLog.LOGV) |
| HttpLog.v("HttpsConnection.closeConnection():" + |
| " failed closing connection " + mHost); |
| e.printStackTrace(); |
| } |
| } |
| |
| /** |
| * Restart a secure connection suspended waiting for user interaction. |
| */ |
| void restartConnection(boolean proceed) { |
| if (HttpLog.LOGV) { |
| HttpLog.v("HttpsConnection.restartConnection():" + |
| " proceed: " + proceed); |
| } |
| |
| synchronized (mSuspendLock) { |
| if (mSuspended) { |
| mSuspended = false; |
| mAborted = !proceed; |
| mSuspendLock.notify(); |
| } |
| } |
| } |
| |
| @Override |
| String getScheme() { |
| return "https"; |
| } |
| } |
| |
| /** |
| * Simple exception we throw if the SSL connection is closed by the user. |
| * |
| */ |
| class SSLConnectionClosedByUserException extends SSLException { |
| |
| public SSLConnectionClosedByUserException(String reason) { |
| super(reason); |
| } |
| } |