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