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 */ 16package com.android.voicemail.impl.mail; 17 18import android.content.Context; 19import android.net.Network; 20import android.net.TrafficStats; 21import android.support.annotation.VisibleForTesting; 22import com.android.dialer.constants.TrafficStatsTags; 23import com.android.voicemail.impl.OmtpEvents; 24import com.android.voicemail.impl.imap.ImapHelper; 25import com.android.voicemail.impl.mail.store.ImapStore; 26import com.android.voicemail.impl.mail.utils.LogUtils; 27import java.io.BufferedInputStream; 28import java.io.BufferedOutputStream; 29import java.io.IOException; 30import java.io.InputStream; 31import java.io.OutputStream; 32import java.net.InetAddress; 33import java.net.InetSocketAddress; 34import java.net.Socket; 35import java.util.ArrayList; 36import java.util.List; 37import javax.net.ssl.HostnameVerifier; 38import javax.net.ssl.HttpsURLConnection; 39import javax.net.ssl.SSLException; 40import javax.net.ssl.SSLPeerUnverifiedException; 41import javax.net.ssl.SSLSession; 42import javax.net.ssl.SSLSocket; 43 44/** Make connection and perform operations on mail server by reading and writing lines. */ 45public 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 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 87 public MailTransport clone() { 88 return new MailTransport(context, imapHelper, network, host, port, flags); 89 } 90 91 public boolean canTrySslSecurity() { 92 return (flags & ImapStore.FLAG_SSL) != 0; 93 } 94 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 */ 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 173 Socket createSocket() throws MessagingException; 174 } 175 176 @VisibleForTesting 177 void setSocketCreator(SocketCreator creator) { 178 socketCreator = creator; 179 } 180 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. */ 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 */ 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 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. */ 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 295 public String getHost() { 296 return host; 297 } 298 299 public InputStream getInputStream() { 300 return in; 301 } 302 303 public OutputStream getOutputStream() { 304 return out; 305 } 306 307 /** Writes a single line to the server using \r\n termination. */ 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 */ 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