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(
    171                         canTrustAllCertificates(), SOCKET_CONNECT_TIMEOUT).createSocket();
    172             } else {
    173                 mSocket = new Socket();
    174             }
    175             mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT);
    176             // After the socket connects to an SSL server, confirm that the hostname is as expected
    177             if (canTrySslSecurity() && !canTrustAllCertificates()) {
    178                 verifyHostname(mSocket, getHost());
    179             }
    180             mIn = new BufferedInputStream(mSocket.getInputStream(), 1024);
    181             mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
    182 
    183         } catch (SSLException e) {
    184             if (Email.DEBUG) {
    185                 Log.d(Logging.LOG_TAG, e.toString());
    186             }
    187             throw new CertificateValidationException(e.getMessage(), e);
    188         } catch (IOException ioe) {
    189             if (Email.DEBUG) {
    190                 Log.d(Logging.LOG_TAG, ioe.toString());
    191             }
    192             throw new MessagingException(MessagingException.IOERROR, ioe.toString());
    193         }
    194     }
    195 
    196     /**
    197      * Attempts to reopen a TLS connection using the Uri supplied for connection parameters.
    198      *
    199      * NOTE: No explicit hostname verification is required here, because it's handled automatically
    200      * by the call to createSocket().
    201      *
    202      * TODO should we explicitly close the old socket?  This seems funky to abandon it.
    203      */
    204     @Override
    205     public void reopenTls() throws MessagingException {
    206         try {
    207             mSocket =
    208                     SSLUtils.getSSLSocketFactory(canTrustAllCertificates(), SOCKET_CONNECT_TIMEOUT)
    209                             .createSocket(mSocket, getHost(), getPort(), true);
    210             mSocket.setSoTimeout(SOCKET_READ_TIMEOUT);
    211             mIn = new BufferedInputStream(mSocket.getInputStream(), 1024);
    212             mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
    213 
    214         } catch (SSLException e) {
    215             if (Email.DEBUG) {
    216                 Log.d(Logging.LOG_TAG, e.toString());
    217             }
    218             throw new CertificateValidationException(e.getMessage(), e);
    219         } catch (IOException ioe) {
    220             if (Email.DEBUG) {
    221                 Log.d(Logging.LOG_TAG, ioe.toString());
    222             }
    223             throw new MessagingException(MessagingException.IOERROR, ioe.toString());
    224         }
    225     }
    226 
    227     /**
    228      * Lightweight version of SSLCertificateSocketFactory.verifyHostname, which provides this
    229      * service but is not in the public API.
    230      *
    231      * Verify the hostname of the certificate used by the other end of a
    232      * connected socket.  You MUST call this if you did not supply a hostname
    233      * to SSLCertificateSocketFactory.createSocket().  It is harmless to call this method
    234      * redundantly if the hostname has already been verified.
    235      *
    236      * <p>Wildcard certificates are allowed to verify any matching hostname,
    237      * so "foo.bar.example.com" is verified if the peer has a certificate
    238      * for "*.example.com".
    239      *
    240      * @param socket An SSL socket which has been connected to a server
    241      * @param hostname The expected hostname of the remote server
    242      * @throws IOException if something goes wrong handshaking with the server
    243      * @throws SSLPeerUnverifiedException if the server cannot prove its identity
    244       */
    245     private void verifyHostname(Socket socket, String hostname) throws IOException {
    246         // The code at the start of OpenSSLSocketImpl.startHandshake()
    247         // ensures that the call is idempotent, so we can safely call it.
    248         SSLSocket ssl = (SSLSocket) socket;
    249         ssl.startHandshake();
    250 
    251         SSLSession session = ssl.getSession();
    252         if (session == null) {
    253             throw new SSLException("Cannot verify SSL socket without session");
    254         }
    255         // TODO: Instead of reporting the name of the server we think we're connecting to,
    256         // we should be reporting the bad name in the certificate.  Unfortunately this is buried
    257         // in the verifier code and is not available in the verifier API, and extracting the
    258         // CN & alts is beyond the scope of this patch.
    259         if (!HOSTNAME_VERIFIER.verify(hostname, session)) {
    260             throw new SSLPeerUnverifiedException(
    261                     "Certificate hostname not useable for server: " + hostname);
    262         }
    263     }
    264 
    265     /**
    266      * Set the socket timeout.
    267      * @param timeoutMilliseconds the read timeout value if greater than {@code 0}, or
    268      *            {@code 0} for an infinite timeout.
    269      */
    270     @Override
    271     public void setSoTimeout(int timeoutMilliseconds) throws SocketException {
    272         mSocket.setSoTimeout(timeoutMilliseconds);
    273     }
    274 
    275     @Override
    276     public boolean isOpen() {
    277         return (mIn != null && mOut != null &&
    278                 mSocket != null && mSocket.isConnected() && !mSocket.isClosed());
    279     }
    280 
    281     /**
    282      * Close the connection.  MUST NOT return any exceptions - must be "best effort" and safe.
    283      */
    284     @Override
    285     public void close() {
    286         if (Email.DEBUG) {
    287             Log.d(Logging.LOG_TAG, "*** " + mDebugLabel + " close " +
    288                     getHost() + ":" + String.valueOf(getPort()));
    289         }
    290 
    291         try {
    292             mIn.close();
    293         } catch (Exception e) {
    294             // May fail if the connection is already closed.
    295             if (Email.DEBUG) {
    296                 Log.d(Logging.LOG_TAG, e.toString());
    297             }
    298         }
    299         try {
    300             mOut.close();
    301         } catch (Exception e) {
    302             // May fail if the connection is already closed.
    303             if (Email.DEBUG) {
    304                 Log.d(Logging.LOG_TAG, e.toString());
    305             }
    306         }
    307         try {
    308             mSocket.close();
    309         } catch (Exception e) {
    310             // May fail if the connection is already closed.
    311             if (Email.DEBUG) {
    312                 Log.d(Logging.LOG_TAG, e.toString());
    313             }
    314         }
    315         mIn = null;
    316         mOut = null;
    317         mSocket = null;
    318     }
    319 
    320     @Override
    321     public InputStream getInputStream() {
    322         return mIn;
    323     }
    324 
    325     @Override
    326     public OutputStream getOutputStream() {
    327         return mOut;
    328     }
    329 
    330     /**
    331      * Writes a single line to the server using \r\n termination.
    332      */
    333     @Override
    334     public void writeLine(String s, String sensitiveReplacement) throws IOException {
    335         if (Email.DEBUG) {
    336             if (sensitiveReplacement != null && !Logging.DEBUG_SENSITIVE) {
    337                 Log.d(Logging.LOG_TAG, ">>> " + sensitiveReplacement);
    338             } else {
    339                 Log.d(Logging.LOG_TAG, ">>> " + s);
    340             }
    341         }
    342 
    343         OutputStream out = getOutputStream();
    344         out.write(s.getBytes());
    345         out.write('\r');
    346         out.write('\n');
    347         out.flush();
    348     }
    349 
    350     /**
    351      * Reads a single line from the server, using either \r\n or \n as the delimiter.  The
    352      * delimiter char(s) are not included in the result.
    353      */
    354     @Override
    355     public String readLine() throws IOException {
    356         StringBuffer sb = new StringBuffer();
    357         InputStream in = getInputStream();
    358         int d;
    359         while ((d = in.read()) != -1) {
    360             if (((char)d) == '\r') {
    361                 continue;
    362             } else if (((char)d) == '\n') {
    363                 break;
    364             } else {
    365                 sb.append((char)d);
    366             }
    367         }
    368         if (d == -1 && Email.DEBUG) {
    369             Log.d(Logging.LOG_TAG, "End of stream reached while trying to read line.");
    370         }
    371         String ret = sb.toString();
    372         if (Email.DEBUG) {
    373             Log.d(Logging.LOG_TAG, "<<< " + ret);
    374         }
    375         return ret;
    376     }
    377 
    378     @Override
    379     public InetAddress getLocalAddress() {
    380         if (isOpen()) {
    381             return mSocket.getLocalAddress();
    382         } else {
    383             return null;
    384         }
    385     }
    386 }
    387