MailTransport.java revision 7d5e2a7c08966ffd4a9e8c78f504cc4fd5be4216
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 } 149 } 150 151 /** 152 * Attempts to reopen a TLS connection using the Uri supplied for connection parameters. 153 * 154 * NOTE: No explicit hostname verification is required here, because it's handled automatically 155 * by the call to createSocket(). 156 * 157 * TODO should we explicitly close the old socket? This seems funky to abandon it. 158 */ 159 @Override 160 public void reopenTls() throws MessagingException { 161 try { 162 mSocket = SSLUtils.getSSLSocketFactory(mContext, mHostAuth, canTrustAllCertificates()) 163 .createSocket(mSocket, getHost(), getPort(), true); 164 mSocket.setSoTimeout(SOCKET_READ_TIMEOUT); 165 mIn = new BufferedInputStream(mSocket.getInputStream(), 1024); 166 mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512); 167 168 } catch (SSLException e) { 169 if (MailActivityEmail.DEBUG) { 170 Log.d(Logging.LOG_TAG, e.toString()); 171 } 172 throw new CertificateValidationException(e.getMessage(), e); 173 } catch (IOException ioe) { 174 if (MailActivityEmail.DEBUG) { 175 Log.d(Logging.LOG_TAG, ioe.toString()); 176 } 177 throw new MessagingException(MessagingException.IOERROR, ioe.toString()); 178 } 179 } 180 181 /** 182 * Lightweight version of SSLCertificateSocketFactory.verifyHostname, which provides this 183 * service but is not in the public API. 184 * 185 * Verify the hostname of the certificate used by the other end of a 186 * connected socket. You MUST call this if you did not supply a hostname 187 * to SSLCertificateSocketFactory.createSocket(). It is harmless to call this method 188 * redundantly if the hostname has already been verified. 189 * 190 * <p>Wildcard certificates are allowed to verify any matching hostname, 191 * so "foo.bar.example.com" is verified if the peer has a certificate 192 * for "*.example.com". 193 * 194 * @param socket An SSL socket which has been connected to a server 195 * @param hostname The expected hostname of the remote server 196 * @throws IOException if something goes wrong handshaking with the server 197 * @throws SSLPeerUnverifiedException if the server cannot prove its identity 198 */ 199 private void verifyHostname(Socket socket, String hostname) throws IOException { 200 // The code at the start of OpenSSLSocketImpl.startHandshake() 201 // ensures that the call is idempotent, so we can safely call it. 202 SSLSocket ssl = (SSLSocket) socket; 203 ssl.startHandshake(); 204 205 SSLSession session = ssl.getSession(); 206 if (session == null) { 207 throw new SSLException("Cannot verify SSL socket without session"); 208 } 209 // TODO: Instead of reporting the name of the server we think we're connecting to, 210 // we should be reporting the bad name in the certificate. Unfortunately this is buried 211 // in the verifier code and is not available in the verifier API, and extracting the 212 // CN & alts is beyond the scope of this patch. 213 if (!HOSTNAME_VERIFIER.verify(hostname, session)) { 214 throw new SSLPeerUnverifiedException( 215 "Certificate hostname not useable for server: " + hostname); 216 } 217 } 218 219 /** 220 * Set the socket timeout. 221 * @param timeoutMilliseconds the read timeout value if greater than {@code 0}, or 222 * {@code 0} for an infinite timeout. 223 */ 224 @Override 225 public void setSoTimeout(int timeoutMilliseconds) throws SocketException { 226 mSocket.setSoTimeout(timeoutMilliseconds); 227 } 228 229 @Override 230 public boolean isOpen() { 231 return (mIn != null && mOut != null && 232 mSocket != null && mSocket.isConnected() && !mSocket.isClosed()); 233 } 234 235 /** 236 * Close the connection. MUST NOT return any exceptions - must be "best effort" and safe. 237 */ 238 @Override 239 public void close() { 240 try { 241 mIn.close(); 242 } catch (Exception e) { 243 // May fail if the connection is already closed. 244 } 245 try { 246 mOut.close(); 247 } catch (Exception e) { 248 // May fail if the connection is already closed. 249 } 250 try { 251 mSocket.close(); 252 } catch (Exception e) { 253 // May fail if the connection is already closed. 254 } 255 mIn = null; 256 mOut = null; 257 mSocket = null; 258 } 259 260 @Override 261 public InputStream getInputStream() { 262 return mIn; 263 } 264 265 @Override 266 public OutputStream getOutputStream() { 267 return mOut; 268 } 269 270 /** 271 * Writes a single line to the server using \r\n termination. 272 */ 273 @Override 274 public void writeLine(String s, String sensitiveReplacement) throws IOException { 275 if (MailActivityEmail.DEBUG) { 276 if (sensitiveReplacement != null && !Logging.DEBUG_SENSITIVE) { 277 Log.d(Logging.LOG_TAG, ">>> " + sensitiveReplacement); 278 } else { 279 Log.d(Logging.LOG_TAG, ">>> " + s); 280 } 281 } 282 283 OutputStream out = getOutputStream(); 284 out.write(s.getBytes()); 285 out.write('\r'); 286 out.write('\n'); 287 out.flush(); 288 } 289 290 /** 291 * Reads a single line from the server, using either \r\n or \n as the delimiter. The 292 * delimiter char(s) are not included in the result. 293 */ 294 @Override 295 public String readLine() throws IOException { 296 StringBuffer sb = new StringBuffer(); 297 InputStream in = getInputStream(); 298 int d; 299 while ((d = in.read()) != -1) { 300 if (((char)d) == '\r') { 301 continue; 302 } else if (((char)d) == '\n') { 303 break; 304 } else { 305 sb.append((char)d); 306 } 307 } 308 if (d == -1 && MailActivityEmail.DEBUG) { 309 Log.d(Logging.LOG_TAG, "End of stream reached while trying to read line."); 310 } 311 String ret = sb.toString(); 312 if (MailActivityEmail.DEBUG) { 313 Log.d(Logging.LOG_TAG, "<<< " + ret); 314 } 315 return ret; 316 } 317 318 @Override 319 public InetAddress getLocalAddress() { 320 if (isOpen()) { 321 return mSocket.getLocalAddress(); 322 } else { 323 return null; 324 } 325 } 326} 327