1 /* 2 * Copyright (C) 2015 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 package com.android.phone.common.mail; 17 18 import android.content.Context; 19 import android.net.Network; 20 21 import com.android.phone.common.mail.store.ImapStore; 22 import com.android.phone.common.mail.utils.LogUtils; 23 24 import java.net.SocketAddress; 25 import java.util.ArrayList; 26 import java.util.List; 27 28 import javax.net.ssl.HostnameVerifier; 29 import javax.net.ssl.HttpsURLConnection; 30 import javax.net.ssl.SSLException; 31 import javax.net.ssl.SSLPeerUnverifiedException; 32 import javax.net.ssl.SSLSession; 33 import javax.net.ssl.SSLSocket; 34 35 import java.io.BufferedInputStream; 36 import java.io.BufferedOutputStream; 37 import java.io.IOException; 38 import java.io.InputStream; 39 import java.io.OutputStream; 40 import java.net.InetAddress; 41 import java.net.InetSocketAddress; 42 import java.net.Socket; 43 44 /** 45 * Make connection and perform operations on mail server by reading and writing lines. 46 */ 47 public class MailTransport { 48 private static final String TAG = "MailTransport"; 49 50 // TODO protected eventually 51 /*protected*/ public static final int SOCKET_CONNECT_TIMEOUT = 10000; 52 /*protected*/ public static final int SOCKET_READ_TIMEOUT = 60000; 53 54 private static final HostnameVerifier HOSTNAME_VERIFIER = 55 HttpsURLConnection.getDefaultHostnameVerifier(); 56 57 private Context mContext; 58 private Network mNetwork; 59 private String mHost; 60 private int mPort; 61 private Socket mSocket; 62 private BufferedInputStream mIn; 63 private BufferedOutputStream mOut; 64 private int mFlags; 65 MailTransport(Context context, Network network, String address, int port, int flags)66 public MailTransport(Context context, Network network, String address, int port, int flags) { 67 mContext = context; 68 mNetwork = network; 69 mHost = address; 70 mPort = port; 71 mFlags = flags; 72 } 73 74 /** 75 * Returns a new transport, using the current transport as a model. The new transport is 76 * configured identically, but not opened or connected in any way. 77 */ 78 @Override clone()79 public MailTransport clone() { 80 return new MailTransport(mContext, mNetwork, mHost, mPort, mFlags); 81 } 82 canTrySslSecurity()83 public boolean canTrySslSecurity() { 84 return (mFlags & ImapStore.FLAG_SSL) != 0; 85 } 86 canTrustAllCertificates()87 public boolean canTrustAllCertificates() { 88 return (mFlags & ImapStore.FLAG_TRUST_ALL) != 0; 89 } 90 91 /** 92 * Attempts to open a connection using the Uri supplied for connection parameters. Will attempt 93 * an SSL connection if indicated. 94 */ open()95 public void open() throws MessagingException, CertificateValidationException { 96 LogUtils.d(TAG, "*** IMAP open " + mHost + ":" + String.valueOf(mPort)); 97 98 List<SocketAddress> socketAddresses = new ArrayList<SocketAddress>(); 99 try { 100 if (canTrySslSecurity()) { 101 mSocket = HttpsURLConnection.getDefaultSSLSocketFactory().createSocket(); 102 socketAddresses.add(new InetSocketAddress(mHost, mPort)); 103 } else { 104 if (mNetwork == null) { 105 mSocket = new Socket(); 106 socketAddresses.add(new InetSocketAddress(mHost, mPort)); 107 } else { 108 InetAddress[] inetAddresses = mNetwork.getAllByName(mHost); 109 for (int i = 0; i < inetAddresses.length; i++) { 110 socketAddresses.add(new InetSocketAddress(inetAddresses[i], mPort)); 111 } 112 mSocket = mNetwork.getSocketFactory().createSocket(); 113 } 114 } 115 } catch (IOException ioe) { 116 LogUtils.d(TAG, ioe.toString()); 117 throw new MessagingException(MessagingException.IOERROR, ioe.toString()); 118 } 119 120 while (socketAddresses.size() > 0) { 121 try { 122 mSocket.connect(socketAddresses.remove(0), SOCKET_CONNECT_TIMEOUT); 123 124 // After the socket connects to an SSL server, confirm that the hostname is as 125 // expected 126 if (canTrySslSecurity() && !canTrustAllCertificates()) { 127 verifyHostname(mSocket, mHost); 128 } 129 130 mIn = new BufferedInputStream(mSocket.getInputStream(), 1024); 131 mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512); 132 mSocket.setSoTimeout(SOCKET_READ_TIMEOUT); 133 return; 134 } catch (IOException ioe) { 135 LogUtils.d(TAG, ioe.toString()); 136 if (socketAddresses.size() == 0) { 137 // Only throw an error when there are no more sockets to try. 138 throw new MessagingException(MessagingException.IOERROR, ioe.toString()); 139 } 140 } 141 } 142 } 143 144 /** 145 * Lightweight version of SSLCertificateSocketFactory.verifyHostname, which provides this 146 * service but is not in the public API. 147 * 148 * Verify the hostname of the certificate used by the other end of a 149 * connected socket. It is harmless to call this method redundantly if the hostname has already 150 * been verified. 151 * 152 * <p>Wildcard certificates are allowed to verify any matching hostname, 153 * so "foo.bar.example.com" is verified if the peer has a certificate 154 * for "*.example.com". 155 * 156 * @param socket An SSL socket which has been connected to a server 157 * @param hostname The expected hostname of the remote server 158 * @throws IOException if something goes wrong handshaking with the server 159 * @throws SSLPeerUnverifiedException if the server cannot prove its identity 160 */ verifyHostname(Socket socket, String hostname)161 private static void verifyHostname(Socket socket, String hostname) throws IOException { 162 // The code at the start of OpenSSLSocketImpl.startHandshake() 163 // ensures that the call is idempotent, so we can safely call it. 164 SSLSocket ssl = (SSLSocket) socket; 165 ssl.startHandshake(); 166 167 SSLSession session = ssl.getSession(); 168 if (session == null) { 169 throw new SSLException("Cannot verify SSL socket without session"); 170 } 171 // TODO: Instead of reporting the name of the server we think we're connecting to, 172 // we should be reporting the bad name in the certificate. Unfortunately this is buried 173 // in the verifier code and is not available in the verifier API, and extracting the 174 // CN & alts is beyond the scope of this patch. 175 if (!HOSTNAME_VERIFIER.verify(hostname, session)) { 176 throw new SSLPeerUnverifiedException( 177 "Certificate hostname not useable for server: " + hostname); 178 } 179 } 180 isOpen()181 public boolean isOpen() { 182 return (mIn != null && mOut != null && 183 mSocket != null && mSocket.isConnected() && !mSocket.isClosed()); 184 } 185 186 /** 187 * Close the connection. MUST NOT return any exceptions - must be "best effort" and safe. 188 */ close()189 public void close() { 190 try { 191 mIn.close(); 192 } catch (Exception e) { 193 // May fail if the connection is already closed. 194 } 195 try { 196 mOut.close(); 197 } catch (Exception e) { 198 // May fail if the connection is already closed. 199 } 200 try { 201 mSocket.close(); 202 } catch (Exception e) { 203 // May fail if the connection is already closed. 204 } 205 mIn = null; 206 mOut = null; 207 mSocket = null; 208 } 209 getInputStream()210 public InputStream getInputStream() { 211 return mIn; 212 } 213 getOutputStream()214 public OutputStream getOutputStream() { 215 return mOut; 216 } 217 218 /** 219 * Writes a single line to the server using \r\n termination. 220 */ writeLine(String s, String sensitiveReplacement)221 public void writeLine(String s, String sensitiveReplacement) throws IOException { 222 if (sensitiveReplacement != null) { 223 LogUtils.d(TAG, ">>> " + sensitiveReplacement); 224 } else { 225 LogUtils.d(TAG, ">>> " + s); 226 } 227 228 OutputStream out = getOutputStream(); 229 out.write(s.getBytes()); 230 out.write('\r'); 231 out.write('\n'); 232 out.flush(); 233 } 234 235 /** 236 * Reads a single line from the server, using either \r\n or \n as the delimiter. The 237 * delimiter char(s) are not included in the result. 238 */ readLine(boolean loggable)239 public String readLine(boolean loggable) throws IOException { 240 StringBuffer sb = new StringBuffer(); 241 InputStream in = getInputStream(); 242 int d; 243 while ((d = in.read()) != -1) { 244 if (((char)d) == '\r') { 245 continue; 246 } else if (((char)d) == '\n') { 247 break; 248 } else { 249 sb.append((char)d); 250 } 251 } 252 if (d == -1) { 253 LogUtils.d(TAG, "End of stream reached while trying to read line."); 254 } 255 String ret = sb.toString(); 256 if (loggable) { 257 LogUtils.d(TAG, "<<< " + ret); 258 } 259 return ret; 260 } 261 } 262