| /* |
| * Copyright (C) 2008 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.email.mail.transport; |
| |
| import android.content.Context; |
| |
| import com.android.email.DebugUtils; |
| import com.android.emailcommon.Logging; |
| import com.android.emailcommon.mail.CertificateValidationException; |
| import com.android.emailcommon.mail.MessagingException; |
| import com.android.emailcommon.provider.HostAuth; |
| import com.android.emailcommon.utility.SSLUtils; |
| import com.android.mail.analytics.Analytics; |
| import com.android.mail.utils.LogUtils; |
| |
| import java.io.BufferedInputStream; |
| import java.io.BufferedOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.net.InetAddress; |
| import java.net.InetSocketAddress; |
| import java.net.Socket; |
| import java.net.SocketAddress; |
| import java.net.SocketException; |
| |
| import javax.net.ssl.HostnameVerifier; |
| import javax.net.ssl.HttpsURLConnection; |
| import javax.net.ssl.SSLException; |
| import javax.net.ssl.SSLPeerUnverifiedException; |
| import javax.net.ssl.SSLSession; |
| import javax.net.ssl.SSLSocket; |
| |
| public class MailTransport { |
| |
| // TODO protected eventually |
| /*protected*/ public static final int SOCKET_CONNECT_TIMEOUT = 10000; |
| /*protected*/ public static final int SOCKET_READ_TIMEOUT = 60000; |
| |
| private static final HostnameVerifier HOSTNAME_VERIFIER = |
| HttpsURLConnection.getDefaultHostnameVerifier(); |
| |
| private final String mDebugLabel; |
| private final Context mContext; |
| protected final HostAuth mHostAuth; |
| |
| private Socket mSocket; |
| private InputStream mIn; |
| private OutputStream mOut; |
| |
| public MailTransport(Context context, String debugLabel, HostAuth hostAuth) { |
| super(); |
| mContext = context; |
| mDebugLabel = debugLabel; |
| mHostAuth = hostAuth; |
| } |
| |
| /** |
| * Returns a new transport, using the current transport as a model. The new transport is |
| * configured identically (as if {@link #setSecurity(int, boolean)}, {@link #setPort(int)} |
| * and {@link #setHost(String)} were invoked), but not opened or connected in any way. |
| */ |
| @Override |
| public MailTransport clone() { |
| return new MailTransport(mContext, mDebugLabel, mHostAuth); |
| } |
| |
| public String getHost() { |
| return mHostAuth.mAddress; |
| } |
| |
| public int getPort() { |
| return mHostAuth.mPort; |
| } |
| |
| public boolean canTrySslSecurity() { |
| return (mHostAuth.mFlags & HostAuth.FLAG_SSL) != 0; |
| } |
| |
| public boolean canTryTlsSecurity() { |
| return (mHostAuth.mFlags & HostAuth.FLAG_TLS) != 0; |
| } |
| |
| public boolean canTrustAllCertificates() { |
| return (mHostAuth.mFlags & HostAuth.FLAG_TRUST_ALL) != 0; |
| } |
| |
| /** |
| * Attempts to open a connection using the Uri supplied for connection parameters. Will attempt |
| * an SSL connection if indicated. |
| */ |
| public void open() throws MessagingException, CertificateValidationException { |
| if (DebugUtils.DEBUG) { |
| LogUtils.d(Logging.LOG_TAG, "*** " + mDebugLabel + " open " + |
| getHost() + ":" + String.valueOf(getPort())); |
| } |
| |
| try { |
| SocketAddress socketAddress = new InetSocketAddress(getHost(), getPort()); |
| if (canTrySslSecurity()) { |
| mSocket = SSLUtils.getSSLSocketFactory( |
| mContext, mHostAuth, null, canTrustAllCertificates()).createSocket(); |
| } else { |
| mSocket = new Socket(); |
| } |
| mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT); |
| // After the socket connects to an SSL server, confirm that the hostname is as expected |
| if (canTrySslSecurity() && !canTrustAllCertificates()) { |
| verifyHostname(mSocket, getHost()); |
| } |
| Analytics.getInstance().sendEvent("socket_certificates", |
| "open", Boolean.toString(canTrustAllCertificates()), 0); |
| if (mSocket instanceof SSLSocket) { |
| final SSLSocket sslSocket = (SSLSocket) mSocket; |
| if (sslSocket.getSession() != null) { |
| Analytics.getInstance().sendEvent("cipher_suite", |
| sslSocket.getSession().getProtocol(), |
| sslSocket.getSession().getCipherSuite(), 0); |
| } |
| } |
| mIn = new BufferedInputStream(mSocket.getInputStream(), 1024); |
| mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512); |
| mSocket.setSoTimeout(SOCKET_READ_TIMEOUT); |
| } catch (SSLException e) { |
| if (DebugUtils.DEBUG) { |
| LogUtils.d(Logging.LOG_TAG, e.toString()); |
| } |
| throw new CertificateValidationException(e.getMessage(), e); |
| } catch (IOException ioe) { |
| if (DebugUtils.DEBUG) { |
| LogUtils.d(Logging.LOG_TAG, ioe.toString()); |
| } |
| throw new MessagingException(MessagingException.IOERROR, ioe.toString()); |
| } catch (IllegalArgumentException iae) { |
| if (DebugUtils.DEBUG) { |
| LogUtils.d(Logging.LOG_TAG, iae.toString()); |
| } |
| throw new MessagingException(MessagingException.UNSPECIFIED_EXCEPTION, iae.toString()); |
| } |
| } |
| |
| /** |
| * Attempts to reopen a TLS connection using the Uri supplied for connection parameters. |
| * |
| * NOTE: No explicit hostname verification is required here, because it's handled automatically |
| * by the call to createSocket(). |
| * |
| * TODO should we explicitly close the old socket? This seems funky to abandon it. |
| */ |
| public void reopenTls() throws MessagingException { |
| try { |
| mSocket = SSLUtils.getSSLSocketFactory(mContext, mHostAuth, null, |
| canTrustAllCertificates()) |
| .createSocket(mSocket, getHost(), getPort(), true); |
| mSocket.setSoTimeout(SOCKET_READ_TIMEOUT); |
| mIn = new BufferedInputStream(mSocket.getInputStream(), 1024); |
| mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512); |
| |
| Analytics.getInstance().sendEvent("socket_certificates", |
| "reopenTls", Boolean.toString(canTrustAllCertificates()), 0); |
| final SSLSocket sslSocket = (SSLSocket) mSocket; |
| if (sslSocket.getSession() != null) { |
| Analytics.getInstance().sendEvent("cipher_suite", |
| sslSocket.getSession().getProtocol(), |
| sslSocket.getSession().getCipherSuite(), 0); |
| } |
| } catch (SSLException e) { |
| if (DebugUtils.DEBUG) { |
| LogUtils.d(Logging.LOG_TAG, e.toString()); |
| } |
| throw new CertificateValidationException(e.getMessage(), e); |
| } catch (IOException ioe) { |
| if (DebugUtils.DEBUG) { |
| LogUtils.d(Logging.LOG_TAG, ioe.toString()); |
| } |
| throw new MessagingException(MessagingException.IOERROR, ioe.toString()); |
| } |
| } |
| |
| /** |
| * Lightweight version of SSLCertificateSocketFactory.verifyHostname, which provides this |
| * service but is not in the public API. |
| * |
| * Verify the hostname of the certificate used by the other end of a |
| * connected socket. You MUST call this if you did not supply a hostname |
| * to SSLCertificateSocketFactory.createSocket(). It is harmless to call this method |
| * redundantly if the hostname has already been verified. |
| * |
| * <p>Wildcard certificates are allowed to verify any matching hostname, |
| * so "foo.bar.example.com" is verified if the peer has a certificate |
| * for "*.example.com". |
| * |
| * @param socket An SSL socket which has been connected to a server |
| * @param hostname The expected hostname of the remote server |
| * @throws IOException if something goes wrong handshaking with the server |
| * @throws SSLPeerUnverifiedException if the server cannot prove its identity |
| */ |
| private static void verifyHostname(Socket socket, String hostname) throws IOException { |
| // The code at the start of OpenSSLSocketImpl.startHandshake() |
| // ensures that the call is idempotent, so we can safely call it. |
| SSLSocket ssl = (SSLSocket) socket; |
| ssl.startHandshake(); |
| |
| SSLSession session = ssl.getSession(); |
| if (session == null) { |
| throw new SSLException("Cannot verify SSL socket without session"); |
| } |
| // TODO: Instead of reporting the name of the server we think we're connecting to, |
| // we should be reporting the bad name in the certificate. Unfortunately this is buried |
| // in the verifier code and is not available in the verifier API, and extracting the |
| // CN & alts is beyond the scope of this patch. |
| if (!HOSTNAME_VERIFIER.verify(hostname, session)) { |
| throw new SSLPeerUnverifiedException( |
| "Certificate hostname not useable for server: " + hostname); |
| } |
| } |
| |
| /** |
| * Get the socket timeout. |
| * @return the read timeout value in milliseconds |
| * @throws SocketException |
| */ |
| public int getSoTimeout() throws SocketException { |
| return mSocket.getSoTimeout(); |
| } |
| |
| /** |
| * Set the socket timeout. |
| * @param timeoutMilliseconds the read timeout value if greater than {@code 0}, or |
| * {@code 0} for an infinite timeout. |
| */ |
| public void setSoTimeout(int timeoutMilliseconds) throws SocketException { |
| mSocket.setSoTimeout(timeoutMilliseconds); |
| } |
| |
| public boolean isOpen() { |
| return (mIn != null && mOut != null && |
| mSocket != null && mSocket.isConnected() && !mSocket.isClosed()); |
| } |
| |
| /** |
| * Close the connection. MUST NOT return any exceptions - must be "best effort" and safe. |
| */ |
| public void close() { |
| try { |
| mIn.close(); |
| } catch (Exception e) { |
| // May fail if the connection is already closed. |
| } |
| try { |
| mOut.close(); |
| } catch (Exception e) { |
| // May fail if the connection is already closed. |
| } |
| try { |
| mSocket.close(); |
| } catch (Exception e) { |
| // May fail if the connection is already closed. |
| } |
| mIn = null; |
| mOut = null; |
| mSocket = null; |
| } |
| |
| public InputStream getInputStream() { |
| return mIn; |
| } |
| |
| public OutputStream getOutputStream() { |
| return mOut; |
| } |
| |
| /** |
| * Writes a single line to the server using \r\n termination. |
| */ |
| public void writeLine(String s, String sensitiveReplacement) throws IOException { |
| if (DebugUtils.DEBUG) { |
| if (sensitiveReplacement != null && !Logging.DEBUG_SENSITIVE) { |
| LogUtils.d(Logging.LOG_TAG, ">>> " + sensitiveReplacement); |
| } else { |
| LogUtils.d(Logging.LOG_TAG, ">>> " + s); |
| } |
| } |
| |
| OutputStream out = getOutputStream(); |
| out.write(s.getBytes()); |
| out.write('\r'); |
| out.write('\n'); |
| out.flush(); |
| } |
| |
| /** |
| * Reads a single line from the server, using either \r\n or \n as the delimiter. The |
| * delimiter char(s) are not included in the result. |
| */ |
| public String readLine(boolean loggable) throws IOException { |
| StringBuffer sb = new StringBuffer(); |
| InputStream in = getInputStream(); |
| int d; |
| while ((d = in.read()) != -1) { |
| if (((char)d) == '\r') { |
| continue; |
| } else if (((char)d) == '\n') { |
| break; |
| } else { |
| sb.append((char)d); |
| } |
| } |
| if (d == -1 && DebugUtils.DEBUG) { |
| LogUtils.d(Logging.LOG_TAG, "End of stream reached while trying to read line."); |
| } |
| String ret = sb.toString(); |
| if (loggable && DebugUtils.DEBUG) { |
| LogUtils.d(Logging.LOG_TAG, "<<< " + ret); |
| } |
| return ret; |
| } |
| |
| public InetAddress getLocalAddress() { |
| if (isOpen()) { |
| return mSocket.getLocalAddress(); |
| } else { |
| return null; |
| } |
| } |
| } |