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