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