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