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 android.content.Context; 20 21 import com.android.email2.ui.MailActivityEmail; 22 import com.android.emailcommon.Logging; 23 import com.android.emailcommon.mail.CertificateValidationException; 24 import com.android.emailcommon.mail.MessagingException; 25 import com.android.emailcommon.provider.HostAuth; 26 import com.android.emailcommon.utility.SSLUtils; 27 import com.android.mail.utils.LogUtils; 28 29 import java.io.BufferedInputStream; 30 import java.io.BufferedOutputStream; 31 import java.io.IOException; 32 import java.io.InputStream; 33 import java.io.OutputStream; 34 import java.net.InetAddress; 35 import java.net.InetSocketAddress; 36 import java.net.Socket; 37 import java.net.SocketAddress; 38 import java.net.SocketException; 39 40 import javax.net.ssl.HostnameVerifier; 41 import javax.net.ssl.HttpsURLConnection; 42 import javax.net.ssl.SSLException; 43 import javax.net.ssl.SSLPeerUnverifiedException; 44 import javax.net.ssl.SSLSession; 45 import javax.net.ssl.SSLSocket; 46 47 public class MailTransport { 48 49 // TODO protected eventually 50 /*protected*/ public static final int SOCKET_CONNECT_TIMEOUT = 10000; 51 /*protected*/ public static final int SOCKET_READ_TIMEOUT = 60000; 52 53 private static final HostnameVerifier HOSTNAME_VERIFIER = 54 HttpsURLConnection.getDefaultHostnameVerifier(); 55 56 private final String mDebugLabel; 57 private final Context mContext; 58 protected final HostAuth mHostAuth; 59 60 private Socket mSocket; 61 private InputStream mIn; 62 private OutputStream mOut; 63 64 public MailTransport(Context context, String debugLabel, HostAuth hostAuth) { 65 super(); 66 mContext = context; 67 mDebugLabel = debugLabel; 68 mHostAuth = hostAuth; 69 } 70 71 /** 72 * Returns a new transport, using the current transport as a model. The new transport is 73 * configured identically (as if {@link #setSecurity(int, boolean)}, {@link #setPort(int)} 74 * and {@link #setHost(String)} were invoked), but not opened or connected in any way. 75 */ 76 @Override 77 public MailTransport clone() { 78 return new MailTransport(mContext, mDebugLabel, mHostAuth); 79 } 80 81 public String getHost() { 82 return mHostAuth.mAddress; 83 } 84 85 public int getPort() { 86 return mHostAuth.mPort; 87 } 88 89 public boolean canTrySslSecurity() { 90 return (mHostAuth.mFlags & HostAuth.FLAG_SSL) != 0; 91 } 92 93 public boolean canTryTlsSecurity() { 94 return (mHostAuth.mFlags & HostAuth.FLAG_TLS) != 0; 95 } 96 97 public boolean canTrustAllCertificates() { 98 return (mHostAuth.mFlags & HostAuth.FLAG_TRUST_ALL) != 0; 99 } 100 101 /** 102 * Attempts to open a connection using the Uri supplied for connection parameters. Will attempt 103 * an SSL connection if indicated. 104 */ 105 public void open() throws MessagingException, CertificateValidationException { 106 if (MailActivityEmail.DEBUG) { 107 LogUtils.d(Logging.LOG_TAG, "*** " + mDebugLabel + " open " + 108 getHost() + ":" + String.valueOf(getPort())); 109 } 110 111 try { 112 SocketAddress socketAddress = new InetSocketAddress(getHost(), getPort()); 113 if (canTrySslSecurity()) { 114 mSocket = SSLUtils.getSSLSocketFactory( 115 mContext, mHostAuth, canTrustAllCertificates()).createSocket(); 116 } else { 117 mSocket = new Socket(); 118 } 119 mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT); 120 // After the socket connects to an SSL server, confirm that the hostname is as expected 121 if (canTrySslSecurity() && !canTrustAllCertificates()) { 122 verifyHostname(mSocket, getHost()); 123 } 124 mIn = new BufferedInputStream(mSocket.getInputStream(), 1024); 125 mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512); 126 mSocket.setSoTimeout(SOCKET_READ_TIMEOUT); 127 } catch (SSLException e) { 128 if (MailActivityEmail.DEBUG) { 129 LogUtils.d(Logging.LOG_TAG, e.toString()); 130 } 131 throw new CertificateValidationException(e.getMessage(), e); 132 } catch (IOException ioe) { 133 if (MailActivityEmail.DEBUG) { 134 LogUtils.d(Logging.LOG_TAG, ioe.toString()); 135 } 136 throw new MessagingException(MessagingException.IOERROR, ioe.toString()); 137 } catch (IllegalArgumentException iae) { 138 if (MailActivityEmail.DEBUG) { 139 LogUtils.d(Logging.LOG_TAG, iae.toString()); 140 } 141 throw new MessagingException(MessagingException.UNSPECIFIED_EXCEPTION, iae.toString()); 142 } 143 } 144 145 /** 146 * Attempts to reopen a TLS connection using the Uri supplied for connection parameters. 147 * 148 * NOTE: No explicit hostname verification is required here, because it's handled automatically 149 * by the call to createSocket(). 150 * 151 * TODO should we explicitly close the old socket? This seems funky to abandon it. 152 */ 153 public void reopenTls() throws MessagingException { 154 try { 155 mSocket = SSLUtils.getSSLSocketFactory(mContext, mHostAuth, canTrustAllCertificates()) 156 .createSocket(mSocket, getHost(), getPort(), true); 157 mSocket.setSoTimeout(SOCKET_READ_TIMEOUT); 158 mIn = new BufferedInputStream(mSocket.getInputStream(), 1024); 159 mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512); 160 161 } catch (SSLException e) { 162 if (MailActivityEmail.DEBUG) { 163 LogUtils.d(Logging.LOG_TAG, e.toString()); 164 } 165 throw new CertificateValidationException(e.getMessage(), e); 166 } catch (IOException ioe) { 167 if (MailActivityEmail.DEBUG) { 168 LogUtils.d(Logging.LOG_TAG, ioe.toString()); 169 } 170 throw new MessagingException(MessagingException.IOERROR, ioe.toString()); 171 } 172 } 173 174 /** 175 * Lightweight version of SSLCertificateSocketFactory.verifyHostname, which provides this 176 * service but is not in the public API. 177 * 178 * Verify the hostname of the certificate used by the other end of a 179 * connected socket. You MUST call this if you did not supply a hostname 180 * to SSLCertificateSocketFactory.createSocket(). It is harmless to call this method 181 * redundantly if the hostname has already been verified. 182 * 183 * <p>Wildcard certificates are allowed to verify any matching hostname, 184 * so "foo.bar.example.com" is verified if the peer has a certificate 185 * for "*.example.com". 186 * 187 * @param socket An SSL socket which has been connected to a server 188 * @param hostname The expected hostname of the remote server 189 * @throws IOException if something goes wrong handshaking with the server 190 * @throws SSLPeerUnverifiedException if the server cannot prove its identity 191 */ 192 private static void verifyHostname(Socket socket, String hostname) throws IOException { 193 // The code at the start of OpenSSLSocketImpl.startHandshake() 194 // ensures that the call is idempotent, so we can safely call it. 195 SSLSocket ssl = (SSLSocket) socket; 196 ssl.startHandshake(); 197 198 SSLSession session = ssl.getSession(); 199 if (session == null) { 200 throw new SSLException("Cannot verify SSL socket without session"); 201 } 202 // TODO: Instead of reporting the name of the server we think we're connecting to, 203 // we should be reporting the bad name in the certificate. Unfortunately this is buried 204 // in the verifier code and is not available in the verifier API, and extracting the 205 // CN & alts is beyond the scope of this patch. 206 if (!HOSTNAME_VERIFIER.verify(hostname, session)) { 207 throw new SSLPeerUnverifiedException( 208 "Certificate hostname not useable for server: " + hostname); 209 } 210 } 211 212 /** 213 * Set the socket timeout. 214 * @param timeoutMilliseconds the read timeout value if greater than {@code 0}, or 215 * {@code 0} for an infinite timeout. 216 */ 217 public void setSoTimeout(int timeoutMilliseconds) throws SocketException { 218 mSocket.setSoTimeout(timeoutMilliseconds); 219 } 220 221 public boolean isOpen() { 222 return (mIn != null && mOut != null && 223 mSocket != null && mSocket.isConnected() && !mSocket.isClosed()); 224 } 225 226 /** 227 * Close the connection. MUST NOT return any exceptions - must be "best effort" and safe. 228 */ 229 public void close() { 230 try { 231 mIn.close(); 232 } catch (Exception e) { 233 // May fail if the connection is already closed. 234 } 235 try { 236 mOut.close(); 237 } catch (Exception e) { 238 // May fail if the connection is already closed. 239 } 240 try { 241 mSocket.close(); 242 } catch (Exception e) { 243 // May fail if the connection is already closed. 244 } 245 mIn = null; 246 mOut = null; 247 mSocket = null; 248 } 249 250 public InputStream getInputStream() { 251 return mIn; 252 } 253 254 public OutputStream getOutputStream() { 255 return mOut; 256 } 257 258 /** 259 * Writes a single line to the server using \r\n termination. 260 */ 261 public void writeLine(String s, String sensitiveReplacement) throws IOException { 262 if (MailActivityEmail.DEBUG) { 263 if (sensitiveReplacement != null && !Logging.DEBUG_SENSITIVE) { 264 LogUtils.d(Logging.LOG_TAG, ">>> " + sensitiveReplacement); 265 } else { 266 LogUtils.d(Logging.LOG_TAG, ">>> " + s); 267 } 268 } 269 270 OutputStream out = getOutputStream(); 271 out.write(s.getBytes()); 272 out.write('\r'); 273 out.write('\n'); 274 out.flush(); 275 } 276 277 /** 278 * Reads a single line from the server, using either \r\n or \n as the delimiter. The 279 * delimiter char(s) are not included in the result. 280 */ 281 public String readLine(boolean loggable) throws IOException { 282 StringBuffer sb = new StringBuffer(); 283 InputStream in = getInputStream(); 284 int d; 285 while ((d = in.read()) != -1) { 286 if (((char)d) == '\r') { 287 continue; 288 } else if (((char)d) == '\n') { 289 break; 290 } else { 291 sb.append((char)d); 292 } 293 } 294 if (d == -1 && MailActivityEmail.DEBUG) { 295 LogUtils.d(Logging.LOG_TAG, "End of stream reached while trying to read line."); 296 } 297 String ret = sb.toString(); 298 if (loggable && MailActivityEmail.DEBUG) { 299 LogUtils.d(Logging.LOG_TAG, "<<< " + ret); 300 } 301 return ret; 302 } 303 304 public InetAddress getLocalAddress() { 305 if (isOpen()) { 306 return mSocket.getLocalAddress(); 307 } else { 308 return null; 309 } 310 } 311 } 312