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 org.apache.harmony.xnet.provider.jsse.FileClientSessionCache;
     22 import org.apache.harmony.xnet.provider.jsse.OpenSSLContextImpl;
     23 import org.apache.harmony.xnet.provider.jsse.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 
     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
    209                 for (Header h : req.mHttpRequest.getAllHeaders()) {
    210                     String headerName = h.getName().toLowerCase();
    211                     if (headerName.startsWith("proxy") || headerName.equals("keep-alive")) {
    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 
    291                 sslSock.setSoTimeout(SOCKET_TIMEOUT);
    292                 sslSock.connect(new InetSocketAddress(mHost.getHostName(),
    293                         mHost.getPort()));
    294             } catch(IOException e) {
    295                 if (sslSock != null) {
    296                     sslSock.close();
    297                 }
    298 
    299                 String errorMessage = e.getMessage();
    300                 if (errorMessage == null) {
    301                     errorMessage = "failed to create an SSL socket";
    302                 }
    303 
    304                 throw new IOException(errorMessage);
    305             }
    306         }
    307 
    308         // do handshake and validate server certificates
    309         SslError error = CertificateChainValidator.getInstance().
    310             doHandshakeAndValidateServerCertificates(this, sslSock, mHost.getHostName());
    311 
    312         // Inform the user if there is a problem
    313         if (error != null) {
    314             // handleSslErrorRequest may immediately unsuspend if it wants to
    315             // allow the certificate anyway.
    316             // So we mark the connection as suspended, call handleSslErrorRequest
    317             // then check if we're still suspended and only wait if we actually
    318             // need to.
    319             synchronized (mSuspendLock) {
    320                 mSuspended = true;
    321             }
    322             // don't hold the lock while calling out to the event handler
    323             boolean canHandle = req.getEventHandler().handleSslErrorRequest(error);
    324             if(!canHandle) {
    325                 throw new IOException("failed to handle "+ error);
    326             }
    327             synchronized (mSuspendLock) {
    328                 if (mSuspended) {
    329                     try {
    330                         // Put a limit on how long we are waiting; if the timeout
    331                         // expires (which should never happen unless you choose
    332                         // to ignore the SSL error dialog for a very long time),
    333                         // we wake up the thread and abort the request. This is
    334                         // to prevent us from stalling the network if things go
    335                         // very bad.
    336                         mSuspendLock.wait(10 * 60 * 1000);
    337                         if (mSuspended) {
    338                             // mSuspended is true if we have not had a chance to
    339                             // restart the connection yet (ie, the wait timeout
    340                             // has expired)
    341                             mSuspended = false;
    342                             mAborted = true;
    343                             if (HttpLog.LOGV) {
    344                                 HttpLog.v("HttpsConnection.openConnection():" +
    345                                           " SSL timeout expired and request was cancelled!!!");
    346                             }
    347                         }
    348                     } catch (InterruptedException e) {
    349                         // ignore
    350                     }
    351                 }
    352                 if (mAborted) {
    353                     // The user decided not to use this unverified connection
    354                     // so close it immediately.
    355                     sslSock.close();
    356                     throw new SSLConnectionClosedByUserException("connection closed by the user");
    357                 }
    358             }
    359         }
    360 
    361         // All went well, we have an open, verified connection.
    362         AndroidHttpClientConnection conn = new AndroidHttpClientConnection();
    363         BasicHttpParams params = new BasicHttpParams();
    364         params.setIntParameter(HttpConnectionParams.SOCKET_BUFFER_SIZE, 8192);
    365         conn.bind(sslSock, params);
    366 
    367         return conn;
    368     }
    369 
    370     /**
    371      * Closes the low level connection.
    372      *
    373      * If an exception is thrown then it is assumed that the connection will
    374      * have been closed (to the extent possible) anyway and the caller does not
    375      * need to take any further action.
    376      *
    377      */
    378     @Override
    379     void closeConnection() {
    380         // if the connection has been suspended due to an SSL error
    381         if (mSuspended) {
    382             // wake up the network thread
    383             restartConnection(false);
    384         }
    385 
    386         try {
    387             if (mHttpClientConnection != null && mHttpClientConnection.isOpen()) {
    388                 mHttpClientConnection.close();
    389             }
    390         } catch (IOException e) {
    391             if (HttpLog.LOGV)
    392                 HttpLog.v("HttpsConnection.closeConnection():" +
    393                           " failed closing connection " + mHost);
    394             e.printStackTrace();
    395         }
    396     }
    397 
    398     /**
    399      * Restart a secure connection suspended waiting for user interaction.
    400      */
    401     void restartConnection(boolean proceed) {
    402         if (HttpLog.LOGV) {
    403             HttpLog.v("HttpsConnection.restartConnection():" +
    404                       " proceed: " + proceed);
    405         }
    406 
    407         synchronized (mSuspendLock) {
    408             if (mSuspended) {
    409                 mSuspended = false;
    410                 mAborted = !proceed;
    411                 mSuspendLock.notify();
    412             }
    413         }
    414     }
    415 
    416     @Override
    417     String getScheme() {
    418         return "https";
    419     }
    420 }
    421 
    422 /**
    423  * Simple exception we throw if the SSL connection is closed by the user.
    424  *
    425  * {@hide}
    426  */
    427 class SSLConnectionClosedByUserException extends SSLException {
    428 
    429     public SSLConnectionClosedByUserException(String reason) {
    430         super(reason);
    431     }
    432 }
    433