Home | History | Annotate | Download | only in service
      1 package com.android.exchange.service;
      2 
      3 import android.content.Context;
      4 import android.net.Uri;
      5 import android.os.Bundle;
      6 import android.util.Xml;
      7 
      8 import com.android.emailcommon.mail.MessagingException;
      9 import com.android.emailcommon.provider.Account;
     10 import com.android.emailcommon.provider.HostAuth;
     11 import com.android.emailcommon.service.EmailServiceProxy;
     12 import com.android.exchange.Eas;
     13 import com.android.exchange.EasResponse;
     14 import com.android.mail.utils.LogUtils;
     15 
     16 import org.apache.http.HttpStatus;
     17 import org.apache.http.client.methods.HttpPost;
     18 import org.apache.http.entity.StringEntity;
     19 import org.xmlpull.v1.XmlPullParser;
     20 import org.xmlpull.v1.XmlPullParserException;
     21 import org.xmlpull.v1.XmlPullParserFactory;
     22 import org.xmlpull.v1.XmlSerializer;
     23 
     24 import java.io.ByteArrayOutputStream;
     25 import java.io.IOException;
     26 import java.net.URI;
     27 import java.security.cert.CertificateException;
     28 
     29 /**
     30  * Performs Autodiscover for Exchange servers. This feature tries to find all the configuration
     31  * options needed based on just a username and password.
     32  */
     33 public class EasAutoDiscover extends EasServerConnection {
     34     private static final String TAG = Eas.LOG_TAG;
     35 
     36     private static final String AUTO_DISCOVER_SCHEMA_PREFIX =
     37         "http://schemas.microsoft.com/exchange/autodiscover/mobilesync/";
     38     private static final String AUTO_DISCOVER_PAGE = "/autodiscover/autodiscover.xml";
     39 
     40     // Set of string constants for parsing the autodiscover response.
     41     // TODO: Merge this into Tags.java? It's not quite the same but conceptually belongs there.
     42     private static final String ELEMENT_NAME_SERVER = "Server";
     43     private static final String ELEMENT_NAME_TYPE = "Type";
     44     private static final String ELEMENT_NAME_MOBILE_SYNC = "MobileSync";
     45     private static final String ELEMENT_NAME_URL = "Url";
     46     private static final String ELEMENT_NAME_SETTINGS = "Settings";
     47     private static final String ELEMENT_NAME_ACTION = "Action";
     48     private static final String ELEMENT_NAME_ERROR = "Error";
     49     private static final String ELEMENT_NAME_REDIRECT = "Redirect";
     50     private static final String ELEMENT_NAME_USER = "User";
     51     private static final String ELEMENT_NAME_EMAIL_ADDRESS = "EMailAddress";
     52     private static final String ELEMENT_NAME_DISPLAY_NAME = "DisplayName";
     53     private static final String ELEMENT_NAME_RESPONSE = "Response";
     54     private static final String ELEMENT_NAME_AUTODISCOVER = "Autodiscover";
     55 
     56     public EasAutoDiscover(final Context context, final String username, final String password) {
     57         super(context, new Account(), new HostAuth());
     58         mHostAuth.mLogin = username;
     59         mHostAuth.mPassword = password;
     60         mHostAuth.mFlags = HostAuth.FLAG_AUTHENTICATE | HostAuth.FLAG_SSL;
     61         mHostAuth.mPort = 443;
     62     }
     63 
     64     /**
     65      * Do all the work of autodiscovery.
     66      * @return A {@link Bundle} with the host information if autodiscovery succeeded. If we failed
     67      *     due to an authentication failure, we return a {@link Bundle} with no host info but with
     68      *     an appropriate error code. Otherwise, we return null.
     69      */
     70     public Bundle doAutodiscover() {
     71         final String domain = getDomain();
     72         if (domain == null) {
     73             return null;
     74         }
     75 
     76         final StringEntity entity = buildRequestEntity();
     77         if (entity == null) {
     78             return null;
     79         }
     80         try {
     81             final HttpPost post = makePost("https://" + domain + AUTO_DISCOVER_PAGE, entity,
     82                     "text/xml", false);
     83             final EasResponse resp = getResponse(post, domain);
     84             if (resp == null) {
     85                 return null;
     86             }
     87 
     88             try {
     89                 // resp is either an authentication error, or a good response.
     90                 final int code = resp.getStatus();
     91                 if (code == HttpStatus.SC_UNAUTHORIZED) {
     92                     final Bundle bundle = new Bundle(1);
     93                     bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
     94                             MessagingException.AUTODISCOVER_AUTHENTICATION_FAILED);
     95                     return bundle;
     96                 } else {
     97                     final HostAuth hostAuth = parseAutodiscover(resp);
     98                     if (hostAuth != null) {
     99                         // Fill in the rest of the HostAuth
    100                         // We use the user name and password that were successful during
    101                         // the autodiscover process
    102                         hostAuth.mLogin = mHostAuth.mLogin;
    103                         hostAuth.mPassword = mHostAuth.mPassword;
    104                         // Note: there is no way we can auto-discover the proper client
    105                         // SSL certificate to use, if one is needed.
    106                         hostAuth.mPort = 443;
    107                         hostAuth.mProtocol = Eas.PROTOCOL;
    108                         hostAuth.mFlags = HostAuth.FLAG_SSL | HostAuth.FLAG_AUTHENTICATE;
    109                         final Bundle bundle = new Bundle(2);
    110                         bundle.putParcelable(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_HOST_AUTH,
    111                                 hostAuth);
    112                         bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
    113                                 MessagingException.NO_ERROR);
    114                         return bundle;
    115                     }
    116                 }
    117             } finally {
    118                 resp.close();
    119             }
    120         } catch (final IllegalArgumentException e) {
    121             // This happens when the domain is malformatted.
    122             // TODO: Fix sanitizing of the domain -- we try to in UI but apparently not correctly.
    123             LogUtils.e(TAG, "ISE with domain: %s", domain);
    124         }
    125         return null;
    126     }
    127 
    128     /**
    129      * Get the domain of our account.
    130      * @return The domain of the email address.
    131      */
    132     private String getDomain() {
    133         final int amp = mHostAuth.mLogin.indexOf('@');
    134         if (amp < 0) {
    135             return null;
    136         }
    137         return mHostAuth.mLogin.substring(amp + 1);
    138     }
    139 
    140     /**
    141      * Create the payload of the request.
    142      * @return A {@link StringEntity} for the request XML.
    143      */
    144     private StringEntity buildRequestEntity() {
    145         try {
    146             final XmlSerializer s = Xml.newSerializer();
    147             final ByteArrayOutputStream os = new ByteArrayOutputStream(1024);
    148             s.setOutput(os, "UTF-8");
    149             s.startDocument("UTF-8", false);
    150             s.startTag(null, "Autodiscover");
    151             s.attribute(null, "xmlns", AUTO_DISCOVER_SCHEMA_PREFIX + "requestschema/2006");
    152             s.startTag(null, "Request");
    153             s.startTag(null, "EMailAddress").text(mHostAuth.mLogin).endTag(null, "EMailAddress");
    154             s.startTag(null, "AcceptableResponseSchema");
    155             s.text(AUTO_DISCOVER_SCHEMA_PREFIX + "responseschema/2006");
    156             s.endTag(null, "AcceptableResponseSchema");
    157             s.endTag(null, "Request");
    158             s.endTag(null, "Autodiscover");
    159             s.endDocument();
    160             return new StringEntity(os.toString());
    161         } catch (final IOException e) {
    162             // For all exception types, we can simply punt on autodiscover.
    163         } catch (final IllegalArgumentException e) {
    164         } catch (final IllegalStateException e) {
    165         }
    166 
    167         return null;
    168     }
    169 
    170     /**
    171      * Perform all requests necessary and get the server response. If the post fails or is
    172      * redirected, we alter the post and retry.
    173      * @param post The initial {@link HttpPost} for this request.
    174      * @param domain The domain for our account.
    175      * @return If this request succeeded or has an unrecoverable authentication error, an
    176      *     {@link EasResponse} with the details. For other errors, we return null.
    177      */
    178     private EasResponse getResponse(final HttpPost post, final String domain) {
    179         EasResponse resp = doPost(post, true);
    180         if (resp == null) {
    181             LogUtils.d(TAG, "Error in autodiscover, trying aternate address");
    182             post.setURI(URI.create("https://autodiscover." + domain + AUTO_DISCOVER_PAGE));
    183             resp = doPost(post, true);
    184         }
    185         return resp;
    186     }
    187 
    188     /**
    189      * Perform one attempt to get autodiscover information. Redirection and some authentication
    190      * errors are handled by recursively calls with modified host information.
    191      * @param post The {@link HttpPost} for this request.
    192      * @param canRetry Whether we can retry after an authentication failure.
    193      * @return If this request succeeded or has an unrecoverable authentication error, an
    194      *     {@link EasResponse} with the details. For other errors, we return null.
    195      */
    196     private EasResponse doPost(final HttpPost post, final boolean canRetry) {
    197         final EasResponse resp;
    198         try {
    199             resp = executePost(post);
    200         } catch (final IOException e) {
    201             return null;
    202         } catch (final CertificateException e) {
    203             // TODO: Raise this error to the user or something
    204             return null;
    205         }
    206 
    207         final int code = resp.getStatus();
    208 
    209         if (resp.isRedirectError()) {
    210             final String loc = resp.getRedirectAddress();
    211             if (loc != null && loc.startsWith("http")) {
    212                 LogUtils.d(TAG, "Posting autodiscover to redirect: " + loc);
    213                 redirectHostAuth(loc);
    214                 post.setURI(URI.create(loc));
    215                 return doPost(post, canRetry);
    216             }
    217             return null;
    218         }
    219 
    220         if (code == HttpStatus.SC_UNAUTHORIZED) {
    221             if (canRetry && mHostAuth.mLogin.contains("@")) {
    222                 // Try again using the bare user name
    223                 final int atSignIndex = mHostAuth.mLogin.indexOf('@');
    224                 mHostAuth.mLogin = mHostAuth.mLogin.substring(0, atSignIndex);
    225                 LogUtils.d(TAG, "401 received; trying username: %s", mHostAuth.mLogin);
    226                 resetAuthorization(post);
    227                 return doPost(post, false);
    228             }
    229         } else if (code != HttpStatus.SC_OK) {
    230             // We'll try the next address if this doesn't work
    231             LogUtils.d(TAG, "Bad response code when posting autodiscover: %d", code);
    232             return null;
    233         }
    234 
    235         return resp;
    236     }
    237 
    238     /**
    239      * Parse the Server element of the server response.
    240      * @param parser The {@link XmlPullParser}.
    241      * @param hostAuth The {@link HostAuth} to populate with the results of parsing.
    242      * @throws XmlPullParserException
    243      * @throws IOException
    244      */
    245     private static void parseServer(final XmlPullParser parser, final HostAuth hostAuth)
    246             throws XmlPullParserException, IOException {
    247         boolean mobileSync = false;
    248         while (true) {
    249             final int type = parser.next();
    250             if (type == XmlPullParser.END_TAG && parser.getName().equals(ELEMENT_NAME_SERVER)) {
    251                 break;
    252             } else if (type == XmlPullParser.START_TAG) {
    253                 final String name = parser.getName();
    254                 if (name.equals(ELEMENT_NAME_TYPE)) {
    255                     if (parser.nextText().equals(ELEMENT_NAME_MOBILE_SYNC)) {
    256                         mobileSync = true;
    257                     }
    258                 } else if (mobileSync && name.equals(ELEMENT_NAME_URL)) {
    259                     final String url = parser.nextText();
    260                     if (url != null) {
    261                         LogUtils.d(TAG, "Autodiscover URL: %s", url);
    262                         hostAuth.mAddress = Uri.parse(url).getHost();
    263                     }
    264                 }
    265             }
    266         }
    267     }
    268 
    269     /**
    270      * Parse the Settings element of the server response.
    271      * @param parser The {@link XmlPullParser}.
    272      * @param hostAuth The {@link HostAuth} to populate with the results of parsing.
    273      * @throws XmlPullParserException
    274      * @throws IOException
    275      */
    276     private static void parseSettings(final XmlPullParser parser, final HostAuth hostAuth)
    277             throws XmlPullParserException, IOException {
    278         while (true) {
    279             final int type = parser.next();
    280             if (type == XmlPullParser.END_TAG && parser.getName().equals(ELEMENT_NAME_SETTINGS)) {
    281                 break;
    282             } else if (type == XmlPullParser.START_TAG) {
    283                 final String name = parser.getName();
    284                 if (name.equals(ELEMENT_NAME_SERVER)) {
    285                     parseServer(parser, hostAuth);
    286                 }
    287             }
    288         }
    289     }
    290 
    291     /**
    292      * Parse the Action element of the server response.
    293      * @param parser The {@link XmlPullParser}.
    294      * @param hostAuth The {@link HostAuth} to populate with the results of parsing.
    295      * @throws XmlPullParserException
    296      * @throws IOException
    297      */
    298     private static void parseAction(final XmlPullParser parser, final HostAuth hostAuth)
    299             throws XmlPullParserException, IOException {
    300         while (true) {
    301             final int type = parser.next();
    302             if (type == XmlPullParser.END_TAG && parser.getName().equals(ELEMENT_NAME_ACTION)) {
    303                 break;
    304             } else if (type == XmlPullParser.START_TAG) {
    305                 final String name = parser.getName();
    306                 if (name.equals(ELEMENT_NAME_ERROR)) {
    307                     // Should parse the error
    308                 } else if (name.equals(ELEMENT_NAME_REDIRECT)) {
    309                     LogUtils.d(TAG, "Redirect: " + parser.nextText());
    310                 } else if (name.equals(ELEMENT_NAME_SETTINGS)) {
    311                     parseSettings(parser, hostAuth);
    312                 }
    313             }
    314         }
    315     }
    316 
    317     /**
    318      * Parse the User element of the server response.
    319      * @param parser The {@link XmlPullParser}.
    320      * @param hostAuth The {@link HostAuth} to populate with the results of parsing.
    321      * @throws XmlPullParserException
    322      * @throws IOException
    323      */
    324     private static void parseUser(final XmlPullParser parser, final HostAuth hostAuth)
    325             throws XmlPullParserException, IOException {
    326         while (true) {
    327             int type = parser.next();
    328             if (type == XmlPullParser.END_TAG && parser.getName().equals(ELEMENT_NAME_USER)) {
    329                 break;
    330             } else if (type == XmlPullParser.START_TAG) {
    331                 String name = parser.getName();
    332                 if (name.equals(ELEMENT_NAME_EMAIL_ADDRESS)) {
    333                     final String addr = parser.nextText();
    334                     LogUtils.d(TAG, "Autodiscover, email: %s", addr);
    335                 } else if (name.equals(ELEMENT_NAME_DISPLAY_NAME)) {
    336                     final String dn = parser.nextText();
    337                     LogUtils.d(TAG, "Autodiscover, user: %s", dn);
    338                 }
    339             }
    340         }
    341     }
    342 
    343     /**
    344      * Parse the Response element of the server response.
    345      * @param parser The {@link XmlPullParser}.
    346      * @param hostAuth The {@link HostAuth} to populate with the results of parsing.
    347      * @throws XmlPullParserException
    348      * @throws IOException
    349      */
    350     private static void parseResponse(final XmlPullParser parser, final HostAuth hostAuth)
    351             throws XmlPullParserException, IOException {
    352         while (true) {
    353             final int type = parser.next();
    354             if (type == XmlPullParser.END_TAG && parser.getName().equals(ELEMENT_NAME_RESPONSE)) {
    355                 break;
    356             } else if (type == XmlPullParser.START_TAG) {
    357                 final String name = parser.getName();
    358                 if (name.equals(ELEMENT_NAME_USER)) {
    359                     parseUser(parser, hostAuth);
    360                 } else if (name.equals(ELEMENT_NAME_ACTION)) {
    361                     parseAction(parser, hostAuth);
    362                 }
    363             }
    364         }
    365     }
    366 
    367     /**
    368      * Parse the server response for the final {@link HostAuth}.
    369      * @param resp The {@link EasResponse} from the server.
    370      * @return The final {@link HostAuth} for this server.
    371      */
    372     private static HostAuth parseAutodiscover(final EasResponse resp) {
    373         // The response to Autodiscover is regular XML (not WBXML)
    374         try {
    375             final XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
    376             parser.setInput(resp.getInputStream(), "UTF-8");
    377             if (parser.getEventType() != XmlPullParser.START_DOCUMENT) {
    378                 return null;
    379             }
    380             if (parser.next() != XmlPullParser.START_TAG) {
    381                 return null;
    382             }
    383             if (!parser.getName().equals(ELEMENT_NAME_AUTODISCOVER)) {
    384                 return null;
    385             }
    386 
    387             final HostAuth hostAuth = new HostAuth();
    388             while (true) {
    389                 final int type = parser.nextTag();
    390                 if (type == XmlPullParser.END_TAG && parser.getName()
    391                         .equals(ELEMENT_NAME_AUTODISCOVER)) {
    392                     break;
    393                 } else if (type == XmlPullParser.START_TAG && parser.getName()
    394                         .equals(ELEMENT_NAME_RESPONSE)) {
    395                     parseResponse(parser, hostAuth);
    396                     // Valid responses will set the address.
    397                     if (hostAuth.mAddress != null) {
    398                         return hostAuth;
    399                     }
    400                 }
    401             }
    402         } catch (final XmlPullParserException e) {
    403             // Parse error.
    404         } catch (final IOException e) {
    405             // Error reading parser.
    406         }
    407         return null;
    408     }
    409 }
    410