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 */
52public class HttpsConnection extends Connection {
53
54    /**
55     * SSL socket factory
56     */
57    private static SSLSocketFactory mSslSocketFactory = null;
58
59    static {
60        // This initialization happens in the zygote. It triggers some
61        // lazy initialization that can will benefit later invocations of
62        // initializeEngine().
63        initializeEngine(null);
64    }
65
66    /**
67     * @param sessionDir directory to cache SSL sessions
68     */
69    public static void initializeEngine(File sessionDir) {
70        try {
71            SSLClientSessionCache cache = null;
72            if (sessionDir != null) {
73                Log.d("HttpsConnection", "Caching SSL sessions in "
74                        + sessionDir + ".");
75                cache = FileClientSessionCache.usingDirectory(sessionDir);
76            }
77
78            OpenSSLContextImpl sslContext = OpenSSLContextImpl.getPreferred();
79
80            // here, trust managers is a single trust-all manager
81            TrustManager[] trustManagers = new TrustManager[] {
82                new X509TrustManager() {
83                    public X509Certificate[] getAcceptedIssuers() {
84                        return null;
85                    }
86
87                    public void checkClientTrusted(
88                        X509Certificate[] certs, String authType) {
89                    }
90
91                    public void checkServerTrusted(
92                        X509Certificate[] certs, String authType) {
93                    }
94                }
95            };
96
97            sslContext.engineInit(null, trustManagers, null);
98            sslContext.engineGetClientSessionContext().setPersistentCache(cache);
99
100            synchronized (HttpsConnection.class) {
101                mSslSocketFactory = sslContext.engineGetSocketFactory();
102            }
103        } catch (KeyManagementException e) {
104            throw new RuntimeException(e);
105        } catch (IOException e) {
106            throw new RuntimeException(e);
107        }
108    }
109
110    private synchronized static SSLSocketFactory getSocketFactory() {
111        return mSslSocketFactory;
112    }
113
114    /**
115     * Object to wait on when suspending the SSL connection
116     */
117    private Object mSuspendLock = new Object();
118
119    /**
120     * True if the connection is suspended pending the result of asking the
121     * user about an error.
122     */
123    private boolean mSuspended = false;
124
125    /**
126     * True if the connection attempt should be aborted due to an ssl
127     * error.
128     */
129    private boolean mAborted = false;
130
131    // Used when connecting through a proxy.
132    private HttpHost mProxyHost;
133
134    /**
135     * Contructor for a https connection.
136     */
137    HttpsConnection(Context context, HttpHost host, HttpHost proxy,
138                    RequestFeeder requestFeeder) {
139        super(context, host, requestFeeder);
140        mProxyHost = proxy;
141    }
142
143    /**
144     * Sets the server SSL certificate associated with this
145     * connection.
146     * @param certificate The SSL certificate
147     */
148    /* package */ void setCertificate(SslCertificate certificate) {
149        mCertificate = certificate;
150    }
151
152    /**
153     * Opens the connection to a http server or proxy.
154     *
155     * @return the opened low level connection
156     * @throws IOException if the connection fails for any reason.
157     */
158    @Override
159    AndroidHttpClientConnection openConnection(Request req) throws IOException {
160        SSLSocket sslSock = null;
161
162        if (mProxyHost != null) {
163            // If we have a proxy set, we first send a CONNECT request
164            // to the proxy; if the proxy returns 200 OK, we negotiate
165            // a secure connection to the target server via the proxy.
166            // If the request fails, we drop it, but provide the event
167            // handler with the response status and headers. The event
168            // handler is then responsible for cancelling the load or
169            // issueing a new request.
170            AndroidHttpClientConnection proxyConnection = null;
171            Socket proxySock = null;
172            try {
173                proxySock = new Socket
174                    (mProxyHost.getHostName(), mProxyHost.getPort());
175
176                proxySock.setSoTimeout(60 * 1000);
177
178                proxyConnection = new AndroidHttpClientConnection();
179                HttpParams params = new BasicHttpParams();
180                HttpConnectionParams.setSocketBufferSize(params, 8192);
181
182                proxyConnection.bind(proxySock, params);
183            } catch(IOException e) {
184                if (proxyConnection != null) {
185                    proxyConnection.close();
186                }
187
188                String errorMessage = e.getMessage();
189                if (errorMessage == null) {
190                    errorMessage =
191                        "failed to establish a connection to the proxy";
192                }
193
194                throw new IOException(errorMessage);
195            }
196
197            StatusLine statusLine = null;
198            int statusCode = 0;
199            Headers headers = new Headers();
200            try {
201                BasicHttpRequest proxyReq = new BasicHttpRequest
202                    ("CONNECT", mHost.toHostString());
203
204                // add all 'proxy' headers from the original request, we also need
205                // to add 'host' header unless we want proxy to answer us with a
206                // 400 Bad Request
207                for (Header h : req.mHttpRequest.getAllHeaders()) {
208                    String headerName = h.getName().toLowerCase(Locale.ROOT);
209                    if (headerName.startsWith("proxy") || headerName.equals("keep-alive")
210                            || headerName.equals("host")) {
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                        mHost.getHostName(), mHost.getPort());
290                sslSock.setSoTimeout(SOCKET_TIMEOUT);
291            } catch(IOException e) {
292                if (sslSock != null) {
293                    sslSock.close();
294                }
295
296                String errorMessage = e.getMessage();
297                if (errorMessage == null) {
298                    errorMessage = "failed to create an SSL socket";
299                }
300
301                throw new IOException(errorMessage);
302            }
303        }
304
305        // do handshake and validate server certificates
306        SslError error = CertificateChainValidator.getInstance().
307            doHandshakeAndValidateServerCertificates(this, sslSock, mHost.getHostName());
308
309        // Inform the user if there is a problem
310        if (error != null) {
311            // handleSslErrorRequest may immediately unsuspend if it wants to
312            // allow the certificate anyway.
313            // So we mark the connection as suspended, call handleSslErrorRequest
314            // then check if we're still suspended and only wait if we actually
315            // need to.
316            synchronized (mSuspendLock) {
317                mSuspended = true;
318            }
319            // don't hold the lock while calling out to the event handler
320            boolean canHandle = req.getEventHandler().handleSslErrorRequest(error);
321            if(!canHandle) {
322                throw new IOException("failed to handle "+ error);
323            }
324            synchronized (mSuspendLock) {
325                if (mSuspended) {
326                    try {
327                        // Put a limit on how long we are waiting; if the timeout
328                        // expires (which should never happen unless you choose
329                        // to ignore the SSL error dialog for a very long time),
330                        // we wake up the thread and abort the request. This is
331                        // to prevent us from stalling the network if things go
332                        // very bad.
333                        mSuspendLock.wait(10 * 60 * 1000);
334                        if (mSuspended) {
335                            // mSuspended is true if we have not had a chance to
336                            // restart the connection yet (ie, the wait timeout
337                            // has expired)
338                            mSuspended = false;
339                            mAborted = true;
340                            if (HttpLog.LOGV) {
341                                HttpLog.v("HttpsConnection.openConnection():" +
342                                          " SSL timeout expired and request was cancelled!!!");
343                            }
344                        }
345                    } catch (InterruptedException e) {
346                        // ignore
347                    }
348                }
349                if (mAborted) {
350                    // The user decided not to use this unverified connection
351                    // so close it immediately.
352                    sslSock.close();
353                    throw new SSLConnectionClosedByUserException("connection closed by the user");
354                }
355            }
356        }
357
358        // All went well, we have an open, verified connection.
359        AndroidHttpClientConnection conn = new AndroidHttpClientConnection();
360        BasicHttpParams params = new BasicHttpParams();
361        params.setIntParameter(HttpConnectionParams.SOCKET_BUFFER_SIZE, 8192);
362        conn.bind(sslSock, params);
363
364        return conn;
365    }
366
367    /**
368     * Closes the low level connection.
369     *
370     * If an exception is thrown then it is assumed that the connection will
371     * have been closed (to the extent possible) anyway and the caller does not
372     * need to take any further action.
373     *
374     */
375    @Override
376    void closeConnection() {
377        // if the connection has been suspended due to an SSL error
378        if (mSuspended) {
379            // wake up the network thread
380            restartConnection(false);
381        }
382
383        try {
384            if (mHttpClientConnection != null && mHttpClientConnection.isOpen()) {
385                mHttpClientConnection.close();
386            }
387        } catch (IOException e) {
388            if (HttpLog.LOGV)
389                HttpLog.v("HttpsConnection.closeConnection():" +
390                          " failed closing connection " + mHost);
391            e.printStackTrace();
392        }
393    }
394
395    /**
396     * Restart a secure connection suspended waiting for user interaction.
397     */
398    void restartConnection(boolean proceed) {
399        if (HttpLog.LOGV) {
400            HttpLog.v("HttpsConnection.restartConnection():" +
401                      " proceed: " + proceed);
402        }
403
404        synchronized (mSuspendLock) {
405            if (mSuspended) {
406                mSuspended = false;
407                mAborted = !proceed;
408                mSuspendLock.notify();
409            }
410        }
411    }
412
413    @Override
414    String getScheme() {
415        return "https";
416    }
417}
418
419/**
420 * Simple exception we throw if the SSL connection is closed by the user.
421 *
422 */
423class SSLConnectionClosedByUserException extends SSLException {
424
425    public SSLConnectionClosedByUserException(String reason) {
426        super(reason);
427    }
428}
429