blob: 213537b1f1733d3cfbf47f8dd437c0e06e262b84 [file] [log] [blame]
/*
* 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.
*/
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.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.
*
*/
class SSLConnectionClosedByUserException extends SSLException {
public SSLConnectionClosedByUserException(String reason) {
super(reason);
}
}