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