Home | History | Annotate | Download | only in http
      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.FileClientSessionCache;
     22 import com.android.org.conscrypt.OpenSSLContextImpl;
     23 import com.android.org.conscrypt.SSLClientSessionCache;
     24 import org.apache.http.Header;
     25 import org.apache.http.HttpException;
     26 import org.apache.http.HttpHost;
     27 import org.apache.http.HttpStatus;
     28 import org.apache.http.ParseException;
     29 import org.apache.http.ProtocolVersion;
     30 import org.apache.http.StatusLine;
     31 import org.apache.http.message.BasicHttpRequest;
     32 import org.apache.http.params.BasicHttpParams;
     33 import org.apache.http.params.HttpConnectionParams;
     34 import org.apache.http.params.HttpParams;
     35 
     36 import javax.net.ssl.SSLException;
     37 import javax.net.ssl.SSLSocket;
     38 import javax.net.ssl.SSLSocketFactory;
     39 import javax.net.ssl.TrustManager;
     40 import javax.net.ssl.X509TrustManager;
     41 import java.io.File;
     42 import java.io.IOException;
     43 import java.net.InetSocketAddress;
     44 import java.net.Socket;
     45 import java.security.KeyManagementException;
     46 import java.security.cert.X509Certificate;
     47 import 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  */
     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      * @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  */
    429 class SSLConnectionClosedByUserException extends SSLException {
    430 
    431     public SSLConnectionClosedByUserException(String reason) {
    432         super(reason);
    433     }
    434 }
    435