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