• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 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 package com.android.phone.common.mail;
17 
18 import android.content.Context;
19 import android.net.Network;
20 
21 import com.android.internal.annotations.VisibleForTesting;
22 import com.android.phone.common.mail.store.ImapStore;
23 import com.android.phone.common.mail.utils.LogUtils;
24 import com.android.phone.vvm.omtp.OmtpEvents;
25 import com.android.phone.vvm.omtp.imap.ImapHelper;
26 
27 import java.io.BufferedInputStream;
28 import java.io.BufferedOutputStream;
29 import java.io.IOException;
30 import java.io.InputStream;
31 import java.io.OutputStream;
32 import java.net.InetAddress;
33 import java.net.InetSocketAddress;
34 import java.net.Socket;
35 import java.util.ArrayList;
36 import java.util.List;
37 
38 import javax.net.ssl.HostnameVerifier;
39 import javax.net.ssl.HttpsURLConnection;
40 import javax.net.ssl.SSLException;
41 import javax.net.ssl.SSLPeerUnverifiedException;
42 import javax.net.ssl.SSLSession;
43 import javax.net.ssl.SSLSocket;
44 
45 /**
46  * Make connection and perform operations on mail server by reading and writing lines.
47  */
48 public class MailTransport {
49     private static final String TAG = "MailTransport";
50 
51     // TODO protected eventually
52     /*protected*/ public static final int SOCKET_CONNECT_TIMEOUT = 10000;
53     /*protected*/ public static final int SOCKET_READ_TIMEOUT = 60000;
54 
55     private static final HostnameVerifier HOSTNAME_VERIFIER =
56             HttpsURLConnection.getDefaultHostnameVerifier();
57 
58     private final Context mContext;
59     private final ImapHelper mImapHelper;
60     private final Network mNetwork;
61     private final String mHost;
62     private final int mPort;
63     private Socket mSocket;
64     private BufferedInputStream mIn;
65     private BufferedOutputStream mOut;
66     private final int mFlags;
67     private SocketCreator mSocketCreator;
68     private InetSocketAddress mAddress;
69 
MailTransport(Context context, ImapHelper imapHelper, Network network, String address, int port, int flags)70     public MailTransport(Context context, ImapHelper imapHelper, Network network, String address,
71             int port, int flags) {
72         mContext = context;
73         mImapHelper = imapHelper;
74         mNetwork = network;
75         mHost = address;
76         mPort = port;
77         mFlags = flags;
78     }
79 
80     /**
81      * Returns a new transport, using the current transport as a model. The new transport is
82      * configured identically, but not opened or connected in any way.
83      */
84     @Override
clone()85     public MailTransport clone() {
86         return new MailTransport(mContext, mImapHelper, mNetwork, mHost, mPort, mFlags);
87     }
88 
canTrySslSecurity()89     public boolean canTrySslSecurity() {
90         return (mFlags & ImapStore.FLAG_SSL) != 0;
91     }
92 
canTrustAllCertificates()93     public boolean canTrustAllCertificates() {
94         return (mFlags & ImapStore.FLAG_TRUST_ALL) != 0;
95     }
96 
97     /**
98      * Attempts to open a connection using the Uri supplied for connection parameters.  Will attempt
99      * an SSL connection if indicated.
100      */
open()101     public void open() throws MessagingException {
102         LogUtils.d(TAG, "*** IMAP open " + mHost + ":" + String.valueOf(mPort));
103 
104         List<InetSocketAddress> socketAddresses = new ArrayList<InetSocketAddress>();
105 
106         if (mNetwork == null) {
107             socketAddresses.add(new InetSocketAddress(mHost, mPort));
108         } else {
109             try {
110                 InetAddress[] inetAddresses = mNetwork.getAllByName(mHost);
111                 if (inetAddresses.length == 0) {
112                     throw new MessagingException(MessagingException.IOERROR,
113                             "Host name " + mHost + "cannot be resolved on designated network");
114                 }
115                 for (int i = 0; i < inetAddresses.length; i++) {
116                     socketAddresses.add(new InetSocketAddress(inetAddresses[i], mPort));
117                 }
118             } catch (IOException ioe) {
119                 LogUtils.d(TAG, ioe.toString());
120                 mImapHelper.handleEvent(OmtpEvents.DATA_CANNOT_RESOLVE_HOST_ON_NETWORK);
121                 throw new MessagingException(MessagingException.IOERROR, ioe.toString());
122             }
123         }
124 
125         boolean success = false;
126         while (socketAddresses.size() > 0) {
127             mSocket = createSocket();
128             try {
129                 mAddress = socketAddresses.remove(0);
130                 mSocket.connect(mAddress, SOCKET_CONNECT_TIMEOUT);
131 
132                 if (canTrySslSecurity()) {
133                     /*
134                     SSLSocket cannot be created with a connection timeout, so instead of doing a
135                     direct SSL connection, we connect with a normal connection and upgrade it into
136                     SSL
137                      */
138                     reopenTls();
139                 } else {
140                     mIn = new BufferedInputStream(mSocket.getInputStream(), 1024);
141                     mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
142                     mSocket.setSoTimeout(SOCKET_READ_TIMEOUT);
143                 }
144                 success = true;
145                 return;
146             } catch (IOException ioe) {
147                 LogUtils.d(TAG, ioe.toString());
148                 if (socketAddresses.size() == 0) {
149                     // Only throw an error when there are no more sockets to try.
150                     mImapHelper.handleEvent(OmtpEvents.DATA_ALL_SOCKET_CONNECTION_FAILED);
151                     throw new MessagingException(MessagingException.IOERROR, ioe.toString());
152                 }
153             } finally {
154                 if (!success) {
155                     try {
156                         mSocket.close();
157                         mSocket = null;
158                     } catch (IOException ioe) {
159                         throw new MessagingException(MessagingException.IOERROR, ioe.toString());
160                     }
161 
162                 }
163             }
164         }
165     }
166 
167     // For testing. We need something that can replace the behavior of "new Socket()"
168     @VisibleForTesting
169     interface SocketCreator {
170 
createSocket()171         Socket createSocket() throws MessagingException;
172     }
173 
174     @VisibleForTesting
setSocketCreator(SocketCreator creator)175     void setSocketCreator(SocketCreator creator) {
176         mSocketCreator = creator;
177     }
178 
createSocket()179     protected Socket createSocket() throws MessagingException {
180         if (mSocketCreator != null) {
181             return mSocketCreator.createSocket();
182         }
183 
184         if (mNetwork == null) {
185             LogUtils.v(TAG, "createSocket: network not specified");
186             return new Socket();
187         }
188 
189         try {
190             LogUtils.v(TAG, "createSocket: network specified");
191             return mNetwork.getSocketFactory().createSocket();
192         } catch (IOException ioe) {
193             LogUtils.d(TAG, ioe.toString());
194             throw new MessagingException(MessagingException.IOERROR, ioe.toString());
195         }
196     }
197 
198     /**
199      * Attempts to reopen a normal connection into a TLS connection.
200      */
reopenTls()201     public void reopenTls() throws MessagingException {
202         try {
203             LogUtils.d(TAG, "open: converting to TLS socket");
204             mSocket = HttpsURLConnection.getDefaultSSLSocketFactory()
205                     .createSocket(mSocket, mAddress.getHostName(), mAddress.getPort(), true);
206             // After the socket connects to an SSL server, confirm that the hostname is as
207             // expected
208             if (!canTrustAllCertificates()) {
209                 verifyHostname(mSocket, mHost);
210             }
211             mSocket.setSoTimeout(SOCKET_READ_TIMEOUT);
212             mIn = new BufferedInputStream(mSocket.getInputStream(), 1024);
213             mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
214 
215         } catch (SSLException e) {
216             LogUtils.d(TAG, e.toString());
217             throw new CertificateValidationException(e.getMessage(), e);
218         } catch (IOException ioe) {
219             LogUtils.d(TAG, ioe.toString());
220             throw new MessagingException(MessagingException.IOERROR, ioe.toString());
221         }
222     }
223 
224     /**
225      * Lightweight version of SSLCertificateSocketFactory.verifyHostname, which provides this
226      * service but is not in the public API.
227      *
228      * Verify the hostname of the certificate used by the other end of a
229      * connected socket. It is harmless to call this method redundantly if the hostname has already
230      * been verified.
231      *
232      * <p>Wildcard certificates are allowed to verify any matching hostname,
233      * so "foo.bar.example.com" is verified if the peer has a certificate
234      * for "*.example.com".
235      *
236      * @param socket An SSL socket which has been connected to a server
237      * @param hostname The expected hostname of the remote server
238      * @throws IOException if something goes wrong handshaking with the server
239      * @throws SSLPeerUnverifiedException if the server cannot prove its identity
240       */
verifyHostname(Socket socket, String hostname)241     private void verifyHostname(Socket socket, String hostname) throws IOException {
242         // The code at the start of OpenSSLSocketImpl.startHandshake()
243         // ensures that the call is idempotent, so we can safely call it.
244         SSLSocket ssl = (SSLSocket) socket;
245         ssl.startHandshake();
246 
247         SSLSession session = ssl.getSession();
248         if (session == null) {
249             mImapHelper.handleEvent(OmtpEvents.DATA_CANNOT_ESTABLISH_SSL_SESSION);
250             throw new SSLException("Cannot verify SSL socket without session");
251         }
252         // TODO: Instead of reporting the name of the server we think we're connecting to,
253         // we should be reporting the bad name in the certificate.  Unfortunately this is buried
254         // in the verifier code and is not available in the verifier API, and extracting the
255         // CN & alts is beyond the scope of this patch.
256         if (!HOSTNAME_VERIFIER.verify(hostname, session)) {
257             mImapHelper.handleEvent(OmtpEvents.DATA_SSL_INVALID_HOST_NAME);
258             throw new SSLPeerUnverifiedException("Certificate hostname not useable for server: "
259                     + session.getPeerPrincipal());
260         }
261     }
262 
isOpen()263     public boolean isOpen() {
264         return (mIn != null && mOut != null &&
265                 mSocket != null && mSocket.isConnected() && !mSocket.isClosed());
266     }
267 
268     /**
269      * Close the connection.  MUST NOT return any exceptions - must be "best effort" and safe.
270      */
close()271     public void close() {
272         try {
273             mIn.close();
274         } catch (Exception e) {
275             // May fail if the connection is already closed.
276         }
277         try {
278             mOut.close();
279         } catch (Exception e) {
280             // May fail if the connection is already closed.
281         }
282         try {
283             mSocket.close();
284         } catch (Exception e) {
285             // May fail if the connection is already closed.
286         }
287         mIn = null;
288         mOut = null;
289         mSocket = null;
290     }
291 
getHost()292     public String getHost() {
293         return mHost;
294     }
295 
getInputStream()296     public InputStream getInputStream() {
297         return mIn;
298     }
299 
getOutputStream()300     public OutputStream getOutputStream() {
301         return mOut;
302     }
303 
304     /**
305      * Writes a single line to the server using \r\n termination.
306      */
writeLine(String s, String sensitiveReplacement)307     public void writeLine(String s, String sensitiveReplacement) throws IOException {
308         if (sensitiveReplacement != null) {
309             LogUtils.d(TAG, ">>> " + sensitiveReplacement);
310         } else {
311             LogUtils.d(TAG, ">>> " + s);
312         }
313 
314         OutputStream out = getOutputStream();
315         out.write(s.getBytes());
316         out.write('\r');
317         out.write('\n');
318         out.flush();
319     }
320 
321     /**
322      * Reads a single line from the server, using either \r\n or \n as the delimiter.  The
323      * delimiter char(s) are not included in the result.
324      */
readLine(boolean loggable)325     public String readLine(boolean loggable) throws IOException {
326         StringBuffer sb = new StringBuffer();
327         InputStream in = getInputStream();
328         int d;
329         while ((d = in.read()) != -1) {
330             if (((char)d) == '\r') {
331                 continue;
332             } else if (((char)d) == '\n') {
333                 break;
334             } else {
335                 sb.append((char)d);
336             }
337         }
338         if (d == -1) {
339             LogUtils.d(TAG, "End of stream reached while trying to read line.");
340         }
341         String ret = sb.toString();
342         if (loggable) {
343             LogUtils.d(TAG, "<<< " + ret);
344         }
345         return ret;
346     }
347 }
348