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