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.email.DebugUtils;
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.analytics.Analytics;
28 import com.android.mail.utils.LogUtils;
29 
30 import java.io.BufferedInputStream;
31 import java.io.BufferedOutputStream;
32 import java.io.IOException;
33 import java.io.InputStream;
34 import java.io.OutputStream;
35 import java.net.InetAddress;
36 import java.net.InetSocketAddress;
37 import java.net.Socket;
38 import java.net.SocketAddress;
39 import java.net.SocketException;
40 
41 import javax.net.ssl.HostnameVerifier;
42 import javax.net.ssl.HttpsURLConnection;
43 import javax.net.ssl.SSLException;
44 import javax.net.ssl.SSLPeerUnverifiedException;
45 import javax.net.ssl.SSLSession;
46 import javax.net.ssl.SSLSocket;
47 
48 public 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 
MailTransport(Context context, String debugLabel, HostAuth hostAuth)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
clone()78     public MailTransport clone() {
79         return new MailTransport(mContext, mDebugLabel, mHostAuth);
80     }
81 
getHost()82     public String getHost() {
83         return mHostAuth.mAddress;
84     }
85 
getPort()86     public int getPort() {
87         return mHostAuth.mPort;
88     }
89 
canTrySslSecurity()90     public boolean canTrySslSecurity() {
91         return (mHostAuth.mFlags & HostAuth.FLAG_SSL) != 0;
92     }
93 
canTryTlsSecurity()94     public boolean canTryTlsSecurity() {
95         return (mHostAuth.mFlags & HostAuth.FLAG_TLS) != 0;
96     }
97 
canTrustAllCertificates()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      */
open()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             Analytics.getInstance().sendEvent("socket_certificates",
126                     "open", Boolean.toString(canTrustAllCertificates()), 0);
127             if (mSocket instanceof SSLSocket) {
128                 final SSLSocket sslSocket = (SSLSocket) mSocket;
129                 if (sslSocket.getSession() != null) {
130                     Analytics.getInstance().sendEvent("cipher_suite",
131                             sslSocket.getSession().getProtocol(),
132                             sslSocket.getSession().getCipherSuite(), 0);
133                 }
134             }
135             mIn = new BufferedInputStream(mSocket.getInputStream(), 1024);
136             mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
137             mSocket.setSoTimeout(SOCKET_READ_TIMEOUT);
138         } catch (SSLException e) {
139             if (DebugUtils.DEBUG) {
140                 LogUtils.d(Logging.LOG_TAG, e.toString());
141             }
142             throw new CertificateValidationException(e.getMessage(), e);
143         } catch (IOException ioe) {
144             if (DebugUtils.DEBUG) {
145                 LogUtils.d(Logging.LOG_TAG, ioe.toString());
146             }
147             throw new MessagingException(MessagingException.IOERROR, ioe.toString());
148         } catch (IllegalArgumentException iae) {
149             if (DebugUtils.DEBUG) {
150                 LogUtils.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      */
reopenTls()164     public void reopenTls() throws MessagingException {
165         try {
166             mSocket = SSLUtils.getSSLSocketFactory(mContext, mHostAuth, null,
167                     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             Analytics.getInstance().sendEvent("socket_certificates",
174                     "reopenTls", Boolean.toString(canTrustAllCertificates()), 0);
175             final SSLSocket sslSocket = (SSLSocket) mSocket;
176             if (sslSocket.getSession() != null) {
177                 Analytics.getInstance().sendEvent("cipher_suite",
178                         sslSocket.getSession().getProtocol(),
179                         sslSocket.getSession().getCipherSuite(), 0);
180             }
181         } catch (SSLException e) {
182             if (DebugUtils.DEBUG) {
183                 LogUtils.d(Logging.LOG_TAG, e.toString());
184             }
185             throw new CertificateValidationException(e.getMessage(), e);
186         } catch (IOException ioe) {
187             if (DebugUtils.DEBUG) {
188                 LogUtils.d(Logging.LOG_TAG, ioe.toString());
189             }
190             throw new MessagingException(MessagingException.IOERROR, ioe.toString());
191         }
192     }
193 
194     /**
195      * Lightweight version of SSLCertificateSocketFactory.verifyHostname, which provides this
196      * service but is not in the public API.
197      *
198      * Verify the hostname of the certificate used by the other end of a
199      * connected socket.  You MUST call this if you did not supply a hostname
200      * to SSLCertificateSocketFactory.createSocket().  It is harmless to call this method
201      * redundantly if the hostname has already been verified.
202      *
203      * <p>Wildcard certificates are allowed to verify any matching hostname,
204      * so "foo.bar.example.com" is verified if the peer has a certificate
205      * for "*.example.com".
206      *
207      * @param socket An SSL socket which has been connected to a server
208      * @param hostname The expected hostname of the remote server
209      * @throws IOException if something goes wrong handshaking with the server
210      * @throws SSLPeerUnverifiedException if the server cannot prove its identity
211       */
verifyHostname(Socket socket, String hostname)212     private static void verifyHostname(Socket socket, String hostname) throws IOException {
213         // The code at the start of OpenSSLSocketImpl.startHandshake()
214         // ensures that the call is idempotent, so we can safely call it.
215         SSLSocket ssl = (SSLSocket) socket;
216         ssl.startHandshake();
217 
218         SSLSession session = ssl.getSession();
219         if (session == null) {
220             throw new SSLException("Cannot verify SSL socket without session");
221         }
222         // TODO: Instead of reporting the name of the server we think we're connecting to,
223         // we should be reporting the bad name in the certificate.  Unfortunately this is buried
224         // in the verifier code and is not available in the verifier API, and extracting the
225         // CN & alts is beyond the scope of this patch.
226         if (!HOSTNAME_VERIFIER.verify(hostname, session)) {
227             throw new SSLPeerUnverifiedException(
228                     "Certificate hostname not useable for server: " + hostname);
229         }
230     }
231 
232     /**
233      * Get the socket timeout.
234      * @return the read timeout value in milliseconds
235      * @throws SocketException
236      */
getSoTimeout()237     public int getSoTimeout() throws SocketException {
238         return mSocket.getSoTimeout();
239     }
240 
241     /**
242      * Set the socket timeout.
243      * @param timeoutMilliseconds the read timeout value if greater than {@code 0}, or
244      *            {@code 0} for an infinite timeout.
245      */
setSoTimeout(int timeoutMilliseconds)246     public void setSoTimeout(int timeoutMilliseconds) throws SocketException {
247         mSocket.setSoTimeout(timeoutMilliseconds);
248     }
249 
isOpen()250     public boolean isOpen() {
251         return (mIn != null && mOut != null &&
252                 mSocket != null && mSocket.isConnected() && !mSocket.isClosed());
253     }
254 
255     /**
256      * Close the connection.  MUST NOT return any exceptions - must be "best effort" and safe.
257      */
close()258     public void close() {
259         try {
260             mIn.close();
261         } catch (Exception e) {
262             // May fail if the connection is already closed.
263         }
264         try {
265             mOut.close();
266         } catch (Exception e) {
267             // May fail if the connection is already closed.
268         }
269         try {
270             mSocket.close();
271         } catch (Exception e) {
272             // May fail if the connection is already closed.
273         }
274         mIn = null;
275         mOut = null;
276         mSocket = null;
277     }
278 
getInputStream()279     public InputStream getInputStream() {
280         return mIn;
281     }
282 
getOutputStream()283     public OutputStream getOutputStream() {
284         return mOut;
285     }
286 
287     /**
288      * Writes a single line to the server using \r\n termination.
289      */
writeLine(String s, String sensitiveReplacement)290     public void writeLine(String s, String sensitiveReplacement) throws IOException {
291         if (DebugUtils.DEBUG) {
292             if (sensitiveReplacement != null && !Logging.DEBUG_SENSITIVE) {
293                 LogUtils.d(Logging.LOG_TAG, ">>> " + sensitiveReplacement);
294             } else {
295                 LogUtils.d(Logging.LOG_TAG, ">>> " + s);
296             }
297         }
298 
299         OutputStream out = getOutputStream();
300         out.write(s.getBytes());
301         out.write('\r');
302         out.write('\n');
303         out.flush();
304     }
305 
306     /**
307      * Reads a single line from the server, using either \r\n or \n as the delimiter.  The
308      * delimiter char(s) are not included in the result.
309      */
readLine(boolean loggable)310     public String readLine(boolean loggable) throws IOException {
311         StringBuffer sb = new StringBuffer();
312         InputStream in = getInputStream();
313         int d;
314         while ((d = in.read()) != -1) {
315             if (((char)d) == '\r') {
316                 continue;
317             } else if (((char)d) == '\n') {
318                 break;
319             } else {
320                 sb.append((char)d);
321             }
322         }
323         if (d == -1 && DebugUtils.DEBUG) {
324             LogUtils.d(Logging.LOG_TAG, "End of stream reached while trying to read line.");
325         }
326         String ret = sb.toString();
327         if (loggable && DebugUtils.DEBUG) {
328             LogUtils.d(Logging.LOG_TAG, "<<< " + ret);
329         }
330         return ret;
331     }
332 
getLocalAddress()333     public InetAddress getLocalAddress() {
334         if (isOpen()) {
335             return mSocket.getLocalAddress();
336         } else {
337             return null;
338         }
339     }
340 }
341