diff options
Diffstat (limited to 'android/src/android/net/http/HttpsConnection.java')
-rw-r--r-- | android/src/android/net/http/HttpsConnection.java | 433 |
1 files changed, 433 insertions, 0 deletions
diff --git a/android/src/android/net/http/HttpsConnection.java b/android/src/android/net/http/HttpsConnection.java new file mode 100644 index 0000000..a8674de --- /dev/null +++ b/android/src/android/net/http/HttpsConnection.java @@ -0,0 +1,433 @@ +/* + * 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.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. + * + * @hide + */ +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); + } + + /** + * @hide + * + * @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.getPreferred(); + + // 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. + * + * {@hide} + */ +class SSLConnectionClosedByUserException extends SSLException { + + public SSLConnectionClosedByUserException(String reason) { + super(reason); + } +} |