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 import android.provider.VoicemailContract.Status; 21 22 import com.android.internal.annotations.VisibleForTesting; 23 import com.android.phone.common.mail.store.ImapStore; 24 import com.android.phone.common.mail.utils.LogUtils; 25 import com.android.phone.vvm.omtp.imap.ImapHelper; 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.util.ArrayList; 37 import java.util.List; 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 * Make connection and perform operations on mail server by reading and writing lines. 48 */ 49 public class MailTransport { 50 private static final String TAG = "MailTransport"; 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 final Context mContext; 60 private final ImapHelper mImapHelper; 61 private final Network mNetwork; 62 private final String mHost; 63 private final int mPort; 64 private Socket mSocket; 65 private BufferedInputStream mIn; 66 private BufferedOutputStream mOut; 67 private final int mFlags; 68 private SocketCreator mSocketCreator; 69 MailTransport(Context context, ImapHelper imapHelper, Network network, String address, int port, int flags)70 public MailTransport(Context context, ImapHelper imapHelper, Network network, String address, 71 int port, int flags) { 72 mContext = context; 73 mImapHelper = imapHelper; 74 mNetwork = network; 75 mHost = address; 76 mPort = port; 77 mFlags = flags; 78 } 79 80 /** 81 * Returns a new transport, using the current transport as a model. The new transport is 82 * configured identically, but not opened or connected in any way. 83 */ 84 @Override clone()85 public MailTransport clone() { 86 return new MailTransport(mContext, mImapHelper, mNetwork, mHost, mPort, mFlags); 87 } 88 canTrySslSecurity()89 public boolean canTrySslSecurity() { 90 return (mFlags & ImapStore.FLAG_SSL) != 0; 91 } 92 canTrustAllCertificates()93 public boolean canTrustAllCertificates() { 94 return (mFlags & ImapStore.FLAG_TRUST_ALL) != 0; 95 } 96 97 /** 98 * Attempts to open a connection using the Uri supplied for connection parameters. Will attempt 99 * an SSL connection if indicated. 100 */ open()101 public void open() throws MessagingException { 102 LogUtils.d(TAG, "*** IMAP open " + mHost + ":" + String.valueOf(mPort)); 103 104 List<InetSocketAddress> socketAddresses = new ArrayList<InetSocketAddress>(); 105 106 if (mNetwork == null) { 107 socketAddresses.add(new InetSocketAddress(mHost, mPort)); 108 } else { 109 try { 110 InetAddress[] inetAddresses = mNetwork.getAllByName(mHost); 111 if (inetAddresses.length == 0) { 112 throw new MessagingException(MessagingException.IOERROR, 113 "Host name " + mHost + "cannot be resolved on designated network"); 114 } 115 for (int i = 0; i < inetAddresses.length; i++) { 116 socketAddresses.add(new InetSocketAddress(inetAddresses[i], mPort)); 117 } 118 } catch (IOException ioe) { 119 LogUtils.d(TAG, ioe.toString()); 120 mImapHelper.setDataChannelState(Status.DATA_CHANNEL_STATE_SERVER_CONNECTION_ERROR); 121 throw new MessagingException(MessagingException.IOERROR, ioe.toString()); 122 } 123 } 124 125 boolean success = false; 126 while (socketAddresses.size() > 0) { 127 mSocket = createSocket(); 128 try { 129 InetSocketAddress address = socketAddresses.remove(0); 130 mSocket.connect(address, SOCKET_CONNECT_TIMEOUT); 131 132 if (canTrySslSecurity()) { 133 /** 134 * {@link SSLSocket} must connect in its constructor, or create through a 135 * already connected socket. Since we need to use 136 * {@link Socket#connect(SocketAddress, int) } to set timeout, we can only 137 * create it here. 138 */ 139 LogUtils.d(TAG, "open: converting to SSL socket"); 140 mSocket = HttpsURLConnection.getDefaultSSLSocketFactory() 141 .createSocket(mSocket, address.getHostName(), address.getPort(), true); 142 // After the socket connects to an SSL server, confirm that the hostname is as 143 // expected 144 if (!canTrustAllCertificates()) { 145 verifyHostname(mSocket, mHost); 146 } 147 } 148 149 mIn = new BufferedInputStream(mSocket.getInputStream(), 1024); 150 mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512); 151 mSocket.setSoTimeout(SOCKET_READ_TIMEOUT); 152 success = true; 153 return; 154 } catch (IOException ioe) { 155 LogUtils.d(TAG, ioe.toString()); 156 if (socketAddresses.size() == 0) { 157 // Only throw an error when there are no more sockets to try. 158 mImapHelper 159 .setDataChannelState(Status.DATA_CHANNEL_STATE_SERVER_CONNECTION_ERROR); 160 throw new MessagingException(MessagingException.IOERROR, ioe.toString()); 161 } 162 } finally { 163 if (!success) { 164 try { 165 mSocket.close(); 166 mSocket = null; 167 } catch (IOException ioe) { 168 throw new MessagingException(MessagingException.IOERROR, ioe.toString()); 169 } 170 171 } 172 } 173 } 174 } 175 176 // For testing. We need something that can replace the behavior of "new Socket()" 177 @VisibleForTesting 178 interface SocketCreator { 179 createSocket()180 Socket createSocket() throws MessagingException; 181 } 182 183 @VisibleForTesting setSocketCreator(SocketCreator creator)184 void setSocketCreator(SocketCreator creator) { 185 mSocketCreator = creator; 186 } 187 createSocket()188 protected Socket createSocket() throws MessagingException { 189 if (mSocketCreator != null) { 190 return mSocketCreator.createSocket(); 191 } 192 193 if (mNetwork == null) { 194 LogUtils.v(TAG, "createSocket: network not specified"); 195 return new Socket(); 196 } 197 198 try { 199 LogUtils.v(TAG, "createSocket: network specified"); 200 return mNetwork.getSocketFactory().createSocket(); 201 } catch (IOException ioe) { 202 LogUtils.d(TAG, ioe.toString()); 203 throw new MessagingException(MessagingException.IOERROR, ioe.toString()); 204 } 205 } 206 207 /** 208 * Lightweight version of SSLCertificateSocketFactory.verifyHostname, which provides this 209 * service but is not in the public API. 210 * 211 * Verify the hostname of the certificate used by the other end of a 212 * connected socket. It is harmless to call this method redundantly if the hostname has already 213 * been verified. 214 * 215 * <p>Wildcard certificates are allowed to verify any matching hostname, 216 * so "foo.bar.example.com" is verified if the peer has a certificate 217 * for "*.example.com". 218 * 219 * @param socket An SSL socket which has been connected to a server 220 * @param hostname The expected hostname of the remote server 221 * @throws IOException if something goes wrong handshaking with the server 222 * @throws SSLPeerUnverifiedException if the server cannot prove its identity 223 */ verifyHostname(Socket socket, String hostname)224 private void verifyHostname(Socket socket, String hostname) throws IOException { 225 // The code at the start of OpenSSLSocketImpl.startHandshake() 226 // ensures that the call is idempotent, so we can safely call it. 227 SSLSocket ssl = (SSLSocket) socket; 228 ssl.startHandshake(); 229 230 SSLSession session = ssl.getSession(); 231 if (session == null) { 232 mImapHelper.setDataChannelState(Status.DATA_CHANNEL_STATE_COMMUNICATION_ERROR); 233 throw new SSLException("Cannot verify SSL socket without session"); 234 } 235 // TODO: Instead of reporting the name of the server we think we're connecting to, 236 // we should be reporting the bad name in the certificate. Unfortunately this is buried 237 // in the verifier code and is not available in the verifier API, and extracting the 238 // CN & alts is beyond the scope of this patch. 239 if (!HOSTNAME_VERIFIER.verify(hostname, session)) { 240 mImapHelper.setDataChannelState(Status.DATA_CHANNEL_STATE_COMMUNICATION_ERROR); 241 throw new SSLPeerUnverifiedException("Certificate hostname not useable for server: " 242 + session.getPeerPrincipal()); 243 } 244 } 245 isOpen()246 public boolean isOpen() { 247 return (mIn != null && mOut != null && 248 mSocket != null && mSocket.isConnected() && !mSocket.isClosed()); 249 } 250 251 /** 252 * Close the connection. MUST NOT return any exceptions - must be "best effort" and safe. 253 */ close()254 public void close() { 255 try { 256 mIn.close(); 257 } catch (Exception e) { 258 // May fail if the connection is already closed. 259 } 260 try { 261 mOut.close(); 262 } catch (Exception e) { 263 // May fail if the connection is already closed. 264 } 265 try { 266 mSocket.close(); 267 } catch (Exception e) { 268 // May fail if the connection is already closed. 269 } 270 mIn = null; 271 mOut = null; 272 mSocket = null; 273 } 274 getInputStream()275 public InputStream getInputStream() { 276 return mIn; 277 } 278 getOutputStream()279 public OutputStream getOutputStream() { 280 return mOut; 281 } 282 283 /** 284 * Writes a single line to the server using \r\n termination. 285 */ writeLine(String s, String sensitiveReplacement)286 public void writeLine(String s, String sensitiveReplacement) throws IOException { 287 if (sensitiveReplacement != null) { 288 LogUtils.d(TAG, ">>> " + sensitiveReplacement); 289 } else { 290 LogUtils.d(TAG, ">>> " + s); 291 } 292 293 OutputStream out = getOutputStream(); 294 out.write(s.getBytes()); 295 out.write('\r'); 296 out.write('\n'); 297 out.flush(); 298 } 299 300 /** 301 * Reads a single line from the server, using either \r\n or \n as the delimiter. The 302 * delimiter char(s) are not included in the result. 303 */ readLine(boolean loggable)304 public String readLine(boolean loggable) throws IOException { 305 StringBuffer sb = new StringBuffer(); 306 InputStream in = getInputStream(); 307 int d; 308 while ((d = in.read()) != -1) { 309 if (((char)d) == '\r') { 310 continue; 311 } else if (((char)d) == '\n') { 312 break; 313 } else { 314 sb.append((char)d); 315 } 316 } 317 if (d == -1) { 318 LogUtils.d(TAG, "End of stream reached while trying to read line."); 319 } 320 String ret = sb.toString(); 321 if (loggable) { 322 LogUtils.d(TAG, "<<< " + ret); 323 } 324 return ret; 325 } 326 } 327