• Home
  • History
  • Annotate
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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