HttpsConnection.java revision cb64d430627b71221c588ef5f23599dd34a89ee9
1/* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.net.http; 18 19import android.content.Context; 20import android.util.Log; 21import com.android.org.conscrypt.FileClientSessionCache; 22import com.android.org.conscrypt.OpenSSLContextImpl; 23import com.android.org.conscrypt.SSLClientSessionCache; 24import org.apache.http.Header; 25import org.apache.http.HttpException; 26import org.apache.http.HttpHost; 27import org.apache.http.HttpStatus; 28import org.apache.http.ParseException; 29import org.apache.http.ProtocolVersion; 30import org.apache.http.StatusLine; 31import org.apache.http.message.BasicHttpRequest; 32import org.apache.http.params.BasicHttpParams; 33import org.apache.http.params.HttpConnectionParams; 34import org.apache.http.params.HttpParams; 35 36import javax.net.ssl.SSLException; 37import javax.net.ssl.SSLSocket; 38import javax.net.ssl.SSLSocketFactory; 39import javax.net.ssl.TrustManager; 40import javax.net.ssl.X509TrustManager; 41import java.io.File; 42import java.io.IOException; 43import java.net.InetSocketAddress; 44import java.net.Socket; 45import java.security.KeyManagementException; 46import java.security.cert.X509Certificate; 47import java.util.Locale; 48 49/** 50 * A Connection connecting to a secure http server or tunneling through 51 * a http proxy server to a https server. 52 * 53 * @hide 54 */ 55public class HttpsConnection extends Connection { 56 57 /** 58 * SSL socket factory 59 */ 60 private static SSLSocketFactory mSslSocketFactory = null; 61 62 static { 63 // This initialization happens in the zygote. It triggers some 64 // lazy initialization that can will benefit later invocations of 65 // initializeEngine(). 66 initializeEngine(null); 67 } 68 69 /** 70 * @hide 71 * 72 * @param sessionDir directory to cache SSL sessions 73 */ 74 public static void initializeEngine(File sessionDir) { 75 try { 76 SSLClientSessionCache cache = null; 77 if (sessionDir != null) { 78 Log.d("HttpsConnection", "Caching SSL sessions in " 79 + sessionDir + "."); 80 cache = FileClientSessionCache.usingDirectory(sessionDir); 81 } 82 83 OpenSSLContextImpl sslContext = new OpenSSLContextImpl(); 84 85 // here, trust managers is a single trust-all manager 86 TrustManager[] trustManagers = new TrustManager[] { 87 new X509TrustManager() { 88 public X509Certificate[] getAcceptedIssuers() { 89 return null; 90 } 91 92 public void checkClientTrusted( 93 X509Certificate[] certs, String authType) { 94 } 95 96 public void checkServerTrusted( 97 X509Certificate[] certs, String authType) { 98 } 99 } 100 }; 101 102 sslContext.engineInit(null, trustManagers, null); 103 sslContext.engineGetClientSessionContext().setPersistentCache(cache); 104 105 synchronized (HttpsConnection.class) { 106 mSslSocketFactory = sslContext.engineGetSocketFactory(); 107 } 108 } catch (KeyManagementException e) { 109 throw new RuntimeException(e); 110 } catch (IOException e) { 111 throw new RuntimeException(e); 112 } 113 } 114 115 private synchronized static SSLSocketFactory getSocketFactory() { 116 return mSslSocketFactory; 117 } 118 119 /** 120 * Object to wait on when suspending the SSL connection 121 */ 122 private Object mSuspendLock = new Object(); 123 124 /** 125 * True if the connection is suspended pending the result of asking the 126 * user about an error. 127 */ 128 private boolean mSuspended = false; 129 130 /** 131 * True if the connection attempt should be aborted due to an ssl 132 * error. 133 */ 134 private boolean mAborted = false; 135 136 // Used when connecting through a proxy. 137 private HttpHost mProxyHost; 138 139 /** 140 * Contructor for a https connection. 141 */ 142 HttpsConnection(Context context, HttpHost host, HttpHost proxy, 143 RequestFeeder requestFeeder) { 144 super(context, host, requestFeeder); 145 mProxyHost = proxy; 146 } 147 148 /** 149 * Sets the server SSL certificate associated with this 150 * connection. 151 * @param certificate The SSL certificate 152 */ 153 /* package */ void setCertificate(SslCertificate certificate) { 154 mCertificate = certificate; 155 } 156 157 /** 158 * Opens the connection to a http server or proxy. 159 * 160 * @return the opened low level connection 161 * @throws IOException if the connection fails for any reason. 162 */ 163 @Override 164 AndroidHttpClientConnection openConnection(Request req) throws IOException { 165 SSLSocket sslSock = null; 166 167 if (mProxyHost != null) { 168 // If we have a proxy set, we first send a CONNECT request 169 // to the proxy; if the proxy returns 200 OK, we negotiate 170 // a secure connection to the target server via the proxy. 171 // If the request fails, we drop it, but provide the event 172 // handler with the response status and headers. The event 173 // handler is then responsible for cancelling the load or 174 // issueing a new request. 175 AndroidHttpClientConnection proxyConnection = null; 176 Socket proxySock = null; 177 try { 178 proxySock = new Socket 179 (mProxyHost.getHostName(), mProxyHost.getPort()); 180 181 proxySock.setSoTimeout(60 * 1000); 182 183 proxyConnection = new AndroidHttpClientConnection(); 184 HttpParams params = new BasicHttpParams(); 185 HttpConnectionParams.setSocketBufferSize(params, 8192); 186 187 proxyConnection.bind(proxySock, params); 188 } catch(IOException e) { 189 if (proxyConnection != null) { 190 proxyConnection.close(); 191 } 192 193 String errorMessage = e.getMessage(); 194 if (errorMessage == null) { 195 errorMessage = 196 "failed to establish a connection to the proxy"; 197 } 198 199 throw new IOException(errorMessage); 200 } 201 202 StatusLine statusLine = null; 203 int statusCode = 0; 204 Headers headers = new Headers(); 205 try { 206 BasicHttpRequest proxyReq = new BasicHttpRequest 207 ("CONNECT", mHost.toHostString()); 208 209 // add all 'proxy' headers from the original request, we also need 210 // to add 'host' header unless we want proxy to answer us with a 211 // 400 Bad Request 212 for (Header h : req.mHttpRequest.getAllHeaders()) { 213 String headerName = h.getName().toLowerCase(Locale.ROOT); 214 if (headerName.startsWith("proxy") || headerName.equals("keep-alive") 215 || headerName.equals("host")) { 216 proxyReq.addHeader(h); 217 } 218 } 219 220 proxyConnection.sendRequestHeader(proxyReq); 221 proxyConnection.flush(); 222 223 // it is possible to receive informational status 224 // codes prior to receiving actual headers; 225 // all those status codes are smaller than OK 200 226 // a loop is a standard way of dealing with them 227 do { 228 statusLine = proxyConnection.parseResponseHeader(headers); 229 statusCode = statusLine.getStatusCode(); 230 } while (statusCode < HttpStatus.SC_OK); 231 } catch (ParseException e) { 232 String errorMessage = e.getMessage(); 233 if (errorMessage == null) { 234 errorMessage = 235 "failed to send a CONNECT request"; 236 } 237 238 throw new IOException(errorMessage); 239 } catch (HttpException e) { 240 String errorMessage = e.getMessage(); 241 if (errorMessage == null) { 242 errorMessage = 243 "failed to send a CONNECT request"; 244 } 245 246 throw new IOException(errorMessage); 247 } catch (IOException e) { 248 String errorMessage = e.getMessage(); 249 if (errorMessage == null) { 250 errorMessage = 251 "failed to send a CONNECT request"; 252 } 253 254 throw new IOException(errorMessage); 255 } 256 257 if (statusCode == HttpStatus.SC_OK) { 258 try { 259 sslSock = (SSLSocket) getSocketFactory().createSocket( 260 proxySock, mHost.getHostName(), mHost.getPort(), true); 261 } catch(IOException e) { 262 if (sslSock != null) { 263 sslSock.close(); 264 } 265 266 String errorMessage = e.getMessage(); 267 if (errorMessage == null) { 268 errorMessage = 269 "failed to create an SSL socket"; 270 } 271 throw new IOException(errorMessage); 272 } 273 } else { 274 // if the code is not OK, inform the event handler 275 ProtocolVersion version = statusLine.getProtocolVersion(); 276 277 req.mEventHandler.status(version.getMajor(), 278 version.getMinor(), 279 statusCode, 280 statusLine.getReasonPhrase()); 281 req.mEventHandler.headers(headers); 282 req.mEventHandler.endData(); 283 284 proxyConnection.close(); 285 286 // here, we return null to indicate that the original 287 // request needs to be dropped 288 return null; 289 } 290 } else { 291 // if we do not have a proxy, we simply connect to the host 292 try { 293 sslSock = (SSLSocket) getSocketFactory().createSocket( 294 mHost.getHostName(), mHost.getPort()); 295 sslSock.setSoTimeout(SOCKET_TIMEOUT); 296 } catch(IOException e) { 297 if (sslSock != null) { 298 sslSock.close(); 299 } 300 301 String errorMessage = e.getMessage(); 302 if (errorMessage == null) { 303 errorMessage = "failed to create an SSL socket"; 304 } 305 306 throw new IOException(errorMessage); 307 } 308 } 309 310 // do handshake and validate server certificates 311 SslError error = CertificateChainValidator.getInstance(). 312 doHandshakeAndValidateServerCertificates(this, sslSock, mHost.getHostName()); 313 314 // Inform the user if there is a problem 315 if (error != null) { 316 // handleSslErrorRequest may immediately unsuspend if it wants to 317 // allow the certificate anyway. 318 // So we mark the connection as suspended, call handleSslErrorRequest 319 // then check if we're still suspended and only wait if we actually 320 // need to. 321 synchronized (mSuspendLock) { 322 mSuspended = true; 323 } 324 // don't hold the lock while calling out to the event handler 325 boolean canHandle = req.getEventHandler().handleSslErrorRequest(error); 326 if(!canHandle) { 327 throw new IOException("failed to handle "+ error); 328 } 329 synchronized (mSuspendLock) { 330 if (mSuspended) { 331 try { 332 // Put a limit on how long we are waiting; if the timeout 333 // expires (which should never happen unless you choose 334 // to ignore the SSL error dialog for a very long time), 335 // we wake up the thread and abort the request. This is 336 // to prevent us from stalling the network if things go 337 // very bad. 338 mSuspendLock.wait(10 * 60 * 1000); 339 if (mSuspended) { 340 // mSuspended is true if we have not had a chance to 341 // restart the connection yet (ie, the wait timeout 342 // has expired) 343 mSuspended = false; 344 mAborted = true; 345 if (HttpLog.LOGV) { 346 HttpLog.v("HttpsConnection.openConnection():" + 347 " SSL timeout expired and request was cancelled!!!"); 348 } 349 } 350 } catch (InterruptedException e) { 351 // ignore 352 } 353 } 354 if (mAborted) { 355 // The user decided not to use this unverified connection 356 // so close it immediately. 357 sslSock.close(); 358 throw new SSLConnectionClosedByUserException("connection closed by the user"); 359 } 360 } 361 } 362 363 // All went well, we have an open, verified connection. 364 AndroidHttpClientConnection conn = new AndroidHttpClientConnection(); 365 BasicHttpParams params = new BasicHttpParams(); 366 params.setIntParameter(HttpConnectionParams.SOCKET_BUFFER_SIZE, 8192); 367 conn.bind(sslSock, params); 368 369 return conn; 370 } 371 372 /** 373 * Closes the low level connection. 374 * 375 * If an exception is thrown then it is assumed that the connection will 376 * have been closed (to the extent possible) anyway and the caller does not 377 * need to take any further action. 378 * 379 */ 380 @Override 381 void closeConnection() { 382 // if the connection has been suspended due to an SSL error 383 if (mSuspended) { 384 // wake up the network thread 385 restartConnection(false); 386 } 387 388 try { 389 if (mHttpClientConnection != null && mHttpClientConnection.isOpen()) { 390 mHttpClientConnection.close(); 391 } 392 } catch (IOException e) { 393 if (HttpLog.LOGV) 394 HttpLog.v("HttpsConnection.closeConnection():" + 395 " failed closing connection " + mHost); 396 e.printStackTrace(); 397 } 398 } 399 400 /** 401 * Restart a secure connection suspended waiting for user interaction. 402 */ 403 void restartConnection(boolean proceed) { 404 if (HttpLog.LOGV) { 405 HttpLog.v("HttpsConnection.restartConnection():" + 406 " proceed: " + proceed); 407 } 408 409 synchronized (mSuspendLock) { 410 if (mSuspended) { 411 mSuspended = false; 412 mAborted = !proceed; 413 mSuspendLock.notify(); 414 } 415 } 416 } 417 418 @Override 419 String getScheme() { 420 return "https"; 421 } 422} 423 424/** 425 * Simple exception we throw if the SSL connection is closed by the user. 426 * 427 * {@hide} 428 */ 429class SSLConnectionClosedByUserException extends SSLException { 430 431 public SSLConnectionClosedByUserException(String reason) { 432 super(reason); 433 } 434} 435