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