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