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 public 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  */
    423 class SSLConnectionClosedByUserException extends SSLException {
    424 
    425     public SSLConnectionClosedByUserException(String reason) {
    426         super(reason);
    427     }
    428 }
    429