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 mContext; 56 private final ImapHelper mImapHelper; 57 private final Network mNetwork; 58 private final String mHost; 59 private final int mPort; 60 private Socket mSocket; 61 private BufferedInputStream mIn; 62 private BufferedOutputStream mOut; 63 private final int mFlags; 64 private SocketCreator mSocketCreator; 65 private InetSocketAddress mAddress; 66 67 public MailTransport( 68 Context context, 69 ImapHelper imapHelper, 70 Network network, 71 String address, 72 int port, 73 int flags) { 74 mContext = context; 75 mImapHelper = imapHelper; 76 mNetwork = network; 77 mHost = address; 78 mPort = port; 79 mFlags = 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(mContext, mImapHelper, mNetwork, mHost, mPort, mFlags); 89 } 90 91 public boolean canTrySslSecurity() { 92 return (mFlags & ImapStore.FLAG_SSL) != 0; 93 } 94 95 public boolean canTrustAllCertificates() { 96 return (mFlags & 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 " + mHost + ":" + String.valueOf(mPort)); 105 106 List<InetSocketAddress> socketAddresses = new ArrayList<InetSocketAddress>(); 107 108 if (mNetwork == null) { 109 socketAddresses.add(new InetSocketAddress(mHost, mPort)); 110 } else { 111 try { 112 InetAddress[] inetAddresses = mNetwork.getAllByName(mHost); 113 if (inetAddresses.length == 0) { 114 throw new MessagingException( 115 MessagingException.IOERROR, 116 "Host name " + mHost + "cannot be resolved on designated network"); 117 } 118 for (int i = 0; i < inetAddresses.length; i++) { 119 socketAddresses.add(new InetSocketAddress(inetAddresses[i], mPort)); 120 } 121 } catch (IOException ioe) { 122 LogUtils.d(TAG, ioe.toString()); 123 mImapHelper.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 mSocket = createSocket(); 131 try { 132 mAddress = socketAddresses.remove(0); 133 mSocket.connect(mAddress, 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 mIn = new BufferedInputStream(mSocket.getInputStream(), 1024); 144 mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512); 145 mSocket.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 mImapHelper.handleEvent(OmtpEvents.DATA_ALL_SOCKET_CONNECTION_FAILED); 154 throw new MessagingException(MessagingException.IOERROR, ioe.toString()); 155 } 156 } finally { 157 if (!success) { 158 try { 159 mSocket.close(); 160 mSocket = 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 mSocketCreator = creator; 179 } 180 181 protected Socket createSocket() throws MessagingException { 182 if (mSocketCreator != null) { 183 return mSocketCreator.createSocket(); 184 } 185 186 if (mNetwork == 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 mNetwork.getSocketFactory().createSocket(); 195 } catch (IOException ioe) { 196 LogUtils.d(TAG, ioe.toString()); 197 throw new MessagingException(MessagingException.IOERROR, ioe.toString()); 198 } 199 } 200 201 /** Attempts to reopen a normal connection into a TLS connection. */ 202 public void reopenTls() throws MessagingException { 203 try { 204 LogUtils.d(TAG, "open: converting to TLS socket"); 205 mSocket = 206 HttpsURLConnection.getDefaultSSLSocketFactory() 207 .createSocket(mSocket, mAddress.getHostName(), mAddress.getPort(), true); 208 // After the socket connects to an SSL server, confirm that the hostname is as 209 // expected 210 if (!canTrustAllCertificates()) { 211 verifyHostname(mSocket, mHost); 212 } 213 mSocket.setSoTimeout(SOCKET_READ_TIMEOUT); 214 mIn = new BufferedInputStream(mSocket.getInputStream(), 1024); 215 mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512); 216 217 } catch (SSLException e) { 218 LogUtils.d(TAG, e.toString()); 219 throw new CertificateValidationException(e.getMessage(), e); 220 } catch (IOException ioe) { 221 LogUtils.d(TAG, ioe.toString()); 222 throw new MessagingException(MessagingException.IOERROR, ioe.toString()); 223 } 224 } 225 226 /** 227 * Lightweight version of SSLCertificateSocketFactory.verifyHostname, which provides this service 228 * but is not in the public API. 229 * 230 * <p>Verify the hostname of the certificate used by the other end of a connected socket. It is 231 * harmless to call this method redundantly if the hostname has already been verified. 232 * 233 * <p>Wildcard certificates are allowed to verify any matching hostname, so "foo.bar.example.com" 234 * is verified if the peer has a certificate 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 */ 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( 259 "Certificate hostname not useable for server: " + session.getPeerPrincipal()); 260 } 261 } 262 263 public boolean isOpen() { 264 return (mIn != null 265 && mOut != null 266 && mSocket != null 267 && mSocket.isConnected() 268 && !mSocket.isClosed()); 269 } 270 271 /** Close the connection. MUST NOT return any exceptions - must be "best effort" and safe. */ 272 public void close() { 273 try { 274 mIn.close(); 275 } catch (Exception e) { 276 // May fail if the connection is already closed. 277 } 278 try { 279 mOut.close(); 280 } catch (Exception e) { 281 // May fail if the connection is already closed. 282 } 283 try { 284 mSocket.close(); 285 } catch (Exception e) { 286 // May fail if the connection is already closed. 287 } 288 mIn = null; 289 mOut = null; 290 mSocket = null; 291 } 292 293 public String getHost() { 294 return mHost; 295 } 296 297 public InputStream getInputStream() { 298 return mIn; 299 } 300 301 public OutputStream getOutputStream() { 302 return mOut; 303 } 304 305 /** Writes a single line to the server using \r\n termination. */ 306 public void writeLine(String s, String sensitiveReplacement) throws IOException { 307 if (sensitiveReplacement != null) { 308 LogUtils.d(TAG, ">>> " + sensitiveReplacement); 309 } else { 310 LogUtils.d(TAG, ">>> " + s); 311 } 312 313 OutputStream out = getOutputStream(); 314 out.write(s.getBytes()); 315 out.write('\r'); 316 out.write('\n'); 317 out.flush(); 318 } 319 320 /** 321 * Reads a single line from the server, using either \r\n or \n as the delimiter. The delimiter 322 * char(s) are not included in the result. 323 */ 324 public String readLine(boolean loggable) throws IOException { 325 StringBuffer sb = new StringBuffer(); 326 InputStream in = getInputStream(); 327 int d; 328 while ((d = in.read()) != -1) { 329 if (((char) d) == '\r') { 330 continue; 331 } else if (((char) d) == '\n') { 332 break; 333 } else { 334 sb.append((char) d); 335 } 336 } 337 if (d == -1) { 338 LogUtils.d(TAG, "End of stream reached while trying to read line."); 339 } 340 String ret = sb.toString(); 341 if (loggable) { 342 LogUtils.d(TAG, "<<< " + ret); 343 } 344 return ret; 345 } 346 } 347