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