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