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