• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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