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