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