• 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 import android.provider.VoicemailContract.Status;
21 
22 import com.android.internal.annotations.VisibleForTesting;
23 import com.android.phone.common.mail.store.ImapStore;
24 import com.android.phone.common.mail.utils.LogUtils;
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.net.SocketAddress;
36 import java.util.ArrayList;
37 import java.util.List;
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  * Make connection and perform operations on mail server by reading and writing lines.
48  */
49 public class MailTransport {
50     private static final String TAG = "MailTransport";
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 final Context mContext;
60     private final ImapHelper mImapHelper;
61     private final Network mNetwork;
62     private final String mHost;
63     private final int mPort;
64     private Socket mSocket;
65     private BufferedInputStream mIn;
66     private BufferedOutputStream mOut;
67     private final int mFlags;
68     private SocketCreator mSocketCreator;
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.setDataChannelState(Status.DATA_CHANNEL_STATE_SERVER_CONNECTION_ERROR);
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                 InetSocketAddress address = socketAddresses.remove(0);
130                 mSocket.connect(address, SOCKET_CONNECT_TIMEOUT);
131 
132                 if (canTrySslSecurity()) {
133                     /**
134                      * {@link SSLSocket} must connect in its constructor, or create through a
135                      * already connected socket. Since we need to use
136                      * {@link Socket#connect(SocketAddress, int) } to set timeout, we can only
137                      * create it here.
138                      */
139                     LogUtils.d(TAG, "open: converting to SSL socket");
140                     mSocket = HttpsURLConnection.getDefaultSSLSocketFactory()
141                             .createSocket(mSocket, address.getHostName(), address.getPort(), true);
142                     // After the socket connects to an SSL server, confirm that the hostname is as
143                     // expected
144                     if (!canTrustAllCertificates()) {
145                         verifyHostname(mSocket, mHost);
146                     }
147                 }
148 
149                 mIn = new BufferedInputStream(mSocket.getInputStream(), 1024);
150                 mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
151                 mSocket.setSoTimeout(SOCKET_READ_TIMEOUT);
152                 success = true;
153                 return;
154             } catch (IOException ioe) {
155                 LogUtils.d(TAG, ioe.toString());
156                 if (socketAddresses.size() == 0) {
157                     // Only throw an error when there are no more sockets to try.
158                     mImapHelper
159                             .setDataChannelState(Status.DATA_CHANNEL_STATE_SERVER_CONNECTION_ERROR);
160                     throw new MessagingException(MessagingException.IOERROR, ioe.toString());
161                 }
162             } finally {
163                 if (!success) {
164                     try {
165                         mSocket.close();
166                         mSocket = null;
167                     } catch (IOException ioe) {
168                         throw new MessagingException(MessagingException.IOERROR, ioe.toString());
169                     }
170 
171                 }
172             }
173         }
174     }
175 
176     // For testing. We need something that can replace the behavior of "new Socket()"
177     @VisibleForTesting
178     interface SocketCreator {
179 
createSocket()180         Socket createSocket() throws MessagingException;
181     }
182 
183     @VisibleForTesting
setSocketCreator(SocketCreator creator)184     void setSocketCreator(SocketCreator creator) {
185         mSocketCreator = creator;
186     }
187 
createSocket()188     protected Socket createSocket() throws MessagingException {
189         if (mSocketCreator != null) {
190             return mSocketCreator.createSocket();
191         }
192 
193         if (mNetwork == null) {
194             LogUtils.v(TAG, "createSocket: network not specified");
195             return new Socket();
196         }
197 
198         try {
199             LogUtils.v(TAG, "createSocket: network specified");
200             return mNetwork.getSocketFactory().createSocket();
201         } catch (IOException ioe) {
202             LogUtils.d(TAG, ioe.toString());
203             throw new MessagingException(MessagingException.IOERROR, ioe.toString());
204         }
205     }
206 
207     /**
208      * Lightweight version of SSLCertificateSocketFactory.verifyHostname, which provides this
209      * service but is not in the public API.
210      *
211      * Verify the hostname of the certificate used by the other end of a
212      * connected socket. It is harmless to call this method redundantly if the hostname has already
213      * been verified.
214      *
215      * <p>Wildcard certificates are allowed to verify any matching hostname,
216      * so "foo.bar.example.com" is verified if the peer has a certificate
217      * for "*.example.com".
218      *
219      * @param socket An SSL socket which has been connected to a server
220      * @param hostname The expected hostname of the remote server
221      * @throws IOException if something goes wrong handshaking with the server
222      * @throws SSLPeerUnverifiedException if the server cannot prove its identity
223       */
verifyHostname(Socket socket, String hostname)224     private void verifyHostname(Socket socket, String hostname) throws IOException {
225         // The code at the start of OpenSSLSocketImpl.startHandshake()
226         // ensures that the call is idempotent, so we can safely call it.
227         SSLSocket ssl = (SSLSocket) socket;
228         ssl.startHandshake();
229 
230         SSLSession session = ssl.getSession();
231         if (session == null) {
232             mImapHelper.setDataChannelState(Status.DATA_CHANNEL_STATE_COMMUNICATION_ERROR);
233             throw new SSLException("Cannot verify SSL socket without session");
234         }
235         // TODO: Instead of reporting the name of the server we think we're connecting to,
236         // we should be reporting the bad name in the certificate.  Unfortunately this is buried
237         // in the verifier code and is not available in the verifier API, and extracting the
238         // CN & alts is beyond the scope of this patch.
239         if (!HOSTNAME_VERIFIER.verify(hostname, session)) {
240             mImapHelper.setDataChannelState(Status.DATA_CHANNEL_STATE_COMMUNICATION_ERROR);
241             throw new SSLPeerUnverifiedException("Certificate hostname not useable for server: "
242                     + session.getPeerPrincipal());
243         }
244     }
245 
isOpen()246     public boolean isOpen() {
247         return (mIn != null && mOut != null &&
248                 mSocket != null && mSocket.isConnected() && !mSocket.isClosed());
249     }
250 
251     /**
252      * Close the connection.  MUST NOT return any exceptions - must be "best effort" and safe.
253      */
close()254     public void close() {
255         try {
256             mIn.close();
257         } catch (Exception e) {
258             // May fail if the connection is already closed.
259         }
260         try {
261             mOut.close();
262         } catch (Exception e) {
263             // May fail if the connection is already closed.
264         }
265         try {
266             mSocket.close();
267         } catch (Exception e) {
268             // May fail if the connection is already closed.
269         }
270         mIn = null;
271         mOut = null;
272         mSocket = null;
273     }
274 
getInputStream()275     public InputStream getInputStream() {
276         return mIn;
277     }
278 
getOutputStream()279     public OutputStream getOutputStream() {
280         return mOut;
281     }
282 
283     /**
284      * Writes a single line to the server using \r\n termination.
285      */
writeLine(String s, String sensitiveReplacement)286     public void writeLine(String s, String sensitiveReplacement) throws IOException {
287         if (sensitiveReplacement != null) {
288             LogUtils.d(TAG, ">>> " + sensitiveReplacement);
289         } else {
290             LogUtils.d(TAG, ">>> " + s);
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      */
readLine(boolean loggable)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) {
318             LogUtils.d(TAG, "End of stream reached while trying to read line.");
319         }
320         String ret = sb.toString();
321         if (loggable) {
322             LogUtils.d(TAG, "<<< " + ret);
323         }
324         return ret;
325     }
326 }
327