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