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