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