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.phone.common.mail; 17 18import android.content.Context; 19import android.net.Network; 20 21import com.android.internal.annotations.VisibleForTesting; 22import com.android.phone.common.mail.store.ImapStore; 23import com.android.phone.common.mail.utils.LogUtils; 24import com.android.phone.vvm.omtp.OmtpEvents; 25import com.android.phone.vvm.omtp.imap.ImapHelper; 26 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; 37 38import javax.net.ssl.HostnameVerifier; 39import javax.net.ssl.HttpsURLConnection; 40import javax.net.ssl.SSLException; 41import javax.net.ssl.SSLPeerUnverifiedException; 42import javax.net.ssl.SSLSession; 43import javax.net.ssl.SSLSocket; 44 45/** 46 * Make connection and perform operations on mail server by reading and writing lines. 47 */ 48public class MailTransport { 49 private static final String TAG = "MailTransport"; 50 51 // TODO protected eventually 52 /*protected*/ public static final int SOCKET_CONNECT_TIMEOUT = 10000; 53 /*protected*/ public static final int SOCKET_READ_TIMEOUT = 60000; 54 55 private static final HostnameVerifier HOSTNAME_VERIFIER = 56 HttpsURLConnection.getDefaultHostnameVerifier(); 57 58 private final Context mContext; 59 private final ImapHelper mImapHelper; 60 private final Network mNetwork; 61 private final String mHost; 62 private final int mPort; 63 private Socket mSocket; 64 private BufferedInputStream mIn; 65 private BufferedOutputStream mOut; 66 private final int mFlags; 67 private SocketCreator mSocketCreator; 68 private InetSocketAddress mAddress; 69 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 85 public MailTransport clone() { 86 return new MailTransport(mContext, mImapHelper, mNetwork, mHost, mPort, mFlags); 87 } 88 89 public boolean canTrySslSecurity() { 90 return (mFlags & ImapStore.FLAG_SSL) != 0; 91 } 92 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 */ 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.handleEvent(OmtpEvents.DATA_CANNOT_RESOLVE_HOST_ON_NETWORK); 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 mAddress = socketAddresses.remove(0); 130 mSocket.connect(mAddress, SOCKET_CONNECT_TIMEOUT); 131 132 if (canTrySslSecurity()) { 133 /* 134 SSLSocket cannot be created with a connection timeout, so instead of doing a 135 direct SSL connection, we connect with a normal connection and upgrade it into 136 SSL 137 */ 138 reopenTls(); 139 } else { 140 mIn = new BufferedInputStream(mSocket.getInputStream(), 1024); 141 mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512); 142 mSocket.setSoTimeout(SOCKET_READ_TIMEOUT); 143 } 144 success = true; 145 return; 146 } catch (IOException ioe) { 147 LogUtils.d(TAG, ioe.toString()); 148 if (socketAddresses.size() == 0) { 149 // Only throw an error when there are no more sockets to try. 150 mImapHelper.handleEvent(OmtpEvents.DATA_ALL_SOCKET_CONNECTION_FAILED); 151 throw new MessagingException(MessagingException.IOERROR, ioe.toString()); 152 } 153 } finally { 154 if (!success) { 155 try { 156 mSocket.close(); 157 mSocket = null; 158 } catch (IOException ioe) { 159 throw new MessagingException(MessagingException.IOERROR, ioe.toString()); 160 } 161 162 } 163 } 164 } 165 } 166 167 // For testing. We need something that can replace the behavior of "new Socket()" 168 @VisibleForTesting 169 interface SocketCreator { 170 171 Socket createSocket() throws MessagingException; 172 } 173 174 @VisibleForTesting 175 void setSocketCreator(SocketCreator creator) { 176 mSocketCreator = creator; 177 } 178 179 protected Socket createSocket() throws MessagingException { 180 if (mSocketCreator != null) { 181 return mSocketCreator.createSocket(); 182 } 183 184 if (mNetwork == null) { 185 LogUtils.v(TAG, "createSocket: network not specified"); 186 return new Socket(); 187 } 188 189 try { 190 LogUtils.v(TAG, "createSocket: network specified"); 191 return mNetwork.getSocketFactory().createSocket(); 192 } catch (IOException ioe) { 193 LogUtils.d(TAG, ioe.toString()); 194 throw new MessagingException(MessagingException.IOERROR, ioe.toString()); 195 } 196 } 197 198 /** 199 * Attempts to reopen a normal connection into a TLS connection. 200 */ 201 public void reopenTls() throws MessagingException { 202 try { 203 LogUtils.d(TAG, "open: converting to TLS socket"); 204 mSocket = HttpsURLConnection.getDefaultSSLSocketFactory() 205 .createSocket(mSocket, mAddress.getHostName(), mAddress.getPort(), true); 206 // After the socket connects to an SSL server, confirm that the hostname is as 207 // expected 208 if (!canTrustAllCertificates()) { 209 verifyHostname(mSocket, mHost); 210 } 211 mSocket.setSoTimeout(SOCKET_READ_TIMEOUT); 212 mIn = new BufferedInputStream(mSocket.getInputStream(), 1024); 213 mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512); 214 215 } catch (SSLException e) { 216 LogUtils.d(TAG, e.toString()); 217 throw new CertificateValidationException(e.getMessage(), e); 218 } catch (IOException ioe) { 219 LogUtils.d(TAG, ioe.toString()); 220 throw new MessagingException(MessagingException.IOERROR, ioe.toString()); 221 } 222 } 223 224 /** 225 * Lightweight version of SSLCertificateSocketFactory.verifyHostname, which provides this 226 * service but is not in the public API. 227 * 228 * Verify the hostname of the certificate used by the other end of a 229 * connected socket. It is harmless to call this method redundantly if the hostname has already 230 * been verified. 231 * 232 * <p>Wildcard certificates are allowed to verify any matching hostname, 233 * so "foo.bar.example.com" is verified if the peer has a certificate 234 * 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("Certificate hostname not useable for server: " 259 + session.getPeerPrincipal()); 260 } 261 } 262 263 public boolean isOpen() { 264 return (mIn != null && mOut != null && 265 mSocket != null && mSocket.isConnected() && !mSocket.isClosed()); 266 } 267 268 /** 269 * Close the connection. MUST NOT return any exceptions - must be "best effort" and safe. 270 */ 271 public void close() { 272 try { 273 mIn.close(); 274 } catch (Exception e) { 275 // May fail if the connection is already closed. 276 } 277 try { 278 mOut.close(); 279 } catch (Exception e) { 280 // May fail if the connection is already closed. 281 } 282 try { 283 mSocket.close(); 284 } catch (Exception e) { 285 // May fail if the connection is already closed. 286 } 287 mIn = null; 288 mOut = null; 289 mSocket = null; 290 } 291 292 public String getHost() { 293 return mHost; 294 } 295 296 public InputStream getInputStream() { 297 return mIn; 298 } 299 300 public OutputStream getOutputStream() { 301 return mOut; 302 } 303 304 /** 305 * Writes a single line to the server using \r\n termination. 306 */ 307 public void writeLine(String s, String sensitiveReplacement) throws IOException { 308 if (sensitiveReplacement != null) { 309 LogUtils.d(TAG, ">>> " + sensitiveReplacement); 310 } else { 311 LogUtils.d(TAG, ">>> " + s); 312 } 313 314 OutputStream out = getOutputStream(); 315 out.write(s.getBytes()); 316 out.write('\r'); 317 out.write('\n'); 318 out.flush(); 319 } 320 321 /** 322 * Reads a single line from the server, using either \r\n or \n as the delimiter. The 323 * delimiter char(s) are not included in the result. 324 */ 325 public String readLine(boolean loggable) throws IOException { 326 StringBuffer sb = new StringBuffer(); 327 InputStream in = getInputStream(); 328 int d; 329 while ((d = in.read()) != -1) { 330 if (((char)d) == '\r') { 331 continue; 332 } else if (((char)d) == '\n') { 333 break; 334 } else { 335 sb.append((char)d); 336 } 337 } 338 if (d == -1) { 339 LogUtils.d(TAG, "End of stream reached while trying to read line."); 340 } 341 String ret = sb.toString(); 342 if (loggable) { 343 LogUtils.d(TAG, "<<< " + ret); 344 } 345 return ret; 346 } 347} 348