Rework EAS account creation & ssl operation
* Fixes 2048663, 2025029, and 2100131
* Add "Trust Certificates" checkbox in EAS account creation
* Use custom ClientConnectionManager for HttpClient with registry
for plain, ssl, and tssl (trusted ssl) connection
* Use a ConnectionPool for HttpClient connections
* Remove "Domain" checkbox in EAS account creation
* Remove tests related to the "Domain" field
* TODO Write a test for valid usernames (requires a bit of research)
<name>, <email address>, <domain>/<name, and <domain>\<name> are all
valid, but there might be others
Change-Id: I4a0338df5960bfd3d679a88aaf22d1c49f49992b
diff --git a/res/values/strings.xml b/res/values/strings.xml
index ef7b3ba..8151f0c 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -364,6 +364,8 @@
<string name="account_setup_exchange_domain_hint">Enter domain here</string>
<!-- On "Exchange" setup screen, the use-SSL checkbox label -->
<string name="account_setup_exchange_ssl_label">Use secure connection (SSL)</string>
+ <!-- On "Exchange" setup screen, the trust ssl certificates checkbox label -->
+ <string name="account_setup_exchange_trust_certificates_label">Accept all SSL certificates</string>
<!-- In Account setup options screen, Activity title -->
<string name="account_setup_options_title">Account options</string>
diff --git a/src/com/android/exchange/AbstractSyncService.java b/src/com/android/exchange/AbstractSyncService.java
index 15cede8..97cfb87 100644
--- a/src/com/android/exchange/AbstractSyncService.java
+++ b/src/com/android/exchange/AbstractSyncService.java
@@ -101,7 +101,7 @@
* @throws MessagingException
*/
public abstract void validateAccount(String host, String userName, String password, int port,
- boolean ssl, Context context) throws MessagingException;
+ boolean ssl, boolean trustCertificates, Context context) throws MessagingException;
public AbstractSyncService(Context _context, Mailbox _mailbox) {
mContext = _context;
@@ -129,12 +129,13 @@
* @throws MessagingException
*/
static public void validate(Class<? extends AbstractSyncService> klass, String host,
- String userName, String password, int port, boolean ssl, Context context)
+ String userName, String password, int port, boolean ssl, boolean trustCertificates,
+ Context context)
throws MessagingException {
AbstractSyncService svc;
try {
svc = klass.newInstance();
- svc.validateAccount(host, userName, password, port, ssl, context);
+ svc.validateAccount(host, userName, password, port, ssl, trustCertificates, context);
} catch (IllegalAccessException e) {
throw new MessagingException("internal error", e);
} catch (InstantiationException e) {
diff --git a/src/com/android/exchange/EasSyncService.java b/src/com/android/exchange/EasSyncService.java
index b4f2ac4..7c1e3ae 100644
--- a/src/com/android/exchange/EasSyncService.java
+++ b/src/com/android/exchange/EasSyncService.java
@@ -46,6 +46,7 @@
import org.apache.http.client.methods.HttpOptions;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
+import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.params.BasicHttpParams;
@@ -65,11 +66,11 @@
import java.io.InputStream;
import java.net.URI;
import java.net.URLEncoder;
+import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.HashMap;
public class EasSyncService extends AbstractSyncService {
-
private static final String EMAIL_WINDOW_SIZE = "5";
public static final String PIM_WINDOW_SIZE = "20";
private static final String WHERE_ACCOUNT_KEY_AND_SERVER_ID =
@@ -133,6 +134,7 @@
public String mUserName;
public String mPassword;
private boolean mSsl = true;
+ private boolean mTrustSsl = false;
public ContentResolver mContentResolver;
private String[] mBindArguments = new String[2];
private ArrayList<String> mPingChangeList;
@@ -149,6 +151,7 @@
mContentResolver = _context.getContentResolver();
HostAuth ha = HostAuth.restoreHostAuthWithId(_context, mAccount.mHostAuthKeyRecv);
mSsl = (ha.mFlags & HostAuth.FLAG_SSL) != 0;
+ mTrustSsl = (ha.mFlags & HostAuth.FLAG_TRUST_ALL_CERTIFICATES) != 0;
}
private EasSyncService(String prefix) {
@@ -191,7 +194,7 @@
@Override
public void validateAccount(String hostAddress, String userName, String password, int port,
- boolean ssl, Context context) throws MessagingException {
+ boolean ssl, boolean trustCertificates, Context context) throws MessagingException {
try {
userLog("Testing EAS: ", hostAddress, ", ", userName, ", ssl = ", ssl ? "1" : "0");
EasSyncService svc = new EasSyncService("%TestAccount%");
@@ -200,6 +203,7 @@
svc.mUserName = userName;
svc.mPassword = password;
svc.mSsl = ssl;
+ svc.mTrustSsl = trustCertificates;
svc.mDeviceId = SyncManager.getDeviceId();
HttpResponse resp = svc.sendHttpClientOptions();
int code = resp.getStatusLine().getStatusCode();
@@ -218,7 +222,12 @@
throw new MessagingException(MessagingException.IOERROR);
}
} catch (IOException e) {
- userLog("IOException caught, reporting I/O error: ", e.getMessage());
+ Throwable cause = e.getCause();
+ if (cause != null && cause instanceof CertificateException) {
+ userLog("CertificateException caught: ", e.getMessage());
+ throw new MessagingException(MessagingException.GENERAL_SECURITY);
+ }
+ userLog("IOException caught: ", e.getMessage());
throw new MessagingException(MessagingException.IOERROR);
}
@@ -351,7 +360,7 @@
mCmdString = "&User=" + safeUserName + "&DeviceId=" + mDeviceId + "&DeviceType="
+ mDeviceType;
}
- String us = (mSsl ? "https" : "http") + "://" + mHostAddress +
+ String us = (mSsl ? (mTrustSsl ? "httpts" : "https") : "http") + "://" + mHostAddress +
"/Microsoft-Server-ActiveSync";
if (cmd != null) {
us += "?Cmd=" + cmd + mCmdString;
@@ -369,11 +378,17 @@
method.setHeader("User-Agent", mDeviceType + '/' + Eas.VERSION);
}
+ private ClientConnectionManager getClientConnectionManager() {
+ return SyncManager.getClientConnectionManager();
+ }
+
private HttpClient getHttpClient(int timeout) {
HttpParams params = new BasicHttpParams();
HttpConnectionParams.setConnectionTimeout(params, 15*SECONDS);
HttpConnectionParams.setSoTimeout(params, timeout);
- return new DefaultHttpClient(params);
+ HttpConnectionParams.setSocketBufferSize(params, 8192);
+ HttpClient client = new DefaultHttpClient(getClientConnectionManager(), params);
+ return client;
}
protected HttpResponse sendHttpClientPost(String cmd, byte[] bytes) throws IOException {
diff --git a/src/com/android/exchange/IEmailService.aidl b/src/com/android/exchange/IEmailService.aidl
index 23ff744..a07fab0 100644
--- a/src/com/android/exchange/IEmailService.aidl
+++ b/src/com/android/exchange/IEmailService.aidl
@@ -21,7 +21,7 @@
interface IEmailService {
int validate(in String protocol, in String host, in String userName, in String password,
- int port, boolean ssl) ;
+ int port, boolean ssl, boolean trustCertificates) ;
void startSync(long mailboxId);
void stopSync(long mailboxId);
diff --git a/src/com/android/exchange/SyncManager.java b/src/com/android/exchange/SyncManager.java
index 96196e6..3c4cb13 100644
--- a/src/com/android/exchange/SyncManager.java
+++ b/src/com/android/exchange/SyncManager.java
@@ -18,6 +18,7 @@
package com.android.exchange;
import com.android.email.mail.MessagingException;
+import com.android.email.mail.store.TrustManagerFactory;
import com.android.email.provider.EmailContent;
import com.android.email.provider.EmailContent.Account;
import com.android.email.provider.EmailContent.Attachment;
@@ -29,6 +30,19 @@
import com.android.email.provider.EmailContent.SyncColumns;
import com.android.exchange.utility.FileLogger;
+import org.apache.harmony.xnet.provider.jsse.SSLContextImpl;
+import org.apache.http.conn.ClientConnectionManager;
+import org.apache.http.conn.params.ConnManagerPNames;
+import org.apache.http.conn.params.ConnPerRoute;
+import org.apache.http.conn.routing.HttpRoute;
+import org.apache.http.conn.scheme.PlainSocketFactory;
+import org.apache.http.conn.scheme.Scheme;
+import org.apache.http.conn.scheme.SchemeRegistry;
+import org.apache.http.conn.ssl.SSLSocketFactory;
+import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
+import org.apache.http.params.BasicHttpParams;
+import org.apache.http.params.HttpParams;
+
import android.accounts.AccountManager;
import android.accounts.OnAccountsUpdatedListener;
import android.app.AlarmManager;
@@ -65,10 +79,16 @@
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
/**
* The SyncManager handles all aspects of starting, maintaining, and stopping the various sync
* adapters used by Exchange. However, it is capable of handing any kind of email sync, and it
@@ -176,6 +196,8 @@
private static Thread sServiceThread = null;
// Cached unique device id
private static String sDeviceId = null;
+ // ConnectionManager that all EAS threads can use
+ private static ClientConnectionManager sClientConnectionManager = null;
private boolean mStop = false;
@@ -240,10 +262,10 @@
private final IEmailService.Stub mBinder = new IEmailService.Stub() {
public int validate(String protocol, String host, String userName, String password,
- int port, boolean ssl) throws RemoteException {
+ int port, boolean ssl, boolean trustCertificates) throws RemoteException {
try {
AbstractSyncService.validate(EasSyncService.class, host, userName, password, port,
- ssl, SyncManager.this);
+ ssl, trustCertificates, SyncManager.this);
return MessagingException.NO_ERROR;
} catch (MessagingException e) {
return e.getExceptionType();
@@ -672,6 +694,52 @@
}
}
+ static public ConnPerRoute sConnPerRoute = new ConnPerRoute() {
+ public int getMaxForRoute(HttpRoute route) {
+ return 8;
+ }
+ };
+
+ static public synchronized ClientConnectionManager getClientConnectionManager() {
+ if (sClientConnectionManager == null) {
+ // Create a registry for our three schemes; http and https will use built-in factories
+ SchemeRegistry registry = new SchemeRegistry();
+ registry.register(new Scheme("http",
+ PlainSocketFactory.getSocketFactory(), 80));
+ registry.register(new Scheme("https", SSLSocketFactory.getSocketFactory(), 443));
+
+ // Create a new SSLSocketFactory for our "trusted ssl"
+ // Get the unsecure trust manager from the factory
+ X509TrustManager trustManager = TrustManagerFactory.get(null, false);
+ TrustManager[] trustManagers = new TrustManager[] {trustManager};
+ SSLContext sslcontext;
+ try {
+ sslcontext = SSLContext.getInstance("TLS");
+ sslcontext.init(null, trustManagers, null);
+ SSLContextImpl sslContext = new SSLContextImpl();
+ try {
+ sslContext.engineInit(null, trustManagers, null, null, null);
+ } catch (KeyManagementException e) {
+ throw new AssertionError(e);
+ }
+ // Ok, now make our factory
+ SSLSocketFactory sf = new SSLSocketFactory(sslContext.engineGetSocketFactory());
+ sf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
+ // Register the httpts scheme with our factory
+ registry.register(new Scheme("httpts", sf, 443));
+ // And create a ccm with our registry
+ HttpParams params = new BasicHttpParams();
+ params.setIntParameter(ConnManagerPNames.MAX_TOTAL_CONNECTIONS, 25);
+ params.setParameter(ConnManagerPNames.MAX_CONNECTIONS_PER_ROUTE, sConnPerRoute);
+ sClientConnectionManager = new ThreadSafeClientConnManager(params, registry);
+ } catch (NoSuchAlgorithmException e2) {
+ } catch (KeyManagementException e1) {
+ }
+ }
+ // Null is a valid return result if we get an exception
+ return sClientConnectionManager;
+ }
+
@Override
public void onDestroy() {
log("!!! EAS SyncManager destroyed");