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