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