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.Socket;
     44 import java.security.KeyManagementException;
     45 import java.security.cert.X509Certificate;
     46 import 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  */
     54 public 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  */
    428 class SSLConnectionClosedByUserException extends SSLException {
    429 
    430     public SSLConnectionClosedByUserException(String reason) {
    431         super(reason);
    432     }
    433 }
    434