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