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