Home | History | Annotate | Download | only in transport
      1 /*
      2  * Copyright (C) 2008 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 com.android.email.mail.transport;
     18 
     19 import android.content.Context;
     20 
     21 import com.android.email.DebugUtils;
     22 import com.android.emailcommon.Logging;
     23 import com.android.emailcommon.mail.CertificateValidationException;
     24 import com.android.emailcommon.mail.MessagingException;
     25 import com.android.emailcommon.provider.HostAuth;
     26 import com.android.emailcommon.utility.SSLUtils;
     27 import com.android.mail.analytics.Analytics;
     28 import com.android.mail.utils.LogUtils;
     29 
     30 import java.io.BufferedInputStream;
     31 import java.io.BufferedOutputStream;
     32 import java.io.IOException;
     33 import java.io.InputStream;
     34 import java.io.OutputStream;
     35 import java.net.InetAddress;
     36 import java.net.InetSocketAddress;
     37 import java.net.Socket;
     38 import java.net.SocketAddress;
     39 import java.net.SocketException;
     40 
     41 import javax.net.ssl.HostnameVerifier;
     42 import javax.net.ssl.HttpsURLConnection;
     43 import javax.net.ssl.SSLException;
     44 import javax.net.ssl.SSLPeerUnverifiedException;
     45 import javax.net.ssl.SSLSession;
     46 import javax.net.ssl.SSLSocket;
     47 
     48 public class MailTransport {
     49 
     50     // TODO protected eventually
     51     /*protected*/ public static final int SOCKET_CONNECT_TIMEOUT = 10000;
     52     /*protected*/ public static final int SOCKET_READ_TIMEOUT = 60000;
     53 
     54     private static final HostnameVerifier HOSTNAME_VERIFIER =
     55             HttpsURLConnection.getDefaultHostnameVerifier();
     56 
     57     private final String mDebugLabel;
     58     private final Context mContext;
     59     protected final HostAuth mHostAuth;
     60 
     61     private Socket mSocket;
     62     private InputStream mIn;
     63     private OutputStream mOut;
     64 
     65     public MailTransport(Context context, String debugLabel, HostAuth hostAuth) {
     66         super();
     67         mContext = context;
     68         mDebugLabel = debugLabel;
     69         mHostAuth = hostAuth;
     70     }
     71 
     72    /**
     73      * Returns a new transport, using the current transport as a model. The new transport is
     74      * configured identically (as if {@link #setSecurity(int, boolean)}, {@link #setPort(int)}
     75      * and {@link #setHost(String)} were invoked), but not opened or connected in any way.
     76      */
     77     @Override
     78     public MailTransport clone() {
     79         return new MailTransport(mContext, mDebugLabel, mHostAuth);
     80     }
     81 
     82     public String getHost() {
     83         return mHostAuth.mAddress;
     84     }
     85 
     86     public int getPort() {
     87         return mHostAuth.mPort;
     88     }
     89 
     90     public boolean canTrySslSecurity() {
     91         return (mHostAuth.mFlags & HostAuth.FLAG_SSL) != 0;
     92     }
     93 
     94     public boolean canTryTlsSecurity() {
     95         return (mHostAuth.mFlags & HostAuth.FLAG_TLS) != 0;
     96     }
     97 
     98     public boolean canTrustAllCertificates() {
     99         return (mHostAuth.mFlags & HostAuth.FLAG_TRUST_ALL) != 0;
    100     }
    101 
    102     /**
    103      * Attempts to open a connection using the Uri supplied for connection parameters.  Will attempt
    104      * an SSL connection if indicated.
    105      */
    106     public void open() throws MessagingException, CertificateValidationException {
    107         if (DebugUtils.DEBUG) {
    108             LogUtils.d(Logging.LOG_TAG, "*** " + mDebugLabel + " open " +
    109                     getHost() + ":" + String.valueOf(getPort()));
    110         }
    111 
    112         try {
    113             SocketAddress socketAddress = new InetSocketAddress(getHost(), getPort());
    114             if (canTrySslSecurity()) {
    115                 mSocket = SSLUtils.getSSLSocketFactory(
    116                         mContext, mHostAuth, null, canTrustAllCertificates()).createSocket();
    117             } else {
    118                 mSocket = new Socket();
    119             }
    120             mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT);
    121             // After the socket connects to an SSL server, confirm that the hostname is as expected
    122             if (canTrySslSecurity() && !canTrustAllCertificates()) {
    123                 verifyHostname(mSocket, getHost());
    124             }
    125             Analytics.getInstance().sendEvent("socket_certificates",
    126                     "open", Boolean.toString(canTrustAllCertificates()), 0);
    127             if (mSocket instanceof SSLSocket) {
    128                 final SSLSocket sslSocket = (SSLSocket) mSocket;
    129                 if (sslSocket.getSession() != null) {
    130                     Analytics.getInstance().sendEvent("cipher_suite",
    131                             sslSocket.getSession().getProtocol(),
    132                             sslSocket.getSession().getCipherSuite(), 0);
    133                 }
    134             }
    135             mIn = new BufferedInputStream(mSocket.getInputStream(), 1024);
    136             mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
    137             mSocket.setSoTimeout(SOCKET_READ_TIMEOUT);
    138         } catch (SSLException e) {
    139             if (DebugUtils.DEBUG) {
    140                 LogUtils.d(Logging.LOG_TAG, e.toString());
    141             }
    142             throw new CertificateValidationException(e.getMessage(), e);
    143         } catch (IOException ioe) {
    144             if (DebugUtils.DEBUG) {
    145                 LogUtils.d(Logging.LOG_TAG, ioe.toString());
    146             }
    147             throw new MessagingException(MessagingException.IOERROR, ioe.toString());
    148         } catch (IllegalArgumentException iae) {
    149             if (DebugUtils.DEBUG) {
    150                 LogUtils.d(Logging.LOG_TAG, iae.toString());
    151             }
    152             throw new MessagingException(MessagingException.UNSPECIFIED_EXCEPTION, iae.toString());
    153         }
    154     }
    155 
    156     /**
    157      * Attempts to reopen a TLS connection using the Uri supplied for connection parameters.
    158      *
    159      * NOTE: No explicit hostname verification is required here, because it's handled automatically
    160      * by the call to createSocket().
    161      *
    162      * TODO should we explicitly close the old socket?  This seems funky to abandon it.
    163      */
    164     public void reopenTls() throws MessagingException {
    165         try {
    166             mSocket = SSLUtils.getSSLSocketFactory(mContext, mHostAuth, null,
    167                     canTrustAllCertificates())
    168                     .createSocket(mSocket, getHost(), getPort(), true);
    169             mSocket.setSoTimeout(SOCKET_READ_TIMEOUT);
    170             mIn = new BufferedInputStream(mSocket.getInputStream(), 1024);
    171             mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
    172 
    173             Analytics.getInstance().sendEvent("socket_certificates",
    174                     "reopenTls", Boolean.toString(canTrustAllCertificates()), 0);
    175             final SSLSocket sslSocket = (SSLSocket) mSocket;
    176             if (sslSocket.getSession() != null) {
    177                 Analytics.getInstance().sendEvent("cipher_suite",
    178                         sslSocket.getSession().getProtocol(),
    179                         sslSocket.getSession().getCipherSuite(), 0);
    180             }
    181         } catch (SSLException e) {
    182             if (DebugUtils.DEBUG) {
    183                 LogUtils.d(Logging.LOG_TAG, e.toString());
    184             }
    185             throw new CertificateValidationException(e.getMessage(), e);
    186         } catch (IOException ioe) {
    187             if (DebugUtils.DEBUG) {
    188                 LogUtils.d(Logging.LOG_TAG, ioe.toString());
    189             }
    190             throw new MessagingException(MessagingException.IOERROR, ioe.toString());
    191         }
    192     }
    193 
    194     /**
    195      * Lightweight version of SSLCertificateSocketFactory.verifyHostname, which provides this
    196      * service but is not in the public API.
    197      *
    198      * Verify the hostname of the certificate used by the other end of a
    199      * connected socket.  You MUST call this if you did not supply a hostname
    200      * to SSLCertificateSocketFactory.createSocket().  It is harmless to call this method
    201      * redundantly if the hostname has already been verified.
    202      *
    203      * <p>Wildcard certificates are allowed to verify any matching hostname,
    204      * so "foo.bar.example.com" is verified if the peer has a certificate
    205      * for "*.example.com".
    206      *
    207      * @param socket An SSL socket which has been connected to a server
    208      * @param hostname The expected hostname of the remote server
    209      * @throws IOException if something goes wrong handshaking with the server
    210      * @throws SSLPeerUnverifiedException if the server cannot prove its identity
    211       */
    212     private static void verifyHostname(Socket socket, String hostname) throws IOException {
    213         // The code at the start of OpenSSLSocketImpl.startHandshake()
    214         // ensures that the call is idempotent, so we can safely call it.
    215         SSLSocket ssl = (SSLSocket) socket;
    216         ssl.startHandshake();
    217 
    218         SSLSession session = ssl.getSession();
    219         if (session == null) {
    220             throw new SSLException("Cannot verify SSL socket without session");
    221         }
    222         // TODO: Instead of reporting the name of the server we think we're connecting to,
    223         // we should be reporting the bad name in the certificate.  Unfortunately this is buried
    224         // in the verifier code and is not available in the verifier API, and extracting the
    225         // CN & alts is beyond the scope of this patch.
    226         if (!HOSTNAME_VERIFIER.verify(hostname, session)) {
    227             throw new SSLPeerUnverifiedException(
    228                     "Certificate hostname not useable for server: " + hostname);
    229         }
    230     }
    231 
    232     /**
    233      * Get the socket timeout.
    234      * @return the read timeout value in milliseconds
    235      * @throws SocketException
    236      */
    237     public int getSoTimeout() throws SocketException {
    238         return mSocket.getSoTimeout();
    239     }
    240 
    241     /**
    242      * Set the socket timeout.
    243      * @param timeoutMilliseconds the read timeout value if greater than {@code 0}, or
    244      *            {@code 0} for an infinite timeout.
    245      */
    246     public void setSoTimeout(int timeoutMilliseconds) throws SocketException {
    247         mSocket.setSoTimeout(timeoutMilliseconds);
    248     }
    249 
    250     public boolean isOpen() {
    251         return (mIn != null && mOut != null &&
    252                 mSocket != null && mSocket.isConnected() && !mSocket.isClosed());
    253     }
    254 
    255     /**
    256      * Close the connection.  MUST NOT return any exceptions - must be "best effort" and safe.
    257      */
    258     public void close() {
    259         try {
    260             mIn.close();
    261         } catch (Exception e) {
    262             // May fail if the connection is already closed.
    263         }
    264         try {
    265             mOut.close();
    266         } catch (Exception e) {
    267             // May fail if the connection is already closed.
    268         }
    269         try {
    270             mSocket.close();
    271         } catch (Exception e) {
    272             // May fail if the connection is already closed.
    273         }
    274         mIn = null;
    275         mOut = null;
    276         mSocket = null;
    277     }
    278 
    279     public InputStream getInputStream() {
    280         return mIn;
    281     }
    282 
    283     public OutputStream getOutputStream() {
    284         return mOut;
    285     }
    286 
    287     /**
    288      * Writes a single line to the server using \r\n termination.
    289      */
    290     public void writeLine(String s, String sensitiveReplacement) throws IOException {
    291         if (DebugUtils.DEBUG) {
    292             if (sensitiveReplacement != null && !Logging.DEBUG_SENSITIVE) {
    293                 LogUtils.d(Logging.LOG_TAG, ">>> " + sensitiveReplacement);
    294             } else {
    295                 LogUtils.d(Logging.LOG_TAG, ">>> " + s);
    296             }
    297         }
    298 
    299         OutputStream out = getOutputStream();
    300         out.write(s.getBytes());
    301         out.write('\r');
    302         out.write('\n');
    303         out.flush();
    304     }
    305 
    306     /**
    307      * Reads a single line from the server, using either \r\n or \n as the delimiter.  The
    308      * delimiter char(s) are not included in the result.
    309      */
    310     public String readLine(boolean loggable) throws IOException {
    311         StringBuffer sb = new StringBuffer();
    312         InputStream in = getInputStream();
    313         int d;
    314         while ((d = in.read()) != -1) {
    315             if (((char)d) == '\r') {
    316                 continue;
    317             } else if (((char)d) == '\n') {
    318                 break;
    319             } else {
    320                 sb.append((char)d);
    321             }
    322         }
    323         if (d == -1 && DebugUtils.DEBUG) {
    324             LogUtils.d(Logging.LOG_TAG, "End of stream reached while trying to read line.");
    325         }
    326         String ret = sb.toString();
    327         if (loggable && DebugUtils.DEBUG) {
    328             LogUtils.d(Logging.LOG_TAG, "<<< " + ret);
    329         }
    330         return ret;
    331     }
    332 
    333     public InetAddress getLocalAddress() {
    334         if (isOpen()) {
    335             return mSocket.getLocalAddress();
    336         } else {
    337             return null;
    338         }
    339     }
    340 }
    341