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