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