1 /* 2 * Copyright (C) 2008 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 17 package com.android.email.mail.transport; 18 19 import android.content.Context; 20 21 import com.android.email.DebugUtils; 22 import com.android.emailcommon.Logging; 23 import com.android.emailcommon.mail.CertificateValidationException; 24 import com.android.emailcommon.mail.MessagingException; 25 import com.android.emailcommon.provider.HostAuth; 26 import com.android.emailcommon.utility.SSLUtils; 27 import com.android.mail.analytics.Analytics; 28 import com.android.mail.utils.LogUtils; 29 30 import java.io.BufferedInputStream; 31 import java.io.BufferedOutputStream; 32 import java.io.IOException; 33 import java.io.InputStream; 34 import java.io.OutputStream; 35 import java.net.InetAddress; 36 import java.net.InetSocketAddress; 37 import java.net.Socket; 38 import java.net.SocketAddress; 39 import java.net.SocketException; 40 41 import javax.net.ssl.HostnameVerifier; 42 import javax.net.ssl.HttpsURLConnection; 43 import javax.net.ssl.SSLException; 44 import javax.net.ssl.SSLPeerUnverifiedException; 45 import javax.net.ssl.SSLSession; 46 import javax.net.ssl.SSLSocket; 47 48 public class MailTransport { 49 50 // TODO protected eventually 51 /*protected*/ public static final int SOCKET_CONNECT_TIMEOUT = 10000; 52 /*protected*/ public static final int SOCKET_READ_TIMEOUT = 60000; 53 54 private static final HostnameVerifier HOSTNAME_VERIFIER = 55 HttpsURLConnection.getDefaultHostnameVerifier(); 56 57 private final String mDebugLabel; 58 private final Context mContext; 59 protected final HostAuth mHostAuth; 60 61 private Socket mSocket; 62 private InputStream mIn; 63 private OutputStream mOut; 64 MailTransport(Context context, String debugLabel, HostAuth hostAuth)65 public MailTransport(Context context, String debugLabel, HostAuth hostAuth) { 66 super(); 67 mContext = context; 68 mDebugLabel = debugLabel; 69 mHostAuth = hostAuth; 70 } 71 72 /** 73 * Returns a new transport, using the current transport as a model. The new transport is 74 * configured identically (as if {@link #setSecurity(int, boolean)}, {@link #setPort(int)} 75 * and {@link #setHost(String)} were invoked), but not opened or connected in any way. 76 */ 77 @Override clone()78 public MailTransport clone() { 79 return new MailTransport(mContext, mDebugLabel, mHostAuth); 80 } 81 getHost()82 public String getHost() { 83 return mHostAuth.mAddress; 84 } 85 getPort()86 public int getPort() { 87 return mHostAuth.mPort; 88 } 89 canTrySslSecurity()90 public boolean canTrySslSecurity() { 91 return (mHostAuth.mFlags & HostAuth.FLAG_SSL) != 0; 92 } 93 canTryTlsSecurity()94 public boolean canTryTlsSecurity() { 95 return (mHostAuth.mFlags & HostAuth.FLAG_TLS) != 0; 96 } 97 canTrustAllCertificates()98 public boolean canTrustAllCertificates() { 99 return (mHostAuth.mFlags & HostAuth.FLAG_TRUST_ALL) != 0; 100 } 101 102 /** 103 * Attempts to open a connection using the Uri supplied for connection parameters. Will attempt 104 * an SSL connection if indicated. 105 */ open()106 public void open() throws MessagingException, CertificateValidationException { 107 if (DebugUtils.DEBUG) { 108 LogUtils.d(Logging.LOG_TAG, "*** " + mDebugLabel + " open " + 109 getHost() + ":" + String.valueOf(getPort())); 110 } 111 112 try { 113 SocketAddress socketAddress = new InetSocketAddress(getHost(), getPort()); 114 if (canTrySslSecurity()) { 115 mSocket = SSLUtils.getSSLSocketFactory( 116 mContext, mHostAuth, null, canTrustAllCertificates()).createSocket(); 117 } else { 118 mSocket = new Socket(); 119 } 120 mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT); 121 // After the socket connects to an SSL server, confirm that the hostname is as expected 122 if (canTrySslSecurity() && !canTrustAllCertificates()) { 123 verifyHostname(mSocket, getHost()); 124 } 125 Analytics.getInstance().sendEvent("socket_certificates", 126 "open", Boolean.toString(canTrustAllCertificates()), 0); 127 if (mSocket instanceof SSLSocket) { 128 final SSLSocket sslSocket = (SSLSocket) mSocket; 129 if (sslSocket.getSession() != null) { 130 Analytics.getInstance().sendEvent("cipher_suite", 131 sslSocket.getSession().getProtocol(), 132 sslSocket.getSession().getCipherSuite(), 0); 133 } 134 } 135 mIn = new BufferedInputStream(mSocket.getInputStream(), 1024); 136 mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512); 137 mSocket.setSoTimeout(SOCKET_READ_TIMEOUT); 138 } catch (SSLException e) { 139 if (DebugUtils.DEBUG) { 140 LogUtils.d(Logging.LOG_TAG, e.toString()); 141 } 142 throw new CertificateValidationException(e.getMessage(), e); 143 } catch (IOException ioe) { 144 if (DebugUtils.DEBUG) { 145 LogUtils.d(Logging.LOG_TAG, ioe.toString()); 146 } 147 throw new MessagingException(MessagingException.IOERROR, ioe.toString()); 148 } catch (IllegalArgumentException iae) { 149 if (DebugUtils.DEBUG) { 150 LogUtils.d(Logging.LOG_TAG, iae.toString()); 151 } 152 throw new MessagingException(MessagingException.UNSPECIFIED_EXCEPTION, iae.toString()); 153 } 154 } 155 156 /** 157 * Attempts to reopen a TLS connection using the Uri supplied for connection parameters. 158 * 159 * NOTE: No explicit hostname verification is required here, because it's handled automatically 160 * by the call to createSocket(). 161 * 162 * TODO should we explicitly close the old socket? This seems funky to abandon it. 163 */ reopenTls()164 public void reopenTls() throws MessagingException { 165 try { 166 mSocket = SSLUtils.getSSLSocketFactory(mContext, mHostAuth, null, 167 canTrustAllCertificates()) 168 .createSocket(mSocket, getHost(), getPort(), true); 169 mSocket.setSoTimeout(SOCKET_READ_TIMEOUT); 170 mIn = new BufferedInputStream(mSocket.getInputStream(), 1024); 171 mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512); 172 173 Analytics.getInstance().sendEvent("socket_certificates", 174 "reopenTls", Boolean.toString(canTrustAllCertificates()), 0); 175 final SSLSocket sslSocket = (SSLSocket) mSocket; 176 if (sslSocket.getSession() != null) { 177 Analytics.getInstance().sendEvent("cipher_suite", 178 sslSocket.getSession().getProtocol(), 179 sslSocket.getSession().getCipherSuite(), 0); 180 } 181 } catch (SSLException e) { 182 if (DebugUtils.DEBUG) { 183 LogUtils.d(Logging.LOG_TAG, e.toString()); 184 } 185 throw new CertificateValidationException(e.getMessage(), e); 186 } catch (IOException ioe) { 187 if (DebugUtils.DEBUG) { 188 LogUtils.d(Logging.LOG_TAG, ioe.toString()); 189 } 190 throw new MessagingException(MessagingException.IOERROR, ioe.toString()); 191 } 192 } 193 194 /** 195 * Lightweight version of SSLCertificateSocketFactory.verifyHostname, which provides this 196 * service but is not in the public API. 197 * 198 * Verify the hostname of the certificate used by the other end of a 199 * connected socket. You MUST call this if you did not supply a hostname 200 * to SSLCertificateSocketFactory.createSocket(). It is harmless to call this method 201 * redundantly if the hostname has already been verified. 202 * 203 * <p>Wildcard certificates are allowed to verify any matching hostname, 204 * so "foo.bar.example.com" is verified if the peer has a certificate 205 * for "*.example.com". 206 * 207 * @param socket An SSL socket which has been connected to a server 208 * @param hostname The expected hostname of the remote server 209 * @throws IOException if something goes wrong handshaking with the server 210 * @throws SSLPeerUnverifiedException if the server cannot prove its identity 211 */ verifyHostname(Socket socket, String hostname)212 private static void verifyHostname(Socket socket, String hostname) throws IOException { 213 // The code at the start of OpenSSLSocketImpl.startHandshake() 214 // ensures that the call is idempotent, so we can safely call it. 215 SSLSocket ssl = (SSLSocket) socket; 216 ssl.startHandshake(); 217 218 SSLSession session = ssl.getSession(); 219 if (session == null) { 220 throw new SSLException("Cannot verify SSL socket without session"); 221 } 222 // TODO: Instead of reporting the name of the server we think we're connecting to, 223 // we should be reporting the bad name in the certificate. Unfortunately this is buried 224 // in the verifier code and is not available in the verifier API, and extracting the 225 // CN & alts is beyond the scope of this patch. 226 if (!HOSTNAME_VERIFIER.verify(hostname, session)) { 227 throw new SSLPeerUnverifiedException( 228 "Certificate hostname not useable for server: " + hostname); 229 } 230 } 231 232 /** 233 * Get the socket timeout. 234 * @return the read timeout value in milliseconds 235 * @throws SocketException 236 */ getSoTimeout()237 public int getSoTimeout() throws SocketException { 238 return mSocket.getSoTimeout(); 239 } 240 241 /** 242 * Set the socket timeout. 243 * @param timeoutMilliseconds the read timeout value if greater than {@code 0}, or 244 * {@code 0} for an infinite timeout. 245 */ setSoTimeout(int timeoutMilliseconds)246 public void setSoTimeout(int timeoutMilliseconds) throws SocketException { 247 mSocket.setSoTimeout(timeoutMilliseconds); 248 } 249 isOpen()250 public boolean isOpen() { 251 return (mIn != null && mOut != null && 252 mSocket != null && mSocket.isConnected() && !mSocket.isClosed()); 253 } 254 255 /** 256 * Close the connection. MUST NOT return any exceptions - must be "best effort" and safe. 257 */ close()258 public void close() { 259 try { 260 mIn.close(); 261 } catch (Exception e) { 262 // May fail if the connection is already closed. 263 } 264 try { 265 mOut.close(); 266 } catch (Exception e) { 267 // May fail if the connection is already closed. 268 } 269 try { 270 mSocket.close(); 271 } catch (Exception e) { 272 // May fail if the connection is already closed. 273 } 274 mIn = null; 275 mOut = null; 276 mSocket = null; 277 } 278 getInputStream()279 public InputStream getInputStream() { 280 return mIn; 281 } 282 getOutputStream()283 public OutputStream getOutputStream() { 284 return mOut; 285 } 286 287 /** 288 * Writes a single line to the server using \r\n termination. 289 */ writeLine(String s, String sensitiveReplacement)290 public void writeLine(String s, String sensitiveReplacement) throws IOException { 291 if (DebugUtils.DEBUG) { 292 if (sensitiveReplacement != null && !Logging.DEBUG_SENSITIVE) { 293 LogUtils.d(Logging.LOG_TAG, ">>> " + sensitiveReplacement); 294 } else { 295 LogUtils.d(Logging.LOG_TAG, ">>> " + s); 296 } 297 } 298 299 OutputStream out = getOutputStream(); 300 out.write(s.getBytes()); 301 out.write('\r'); 302 out.write('\n'); 303 out.flush(); 304 } 305 306 /** 307 * Reads a single line from the server, using either \r\n or \n as the delimiter. The 308 * delimiter char(s) are not included in the result. 309 */ readLine(boolean loggable)310 public String readLine(boolean loggable) throws IOException { 311 StringBuffer sb = new StringBuffer(); 312 InputStream in = getInputStream(); 313 int d; 314 while ((d = in.read()) != -1) { 315 if (((char)d) == '\r') { 316 continue; 317 } else if (((char)d) == '\n') { 318 break; 319 } else { 320 sb.append((char)d); 321 } 322 } 323 if (d == -1 && DebugUtils.DEBUG) { 324 LogUtils.d(Logging.LOG_TAG, "End of stream reached while trying to read line."); 325 } 326 String ret = sb.toString(); 327 if (loggable && DebugUtils.DEBUG) { 328 LogUtils.d(Logging.LOG_TAG, "<<< " + ret); 329 } 330 return ret; 331 } 332 getLocalAddress()333 public InetAddress getLocalAddress() { 334 if (isOpen()) { 335 return mSocket.getLocalAddress(); 336 } else { 337 return null; 338 } 339 } 340 } 341