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