Wrap client certificate errors in EasResponse.

Client certificate errors and bad username/error passwords just return
403 and it's difficult for us to differentiate the two. The easiest way
right now is to track it using a dummy KeyManager that can detect when a
certificate is requested upon connection establishment (way low in the
bowels of the Apache HTTP stack).

This change propagates that information up to the EasResponse and
encapsulates it there.

In the future, we should be more flexible as there can theoretically be
servers that REQUEST certificates, but do not REQUIRE it, but this code
will present an error regardless.

Change-Id: I7ee36e2c2ab06bdb8ce34b8967b7cb241812ac96
diff --git a/src/com/android/exchange/EasResponse.java b/src/com/android/exchange/EasResponse.java
index a63c2c0..74150a7 100644
--- a/src/com/android/exchange/EasResponse.java
+++ b/src/com/android/exchange/EasResponse.java
@@ -17,9 +17,16 @@
 
 package com.android.exchange;
 
+import com.android.emailcommon.utility.SSLUtils.CertificateRequestedException;
+
 import org.apache.http.Header;
 import org.apache.http.HttpEntity;
 import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpUriRequest;
+
+import android.util.Log;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -35,9 +42,15 @@
     private InputStream mInputStream;
     private boolean mClosed;
 
-    public EasResponse(HttpResponse response) {
+    /**
+     * Whether or not a certificate was requested by the server and missing.
+     * If this is set, it is essentially a 403 whereby the failure was due
+     */
+    private boolean mClientCertRequested = false;
+
+    private EasResponse(HttpResponse response) {
         mResponse = response;
-        mEntity = mResponse.getEntity();
+        mEntity = (response == null) ? null : mResponse.getEntity();
         if (mEntity !=  null) {
             mLength = (int)mEntity.getContentLength();
         } else {
@@ -45,6 +58,18 @@
         }
     }
 
+    public static EasResponse fromHttpRequest(HttpClient client, HttpUriRequest request)
+            throws IOException {
+        try {
+            return new EasResponse(client.execute(request));
+        } catch (CertificateRequestedException ex) {
+            EasResponse result = new EasResponse(null);
+            result.mClientCertRequested = true;
+            result.mClosed = true;
+            return result;
+        }
+    }
+
     /**
      * Return an appropriate input stream for the response, either a GZIPInputStream, for
      * compressed data, or a generic InputStream otherwise
@@ -80,11 +105,17 @@
     }
 
     public int getStatus() {
-        return mResponse.getStatusLine().getStatusCode();
+        return mClientCertRequested
+                ? HttpStatus.SC_UNAUTHORIZED
+                : mResponse.getStatusLine().getStatusCode();
+    }
+
+    public boolean isMissingCertificate() {
+        return mClientCertRequested;
     }
 
     public Header getHeader(String name) {
-        return mResponse.getFirstHeader(name);
+        return (mResponse == null) ? null : mResponse.getFirstHeader(name);
     }
 
     public int getLength() {
diff --git a/src/com/android/exchange/EasSyncService.java b/src/com/android/exchange/EasSyncService.java
index b469c14..f819a5a 100644
--- a/src/com/android/exchange/EasSyncService.java
+++ b/src/com/android/exchange/EasSyncService.java
@@ -355,10 +355,6 @@
                 mPendingPost.abort();
             }
         }
-
-        // TODO: some kind of unregistering of special client cert aliases.
-        // We can't blindly do this as multiple services could be using the same alias,
-        // so there needs to be some kind of registry to track the interests in an alias.
     }
 
     @Override
@@ -587,7 +583,9 @@
                         // We get a 404 from OWA addresses (which are NOT EAS addresses)
                         resultCode = MessagingException.PROTOCOL_VERSION_UNSUPPORTED;
                     } else if (code == HttpStatus.SC_UNAUTHORIZED) {
-                        resultCode = MessagingException.AUTHENTICATION_FAILED;
+                        resultCode = resp.isMissingCertificate()
+                                ? MessagingException.CLIENT_CERTIFICATE_ERROR
+                                : MessagingException.AUTHENTICATION_FAILED;
                     } else if (code != HttpStatus.SC_OK) {
                         // Fail generically with anything other than success
                         userLog("Unexpected response for FolderSync: ", code);
@@ -606,7 +604,9 @@
                     }
                 } else if (isAuthError(code)) {
                     userLog("Authentication failed");
-                    resultCode = MessagingException.AUTHENTICATION_FAILED;
+                    resultCode = resp.isMissingCertificate()
+                            ? MessagingException.CLIENT_CERTIFICATE_ERROR
+                            : MessagingException.AUTHENTICATION_FAILED;
                 } else if (code == INTERNAL_SERVER_ERROR_CODE) {
                     // For Exchange 2003, this could mean an authentication failure OR server error
                     userLog("Internal server error");
@@ -1275,9 +1275,6 @@
 
         EmailClientConnectionManager connManager = getClientConnectionManager();
 
-        // TODO: unregister the old client cert connection, if there is one. Multiple sync
-        // services may be using the alias though so we need some kind of registry.
-
         mSsl = useSsl;
         mTrustSsl = trustAllServerCerts;
         mClientCertAlias = clientCertAlias;
@@ -1348,7 +1345,7 @@
             }
         }
         try {
-            return new EasResponse(client.execute(method));
+            return EasResponse.fromHttpRequest(client, method);
         } finally {
             synchronized(getSynchronizer()) {
                 if (isPingCommand) {
@@ -1398,7 +1395,7 @@
         String us = makeUriString("OPTIONS", null);
         HttpOptions method = new HttpOptions(URI.create(us));
         setHeaders(method, false);
-        return new EasResponse(client.execute(method));
+        return EasResponse.fromHttpRequest(client, method);
     }
 
     private String getTargetCollectionClassFromCursor(Cursor c) {
@@ -2535,6 +2532,6 @@
 
             // Make sure ExchangeService knows about this
             ExchangeService.kick("sync finished");
-       }
+        }
     }
 }
diff --git a/src/com/android/exchange/ExchangeService.java b/src/com/android/exchange/ExchangeService.java
index ef45962..9a8794b 100644
--- a/src/com/android/exchange/ExchangeService.java
+++ b/src/com/android/exchange/ExchangeService.java
@@ -45,7 +45,6 @@
 import com.android.exchange.provider.MailboxUtilities;
 import com.android.exchange.utility.FileLogger;
 
-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;