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.support.annotation.VisibleForTesting; 21 import com.android.voicemail.impl.OmtpEvents; 22 import com.android.voicemail.impl.imap.ImapHelper; 23 import com.android.voicemail.impl.mail.store.ImapStore; 24 import com.android.voicemail.impl.mail.utils.LogUtils; 25 import java.io.BufferedInputStream; 26 import java.io.BufferedOutputStream; 27 import java.io.IOException; 28 import java.io.InputStream; 29 import java.io.OutputStream; 30 import java.net.InetAddress; 31 import java.net.InetSocketAddress; 32 import java.net.Socket; 33 import java.util.ArrayList; 34 import java.util.List; 35 import javax.net.ssl.HostnameVerifier; 36 import javax.net.ssl.HttpsURLConnection; 37 import javax.net.ssl.SSLException; 38 import javax.net.ssl.SSLPeerUnverifiedException; 39 import javax.net.ssl.SSLSession; 40 import javax.net.ssl.SSLSocket; 41 42 /** Make connection and perform operations on mail server by reading and writing lines. */ 43 public class MailTransport { 44 private static final String TAG = "MailTransport"; 45 46 // TODO protected eventually 47 /*protected*/ public static final int SOCKET_CONNECT_TIMEOUT = 10000; 48 /*protected*/ public static final int SOCKET_READ_TIMEOUT = 60000; 49 50 private static final HostnameVerifier HOSTNAME_VERIFIER = 51 HttpsURLConnection.getDefaultHostnameVerifier(); 52 53 private final Context mContext; 54 private final ImapHelper mImapHelper; 55 private final Network mNetwork; 56 private final String mHost; 57 private final int mPort; 58 private Socket mSocket; 59 private BufferedInputStream mIn; 60 private BufferedOutputStream mOut; 61 private final int mFlags; 62 private SocketCreator mSocketCreator; 63 private InetSocketAddress mAddress; 64 MailTransport( Context context, ImapHelper imapHelper, Network network, String address, int port, int flags)65 public MailTransport( 66 Context context, 67 ImapHelper imapHelper, 68 Network network, 69 String address, 70 int port, 71 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 clone()85 public MailTransport clone() { 86 return new MailTransport(mContext, mImapHelper, mNetwork, mHost, mPort, mFlags); 87 } 88 canTrySslSecurity()89 public boolean canTrySslSecurity() { 90 return (mFlags & ImapStore.FLAG_SSL) != 0; 91 } 92 canTrustAllCertificates()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 an 99 * SSL connection if indicated. 100 */ open()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( 113 MessagingException.IOERROR, 114 "Host name " + mHost + "cannot be resolved on designated network"); 115 } 116 for (int i = 0; i < inetAddresses.length; i++) { 117 socketAddresses.add(new InetSocketAddress(inetAddresses[i], mPort)); 118 } 119 } catch (IOException ioe) { 120 LogUtils.d(TAG, ioe.toString()); 121 mImapHelper.handleEvent(OmtpEvents.DATA_CANNOT_RESOLVE_HOST_ON_NETWORK); 122 throw new MessagingException(MessagingException.IOERROR, ioe.toString()); 123 } 124 } 125 126 boolean success = false; 127 while (socketAddresses.size() > 0) { 128 mSocket = createSocket(); 129 try { 130 mAddress = socketAddresses.remove(0); 131 mSocket.connect(mAddress, SOCKET_CONNECT_TIMEOUT); 132 133 if (canTrySslSecurity()) { 134 /* 135 SSLSocket cannot be created with a connection timeout, so instead of doing a 136 direct SSL connection, we connect with a normal connection and upgrade it into 137 SSL 138 */ 139 reopenTls(); 140 } else { 141 mIn = new BufferedInputStream(mSocket.getInputStream(), 1024); 142 mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512); 143 mSocket.setSoTimeout(SOCKET_READ_TIMEOUT); 144 } 145 success = true; 146 return; 147 } catch (IOException ioe) { 148 LogUtils.d(TAG, ioe.toString()); 149 if (socketAddresses.size() == 0) { 150 // Only throw an error when there are no more sockets to try. 151 mImapHelper.handleEvent(OmtpEvents.DATA_ALL_SOCKET_CONNECTION_FAILED); 152 throw new MessagingException(MessagingException.IOERROR, ioe.toString()); 153 } 154 } finally { 155 if (!success) { 156 try { 157 mSocket.close(); 158 mSocket = null; 159 } catch (IOException ioe) { 160 throw new MessagingException(MessagingException.IOERROR, ioe.toString()); 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 createSocket()171 Socket createSocket() throws MessagingException; 172 } 173 174 @VisibleForTesting setSocketCreator(SocketCreator creator)175 void setSocketCreator(SocketCreator creator) { 176 mSocketCreator = creator; 177 } 178 createSocket()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 /** Attempts to reopen a normal connection into a TLS connection. */ reopenTls()199 public void reopenTls() throws MessagingException { 200 try { 201 LogUtils.d(TAG, "open: converting to TLS socket"); 202 mSocket = 203 HttpsURLConnection.getDefaultSSLSocketFactory() 204 .createSocket(mSocket, mAddress.getHostName(), mAddress.getPort(), true); 205 // After the socket connects to an SSL server, confirm that the hostname is as 206 // expected 207 if (!canTrustAllCertificates()) { 208 verifyHostname(mSocket, mHost); 209 } 210 mSocket.setSoTimeout(SOCKET_READ_TIMEOUT); 211 mIn = new BufferedInputStream(mSocket.getInputStream(), 1024); 212 mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512); 213 214 } catch (SSLException e) { 215 LogUtils.d(TAG, e.toString()); 216 throw new CertificateValidationException(e.getMessage(), e); 217 } catch (IOException ioe) { 218 LogUtils.d(TAG, ioe.toString()); 219 throw new MessagingException(MessagingException.IOERROR, ioe.toString()); 220 } 221 } 222 223 /** 224 * Lightweight version of SSLCertificateSocketFactory.verifyHostname, which provides this service 225 * but is not in the public API. 226 * 227 * <p>Verify the hostname of the certificate used by the other end of a connected socket. It is 228 * harmless to call this method redundantly if the hostname has already been verified. 229 * 230 * <p>Wildcard certificates are allowed to verify any matching hostname, so "foo.bar.example.com" 231 * is verified if the peer has a certificate for "*.example.com". 232 * 233 * @param socket An SSL socket which has been connected to a server 234 * @param hostname The expected hostname of the remote server 235 * @throws IOException if something goes wrong handshaking with the server 236 * @throws SSLPeerUnverifiedException if the server cannot prove its identity 237 */ verifyHostname(Socket socket, String hostname)238 private void verifyHostname(Socket socket, String hostname) throws IOException { 239 // The code at the start of OpenSSLSocketImpl.startHandshake() 240 // ensures that the call is idempotent, so we can safely call it. 241 SSLSocket ssl = (SSLSocket) socket; 242 ssl.startHandshake(); 243 244 SSLSession session = ssl.getSession(); 245 if (session == null) { 246 mImapHelper.handleEvent(OmtpEvents.DATA_CANNOT_ESTABLISH_SSL_SESSION); 247 throw new SSLException("Cannot verify SSL socket without session"); 248 } 249 // TODO: Instead of reporting the name of the server we think we're connecting to, 250 // we should be reporting the bad name in the certificate. Unfortunately this is buried 251 // in the verifier code and is not available in the verifier API, and extracting the 252 // CN & alts is beyond the scope of this patch. 253 if (!HOSTNAME_VERIFIER.verify(hostname, session)) { 254 mImapHelper.handleEvent(OmtpEvents.DATA_SSL_INVALID_HOST_NAME); 255 throw new SSLPeerUnverifiedException( 256 "Certificate hostname not useable for server: " + session.getPeerPrincipal()); 257 } 258 } 259 isOpen()260 public boolean isOpen() { 261 return (mIn != null 262 && mOut != null 263 && mSocket != null 264 && mSocket.isConnected() 265 && !mSocket.isClosed()); 266 } 267 268 /** Close the connection. MUST NOT return any exceptions - must be "best effort" and safe. */ close()269 public void close() { 270 try { 271 mIn.close(); 272 } catch (Exception e) { 273 // May fail if the connection is already closed. 274 } 275 try { 276 mOut.close(); 277 } catch (Exception e) { 278 // May fail if the connection is already closed. 279 } 280 try { 281 mSocket.close(); 282 } catch (Exception e) { 283 // May fail if the connection is already closed. 284 } 285 mIn = null; 286 mOut = null; 287 mSocket = null; 288 } 289 getHost()290 public String getHost() { 291 return mHost; 292 } 293 getInputStream()294 public InputStream getInputStream() { 295 return mIn; 296 } 297 getOutputStream()298 public OutputStream getOutputStream() { 299 return mOut; 300 } 301 302 /** Writes a single line to the server using \r\n termination. */ writeLine(String s, String sensitiveReplacement)303 public void writeLine(String s, String sensitiveReplacement) throws IOException { 304 if (sensitiveReplacement != null) { 305 LogUtils.d(TAG, ">>> " + sensitiveReplacement); 306 } else { 307 LogUtils.d(TAG, ">>> " + s); 308 } 309 310 OutputStream out = getOutputStream(); 311 out.write(s.getBytes()); 312 out.write('\r'); 313 out.write('\n'); 314 out.flush(); 315 } 316 317 /** 318 * Reads a single line from the server, using either \r\n or \n as the delimiter. The delimiter 319 * char(s) are not included in the result. 320 */ readLine(boolean loggable)321 public String readLine(boolean loggable) throws IOException { 322 StringBuffer sb = new StringBuffer(); 323 InputStream in = getInputStream(); 324 int d; 325 while ((d = in.read()) != -1) { 326 if (((char) d) == '\r') { 327 continue; 328 } else if (((char) d) == '\n') { 329 break; 330 } else { 331 sb.append((char) d); 332 } 333 } 334 if (d == -1) { 335 LogUtils.d(TAG, "End of stream reached while trying to read line."); 336 } 337 String ret = sb.toString(); 338 if (loggable) { 339 LogUtils.d(TAG, "<<< " + ret); 340 } 341 return ret; 342 } 343 } 344