HttpsConnection.java revision 86806ce11a89260147d7c2efa2c192b711d923db
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 org.apache.harmony.xnet.provider.jsse.FileClientSessionCache;
22import org.apache.harmony.xnet.provider.jsse.SSLClientSessionCache;
23import org.apache.harmony.xnet.provider.jsse.SSLContextImpl;
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;
47
48/**
49 * A Connection connecting to a secure http server or tunneling through
50 * a http proxy server to a https server.
51 *
52 * @hide
53 */
54public class HttpsConnection extends Connection {
55
56    /**
57     * SSL socket factory
58     */
59    private static SSLSocketFactory mSslSocketFactory = null;
60
61    static {
62        // This intiialization happens in the zygote. It triggers some
63        // lazy initialization that can will benefit later invocations of
64        // initializeEngine().
65        initializeEngine(null);
66    }
67
68    /**
69     * @hide
70     *
71     * @param sessionDir directory to cache SSL sessions
72     */
73    public static void initializeEngine(File sessionDir) {
74        try {
75            SSLClientSessionCache cache = null;
76            if (sessionDir != null) {
77                Log.d("HttpsConnection", "Caching SSL sessions in "
78                        + sessionDir + ".");
79                cache = FileClientSessionCache.usingDirectory(sessionDir);
80            }
81
82            SSLContextImpl sslContext = new SSLContextImpl();
83
84            // here, trust managers is a single trust-all manager
85            TrustManager[] trustManagers = new TrustManager[] {
86                new X509TrustManager() {
87                    public X509Certificate[] getAcceptedIssuers() {
88                        return null;
89                    }
90
91                    public void checkClientTrusted(
92                        X509Certificate[] certs, String authType) {
93                    }
94
95                    public void checkServerTrusted(
96                        X509Certificate[] certs, String authType) {
97                    }
98                }
99            };
100
101            sslContext.engineInit(null, trustManagers, null, cache, null);
102
103            synchronized (HttpsConnection.class) {
104                mSslSocketFactory = sslContext.engineGetSocketFactory();
105            }
106        } catch (KeyManagementException e) {
107            throw new RuntimeException(e);
108        } catch (IOException e) {
109            throw new RuntimeException(e);
110        }
111    }
112
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     */
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     */
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
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
208                for (Header h : req.mHttpRequest.getAllHeaders()) {
209                    String headerName = h.getName().toLowerCase();
210                    if (headerName.startsWith("proxy") || headerName.equals("keep-alive")) {
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
290                sslSock.setSoTimeout(SOCKET_TIMEOUT);
291                sslSock.connect(new InetSocketAddress(mHost.getHostName(),
292                        mHost.getPort()));
293            } catch(IOException e) {
294                if (sslSock != null) {
295                    sslSock.close();
296                }
297
298                String errorMessage = e.getMessage();
299                if (errorMessage == null) {
300                    errorMessage = "failed to create an SSL socket";
301                }
302
303                throw new IOException(errorMessage);
304            }
305        }
306
307        // do handshake and validate server certificates
308        SslError error = CertificateChainValidator.getInstance().
309            doHandshakeAndValidateServerCertificates(this, sslSock, mHost.getHostName());
310
311        EventHandler eventHandler = req.getEventHandler();
312
313        // Update the certificate info (to be consistent, it is better to do it
314        // here, before we start handling SSL errors, if any)
315        eventHandler.certificate(mCertificate);
316
317        // Inform the user if there is a problem
318        if (error != null) {
319            // handleSslErrorRequest may immediately unsuspend if it wants to
320            // allow the certificate anyway.
321            // So we mark the connection as suspended, call handleSslErrorRequest
322            // then check if we're still suspended and only wait if we actually
323            // need to.
324            synchronized (mSuspendLock) {
325                mSuspended = true;
326            }
327            // don't hold the lock while calling out to the event handler
328            boolean canHandle = eventHandler.handleSslErrorRequest(error);
329            if(!canHandle) {
330                throw new IOException("failed to handle "+ error);
331            }
332            synchronized (mSuspendLock) {
333                if (mSuspended) {
334                    try {
335                        // Put a limit on how long we are waiting; if the timeout
336                        // expires (which should never happen unless you choose
337                        // to ignore the SSL error dialog for a very long time),
338                        // we wake up the thread and abort the request. This is
339                        // to prevent us from stalling the network if things go
340                        // very bad.
341                        mSuspendLock.wait(10 * 60 * 1000);
342                        if (mSuspended) {
343                            // mSuspended is true if we have not had a chance to
344                            // restart the connection yet (ie, the wait timeout
345                            // has expired)
346                            mSuspended = false;
347                            mAborted = true;
348                            if (HttpLog.LOGV) {
349                                HttpLog.v("HttpsConnection.openConnection():" +
350                                          " SSL timeout expired and request was cancelled!!!");
351                            }
352                        }
353                    } catch (InterruptedException e) {
354                        // ignore
355                    }
356                }
357                if (mAborted) {
358                    // The user decided not to use this unverified connection
359                    // so close it immediately.
360                    sslSock.close();
361                    throw new SSLConnectionClosedByUserException("connection closed by the user");
362                }
363            }
364        }
365
366        // All went well, we have an open, verified connection.
367        AndroidHttpClientConnection conn = new AndroidHttpClientConnection();
368        BasicHttpParams params = new BasicHttpParams();
369        params.setIntParameter(HttpConnectionParams.SOCKET_BUFFER_SIZE, 8192);
370        conn.bind(sslSock, params);
371
372        return conn;
373    }
374
375    /**
376     * Closes the low level connection.
377     *
378     * If an exception is thrown then it is assumed that the connection will
379     * have been closed (to the extent possible) anyway and the caller does not
380     * need to take any further action.
381     *
382     */
383    @Override
384    void closeConnection() {
385        // if the connection has been suspended due to an SSL error
386        if (mSuspended) {
387            // wake up the network thread
388            restartConnection(false);
389        }
390
391        try {
392            if (mHttpClientConnection != null && mHttpClientConnection.isOpen()) {
393                mHttpClientConnection.close();
394            }
395        } catch (IOException e) {
396            if (HttpLog.LOGV)
397                HttpLog.v("HttpsConnection.closeConnection():" +
398                          " failed closing connection " + mHost);
399            e.printStackTrace();
400        }
401    }
402
403    /**
404     * Restart a secure connection suspended waiting for user interaction.
405     */
406    void restartConnection(boolean proceed) {
407        if (HttpLog.LOGV) {
408            HttpLog.v("HttpsConnection.restartConnection():" +
409                      " proceed: " + proceed);
410        }
411
412        synchronized (mSuspendLock) {
413            if (mSuspended) {
414                mSuspended = false;
415                mAborted = !proceed;
416                mSuspendLock.notify();
417            }
418        }
419    }
420
421    @Override
422    String getScheme() {
423        return "https";
424    }
425}
426
427/**
428 * Simple exception we throw if the SSL connection is closed by the user.
429 *
430 * {@hide}
431 */
432class SSLConnectionClosedByUserException extends SSLException {
433
434    public SSLConnectionClosedByUserException(String reason) {
435        super(reason);
436    }
437}
438