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