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(canTrustAllCertificates()).createSocket(); 171 } else { 172 mSocket = new Socket(); 173 } 174 mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT); 175 // After the socket connects to an SSL server, confirm that the hostname is as expected 176 if (canTrySslSecurity() && !canTrustAllCertificates()) { 177 verifyHostname(mSocket, getHost()); 178 } 179 mIn = new BufferedInputStream(mSocket.getInputStream(), 1024); 180 mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512); 181 182 } catch (SSLException e) { 183 if (Email.DEBUG) { 184 Log.d(Logging.LOG_TAG, e.toString()); 185 } 186 throw new CertificateValidationException(e.getMessage(), e); 187 } catch (IOException ioe) { 188 if (Email.DEBUG) { 189 Log.d(Logging.LOG_TAG, ioe.toString()); 190 } 191 throw new MessagingException(MessagingException.IOERROR, ioe.toString()); 192 } 193 } 194 195 /** 196 * Attempts to reopen a TLS connection using the Uri supplied for connection parameters. 197 * 198 * NOTE: No explicit hostname verification is required here, because it's handled automatically 199 * by the call to createSocket(). 200 * 201 * TODO should we explicitly close the old socket? This seems funky to abandon it. 202 */ 203 @Override 204 public void reopenTls() throws MessagingException { 205 try { 206 mSocket = SSLUtils.getSSLSocketFactory(canTrustAllCertificates()) 207 .createSocket(mSocket, getHost(), getPort(), true); 208 mSocket.setSoTimeout(SOCKET_READ_TIMEOUT); 209 mIn = new BufferedInputStream(mSocket.getInputStream(), 1024); 210 mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512); 211 212 } catch (SSLException e) { 213 if (Email.DEBUG) { 214 Log.d(Logging.LOG_TAG, e.toString()); 215 } 216 throw new CertificateValidationException(e.getMessage(), e); 217 } catch (IOException ioe) { 218 if (Email.DEBUG) { 219 Log.d(Logging.LOG_TAG, ioe.toString()); 220 } 221 throw new MessagingException(MessagingException.IOERROR, ioe.toString()); 222 } 223 } 224 225 /** 226 * Lightweight version of SSLCertificateSocketFactory.verifyHostname, which provides this 227 * service but is not in the public API. 228 * 229 * Verify the hostname of the certificate used by the other end of a 230 * connected socket. You MUST call this if you did not supply a hostname 231 * to SSLCertificateSocketFactory.createSocket(). It is harmless to call this method 232 * redundantly if the hostname has already been verified. 233 * 234 * <p>Wildcard certificates are allowed to verify any matching hostname, 235 * so "foo.bar.example.com" is verified if the peer has a certificate 236 * for "*.example.com". 237 * 238 * @param socket An SSL socket which has been connected to a server 239 * @param hostname The expected hostname of the remote server 240 * @throws IOException if something goes wrong handshaking with the server 241 * @throws SSLPeerUnverifiedException if the server cannot prove its identity 242 */ 243 private void verifyHostname(Socket socket, String hostname) throws IOException { 244 // The code at the start of OpenSSLSocketImpl.startHandshake() 245 // ensures that the call is idempotent, so we can safely call it. 246 SSLSocket ssl = (SSLSocket) socket; 247 ssl.startHandshake(); 248 249 SSLSession session = ssl.getSession(); 250 if (session == null) { 251 throw new SSLException("Cannot verify SSL socket without session"); 252 } 253 // TODO: Instead of reporting the name of the server we think we're connecting to, 254 // we should be reporting the bad name in the certificate. Unfortunately this is buried 255 // in the verifier code and is not available in the verifier API, and extracting the 256 // CN & alts is beyond the scope of this patch. 257 if (!HOSTNAME_VERIFIER.verify(hostname, session)) { 258 throw new SSLPeerUnverifiedException( 259 "Certificate hostname not useable for server: " + hostname); 260 } 261 } 262 263 /** 264 * Set the socket timeout. 265 * @param timeoutMilliseconds the read timeout value if greater than {@code 0}, or 266 * {@code 0} for an infinite timeout. 267 */ 268 @Override 269 public void setSoTimeout(int timeoutMilliseconds) throws SocketException { 270 mSocket.setSoTimeout(timeoutMilliseconds); 271 } 272 273 @Override 274 public boolean isOpen() { 275 return (mIn != null && mOut != null && 276 mSocket != null && mSocket.isConnected() && !mSocket.isClosed()); 277 } 278 279 /** 280 * Close the connection. MUST NOT return any exceptions - must be "best effort" and safe. 281 */ 282 @Override 283 public void close() { 284 try { 285 mIn.close(); 286 } catch (Exception e) { 287 // May fail if the connection is already closed. 288 } 289 try { 290 mOut.close(); 291 } catch (Exception e) { 292 // May fail if the connection is already closed. 293 } 294 try { 295 mSocket.close(); 296 } catch (Exception e) { 297 // May fail if the connection is already closed. 298 } 299 mIn = null; 300 mOut = null; 301 mSocket = null; 302 } 303 304 @Override 305 public InputStream getInputStream() { 306 return mIn; 307 } 308 309 @Override 310 public OutputStream getOutputStream() { 311 return mOut; 312 } 313 314 /** 315 * Writes a single line to the server using \r\n termination. 316 */ 317 @Override 318 public void writeLine(String s, String sensitiveReplacement) throws IOException { 319 if (Email.DEBUG) { 320 if (sensitiveReplacement != null && !Logging.DEBUG_SENSITIVE) { 321 Log.d(Logging.LOG_TAG, ">>> " + sensitiveReplacement); 322 } else { 323 Log.d(Logging.LOG_TAG, ">>> " + s); 324 } 325 } 326 327 OutputStream out = getOutputStream(); 328 out.write(s.getBytes()); 329 out.write('\r'); 330 out.write('\n'); 331 out.flush(); 332 } 333 334 /** 335 * Reads a single line from the server, using either \r\n or \n as the delimiter. The 336 * delimiter char(s) are not included in the result. 337 */ 338 @Override 339 public String readLine() throws IOException { 340 StringBuffer sb = new StringBuffer(); 341 InputStream in = getInputStream(); 342 int d; 343 while ((d = in.read()) != -1) { 344 if (((char)d) == '\r') { 345 continue; 346 } else if (((char)d) == '\n') { 347 break; 348 } else { 349 sb.append((char)d); 350 } 351 } 352 if (d == -1 && Email.DEBUG) { 353 Log.d(Logging.LOG_TAG, "End of stream reached while trying to read line."); 354 } 355 String ret = sb.toString(); 356 if (Email.DEBUG) { 357 Log.d(Logging.LOG_TAG, "<<< " + ret); 358 } 359 return ret; 360 } 361 362 @Override 363 public InetAddress getLocalAddress() { 364 if (isOpen()) { 365 return mSocket.getLocalAddress(); 366 } else { 367 return null; 368 } 369 } 370 } 371