Home | History | Annotate | Download | only in eas
      1 package com.android.exchange.eas;
      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.provider.Account;
      9 import com.android.emailcommon.provider.HostAuth;
     10 import com.android.emailcommon.service.EmailServiceProxy;
     11 import com.android.emailcommon.service.HostAuthCompat;
     12 import com.android.exchange.CommandStatusException;
     13 import com.android.exchange.Eas;
     14 import com.android.exchange.EasResponse;
     15 import com.android.mail.utils.LogUtils;
     16 
     17 import org.apache.http.HttpEntity;
     18 import org.apache.http.HttpStatus;
     19 import org.apache.http.client.methods.HttpUriRequest;
     20 import org.apache.http.entity.StringEntity;
     21 import org.xmlpull.v1.XmlPullParser;
     22 import org.xmlpull.v1.XmlPullParserException;
     23 import org.xmlpull.v1.XmlPullParserFactory;
     24 import org.xmlpull.v1.XmlSerializer;
     25 
     26 import java.io.ByteArrayOutputStream;
     27 import java.io.IOException;
     28 
     29 public class EasAutoDiscover extends EasOperation {
     30 
     31     public final static int ATTEMPT_PRIMARY = 0;
     32     public final static int ATTEMPT_ALTERNATE = 1;
     33     public final static int ATTEMPT_UNAUTHENTICATED_GET = 2;
     34     public final static int ATTEMPT_MAX = 2;
     35 
     36     public final static int RESULT_OK = 1;
     37     public final static int RESULT_SC_UNAUTHORIZED = RESULT_OP_SPECIFIC_ERROR_RESULT - 0;
     38     public final static int RESULT_REDIRECT = RESULT_OP_SPECIFIC_ERROR_RESULT - 1;
     39     public final static int RESULT_BAD_RESPONSE = RESULT_OP_SPECIFIC_ERROR_RESULT - 2;
     40     public final static int RESULT_FATAL_SERVER_ERROR = RESULT_OP_SPECIFIC_ERROR_RESULT - 3;
     41 
     42     private final static String TAG = LogUtils.TAG;
     43 
     44     private static final String AUTO_DISCOVER_SCHEMA_PREFIX =
     45             "http://schemas.microsoft.com/exchange/autodiscover/mobilesync/";
     46     private static final String AUTO_DISCOVER_PAGE = "/autodiscover/autodiscover.xml";
     47 
     48     // Set of string constants for parsing the autodiscover response.
     49     // TODO: Merge this into Tags.java? It's not quite the same but conceptually belongs there.
     50     private static final String ELEMENT_NAME_SERVER = "Server";
     51     private static final String ELEMENT_NAME_TYPE = "Type";
     52     private static final String ELEMENT_NAME_MOBILE_SYNC = "MobileSync";
     53     private static final String ELEMENT_NAME_URL = "Url";
     54     private static final String ELEMENT_NAME_SETTINGS = "Settings";
     55     private static final String ELEMENT_NAME_ACTION = "Action";
     56     private static final String ELEMENT_NAME_ERROR = "Error";
     57     private static final String ELEMENT_NAME_REDIRECT = "Redirect";
     58     private static final String ELEMENT_NAME_USER = "User";
     59     private static final String ELEMENT_NAME_EMAIL_ADDRESS = "EMailAddress";
     60     private static final String ELEMENT_NAME_DISPLAY_NAME = "DisplayName";
     61     private static final String ELEMENT_NAME_RESPONSE = "Response";
     62     private static final String ELEMENT_NAME_AUTODISCOVER = "Autodiscover";
     63 
     64     private final int mAttemptNumber;
     65     private final String mUri;
     66     private final String mUsername;
     67     private final String mPassword;
     68     private HostAuth mHostAuth;
     69     private String mRedirectUri;
     70 
     71 
     72     private static Account makeAccount(final String username, final String password) {
     73         final HostAuth hostAuth = new HostAuth();
     74         hostAuth.mLogin = username;
     75         hostAuth.mPassword = password;
     76         hostAuth.mPort = 443;
     77         hostAuth.mProtocol = Eas.PROTOCOL;
     78         hostAuth.mFlags = HostAuth.FLAG_SSL | HostAuth.FLAG_AUTHENTICATE;
     79         final Account account = new Account();
     80         account.mEmailAddress = username;
     81         account.mHostAuthRecv = hostAuth;
     82         return account;
     83     }
     84 
     85     public EasAutoDiscover(final Context context, final String uri, final int attemptNumber,
     86                            final String username, final String password) {
     87         // We don't actually need an account or a hostAuth, but the EasServerConnection requires
     88         // one. Just create dummy values.
     89         super(context, makeAccount(username, password));
     90         mAttemptNumber = attemptNumber;
     91         mUri = uri;
     92         mUsername = username;
     93         mPassword = password;
     94         mHostAuth = mAccount.mHostAuthRecv;
     95     }
     96 
     97     public static String genUri(final String domain, final int attemptNumber) {
     98         // Try the following uris in order, as per
     99         // http://msdn.microsoft.com/en-us/library/office/jj900169(v=exchg.150).aspx
    100         // TODO: That document also describes a fallback strategy to query DNS for an SRV record,
    101         // but this would require additional DNS lookup services that are not currently available
    102         // in the android platform,
    103         switch (attemptNumber) {
    104             case ATTEMPT_PRIMARY:
    105                 return "https://" + domain + AUTO_DISCOVER_PAGE;
    106             case ATTEMPT_ALTERNATE:
    107                 return "https://autodiscover." + domain + AUTO_DISCOVER_PAGE;
    108             case ATTEMPT_UNAUTHENTICATED_GET:
    109                 return "http://autodiscover." + domain + AUTO_DISCOVER_PAGE;
    110             default:
    111                 LogUtils.wtf(TAG, "Illegal attempt number %d", attemptNumber);
    112                 return null;
    113         }
    114     }
    115 
    116     protected String getRequestUri() {
    117         return mUri;
    118     }
    119 
    120     public static String getDomain(final String login) {
    121         final int amp = login.indexOf('@');
    122         if (amp < 0) {
    123             return null;
    124         }
    125         return login.substring(amp + 1);
    126     }
    127 
    128     @Override
    129     protected String getCommand() {
    130         return null;
    131     }
    132 
    133     @Override
    134     protected HttpEntity getRequestEntity() throws IOException, MessageInvalidException {
    135         try {
    136             final XmlSerializer s = Xml.newSerializer();
    137             final ByteArrayOutputStream os = new ByteArrayOutputStream(1024);
    138             s.setOutput(os, "UTF-8");
    139             s.startDocument("UTF-8", false);
    140             s.startTag(null, "Autodiscover");
    141             s.attribute(null, "xmlns", AUTO_DISCOVER_SCHEMA_PREFIX + "requestschema/2006");
    142             s.startTag(null, "Request");
    143             s.startTag(null, "EMailAddress").text(mUsername).endTag(null, "EMailAddress");
    144             s.startTag(null, "AcceptableResponseSchema");
    145             s.text(AUTO_DISCOVER_SCHEMA_PREFIX + "responseschema/2006");
    146             s.endTag(null, "AcceptableResponseSchema");
    147             s.endTag(null, "Request");
    148             s.endTag(null, "Autodiscover");
    149             s.endDocument();
    150             return new StringEntity(os.toString());
    151         } catch (final IOException e) {
    152             // For all exception types, we can simply punt on autodiscover.
    153         } catch (final IllegalArgumentException e) {
    154         } catch (final IllegalStateException e) {
    155         }
    156         return null;
    157     }
    158 
    159     /**
    160      * Create the request object for this operation.
    161      * The default is to use a POST, but some use other request types (e.g. Options).
    162      * @return An {@link org.apache.http.client.methods.HttpUriRequest}.
    163      * @throws IOException
    164      */
    165     protected HttpUriRequest makeRequest() throws IOException, MessageInvalidException {
    166         final String requestUri = getRequestUri();
    167         HttpUriRequest req;
    168         if (mAttemptNumber == ATTEMPT_UNAUTHENTICATED_GET) {
    169             req = mConnection.makeGet(requestUri);
    170         } else {
    171             req = mConnection.makePost(requestUri, getRequestEntity(),
    172                     getRequestContentType(), addPolicyKeyHeaderToRequest());
    173         }
    174         return req;
    175     }
    176 
    177     public String getRedirectUri() {
    178         return mRedirectUri;
    179     }
    180 
    181     @Override
    182     protected int handleResponse(final EasResponse response) throws
    183             IOException, CommandStatusException {
    184         // resp is either an authentication error, or a good response.
    185         final int code = response.getStatus();
    186 
    187         if (response.isRedirectError()) {
    188             final String loc = response.getRedirectAddress();
    189             if (loc != null && loc.startsWith("http")) {
    190                 LogUtils.d(TAG, "Posting autodiscover to redirect: " + loc);
    191                 mRedirectUri = loc;
    192                 return RESULT_REDIRECT;
    193             } else {
    194                 LogUtils.w(TAG, "Invalid redirect %s", loc);
    195                 return RESULT_FATAL_SERVER_ERROR;
    196             }
    197         }
    198 
    199         if (code == HttpStatus.SC_UNAUTHORIZED) {
    200             LogUtils.w(TAG, "Autodiscover received SC_UNAUTHORIZED");
    201             return RESULT_SC_UNAUTHORIZED;
    202         } else if (code != HttpStatus.SC_OK) {
    203             // We'll try the next address if this doesn't work
    204             LogUtils.d(TAG, "Bad response code when posting autodiscover: %d", code);
    205             return RESULT_BAD_RESPONSE;
    206         } else {
    207             mHostAuth = parseAutodiscover(response);
    208             if (mHostAuth != null) {
    209                 // Fill in the rest of the HostAuth
    210                 // We use the user name and password that were successful during
    211                 // the autodiscover process
    212                 mHostAuth.mLogin = mUsername;
    213                 mHostAuth.mPassword = mPassword;
    214                 // Note: there is no way we can auto-discover the proper client
    215                 // SSL certificate to use, if one is needed.
    216                 mHostAuth.mPort = 443;
    217                 mHostAuth.mProtocol = Eas.PROTOCOL;
    218                 mHostAuth.mFlags = HostAuth.FLAG_SSL | HostAuth.FLAG_AUTHENTICATE;
    219                 return RESULT_OK;
    220             } else {
    221                 return RESULT_HARD_DATA_FAILURE;
    222             }
    223         }
    224     }
    225 
    226     public Bundle getResultBundle() {
    227         final Bundle bundle = new Bundle(2);
    228         final HostAuthCompat hostAuthCompat = new HostAuthCompat(mHostAuth);
    229         bundle.putParcelable(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_HOST_AUTH,
    230                 hostAuthCompat);
    231         bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
    232                 RESULT_OK);
    233         return bundle;
    234     }
    235 
    236     /**
    237      * Parse the Server element of the server response.
    238      * @param parser The {@link XmlPullParser}.
    239      * @param hostAuth The {@link HostAuth} to populate with the results of parsing.
    240      * @throws XmlPullParserException
    241      * @throws IOException
    242      */
    243     private static void parseServer(final XmlPullParser parser, final HostAuth hostAuth)
    244             throws XmlPullParserException, IOException {
    245         boolean mobileSync = false;
    246         while (true) {
    247             final int type = parser.next();
    248             if (type == XmlPullParser.END_TAG && parser.getName().equals(ELEMENT_NAME_SERVER)) {
    249                 break;
    250             } else if (type == XmlPullParser.START_TAG) {
    251                 final String name = parser.getName();
    252                 if (name.equals(ELEMENT_NAME_TYPE)) {
    253                     if (parser.nextText().equals(ELEMENT_NAME_MOBILE_SYNC)) {
    254                         mobileSync = true;
    255                     }
    256                 } else if (mobileSync && name.equals(ELEMENT_NAME_URL)) {
    257                     final String url = parser.nextText();
    258                     if (url != null) {
    259                         LogUtils.d(TAG, "Autodiscover URL: %s", url);
    260                         hostAuth.mAddress = Uri.parse(url).getHost();
    261                     }
    262                 }
    263             }
    264         }
    265     }
    266 
    267     /**
    268      * Parse the Settings element of the server response.
    269      * @param parser The {@link XmlPullParser}.
    270      * @param hostAuth The {@link HostAuth} to populate with the results of parsing.
    271      * @throws XmlPullParserException
    272      * @throws IOException
    273      */
    274     private static void parseSettings(final XmlPullParser parser, final HostAuth hostAuth)
    275             throws XmlPullParserException, IOException {
    276         while (true) {
    277             final int type = parser.next();
    278             if (type == XmlPullParser.END_TAG && parser.getName().equals(ELEMENT_NAME_SETTINGS)) {
    279                 break;
    280             } else if (type == XmlPullParser.START_TAG) {
    281                 final String name = parser.getName();
    282                 if (name.equals(ELEMENT_NAME_SERVER)) {
    283                     parseServer(parser, hostAuth);
    284                 }
    285             }
    286         }
    287     }
    288 
    289     /**
    290      * Parse the Action element of the server response.
    291      * @param parser The {@link XmlPullParser}.
    292      * @param hostAuth The {@link HostAuth} to populate with the results of parsing.
    293      * @throws XmlPullParserException
    294      * @throws IOException
    295      */
    296     private static void parseAction(final XmlPullParser parser, final HostAuth hostAuth)
    297             throws XmlPullParserException, IOException {
    298         while (true) {
    299             final int type = parser.next();
    300             if (type == XmlPullParser.END_TAG && parser.getName().equals(ELEMENT_NAME_ACTION)) {
    301                 break;
    302             } else if (type == XmlPullParser.START_TAG) {
    303                 final String name = parser.getName();
    304                 if (name.equals(ELEMENT_NAME_ERROR)) {
    305                     // Should parse the error
    306                 } else if (name.equals(ELEMENT_NAME_REDIRECT)) {
    307                     LogUtils.d(TAG, "Redirect: " + parser.nextText());
    308                 } else if (name.equals(ELEMENT_NAME_SETTINGS)) {
    309                     parseSettings(parser, hostAuth);
    310                 }
    311             }
    312         }
    313     }
    314 
    315     /**
    316      * Parse the User element of the server response.
    317      * @param parser The {@link XmlPullParser}.
    318      * @param hostAuth The {@link HostAuth} to populate with the results of parsing.
    319      * @throws XmlPullParserException
    320      * @throws IOException
    321      */
    322     private static void parseUser(final XmlPullParser parser, final HostAuth hostAuth)
    323             throws XmlPullParserException, IOException {
    324         while (true) {
    325             int type = parser.next();
    326             if (type == XmlPullParser.END_TAG && parser.getName().equals(ELEMENT_NAME_USER)) {
    327                 break;
    328             } else if (type == XmlPullParser.START_TAG) {
    329                 String name = parser.getName();
    330                 if (name.equals(ELEMENT_NAME_EMAIL_ADDRESS)) {
    331                     final String addr = parser.nextText();
    332                     LogUtils.d(TAG, "Autodiscover, email: %s", addr);
    333                 } else if (name.equals(ELEMENT_NAME_DISPLAY_NAME)) {
    334                     final String dn = parser.nextText();
    335                     LogUtils.d(TAG, "Autodiscover, user: %s", dn);
    336                 }
    337             }
    338         }
    339     }
    340 
    341     /**
    342      * Parse the Response element of the server response.
    343      * @param parser The {@link XmlPullParser}.
    344      * @param hostAuth The {@link HostAuth} to populate with the results of parsing.
    345      * @throws XmlPullParserException
    346      * @throws IOException
    347      */
    348     private static void parseResponse(final XmlPullParser parser, final HostAuth hostAuth)
    349             throws XmlPullParserException, IOException {
    350         while (true) {
    351             final int type = parser.next();
    352             if (type == XmlPullParser.END_TAG && parser.getName().equals(ELEMENT_NAME_RESPONSE)) {
    353                 break;
    354             } else if (type == XmlPullParser.START_TAG) {
    355                 final String name = parser.getName();
    356                 if (name.equals(ELEMENT_NAME_USER)) {
    357                     parseUser(parser, hostAuth);
    358                 } else if (name.equals(ELEMENT_NAME_ACTION)) {
    359                     parseAction(parser, hostAuth);
    360                 }
    361             }
    362         }
    363     }
    364 
    365     /**
    366      * Parse the server response for the final {@link HostAuth}.
    367      * @param resp The {@link EasResponse} from the server.
    368      * @return The final {@link HostAuth} for this server.
    369      */
    370     private static HostAuth parseAutodiscover(final EasResponse resp) {
    371         // The response to Autodiscover is regular XML (not WBXML)
    372         try {
    373             final XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
    374             parser.setInput(resp.getInputStream(), "UTF-8");
    375             if (parser.getEventType() != XmlPullParser.START_DOCUMENT) {
    376                 return null;
    377             }
    378             if (parser.next() != XmlPullParser.START_TAG) {
    379                 return null;
    380             }
    381             if (!parser.getName().equals(ELEMENT_NAME_AUTODISCOVER)) {
    382                 return null;
    383             }
    384 
    385             final HostAuth hostAuth = new HostAuth();
    386             while (true) {
    387                 final int type = parser.nextTag();
    388                 if (type == XmlPullParser.END_TAG && parser.getName()
    389                         .equals(ELEMENT_NAME_AUTODISCOVER)) {
    390                     break;
    391                 } else if (type == XmlPullParser.START_TAG && parser.getName()
    392                         .equals(ELEMENT_NAME_RESPONSE)) {
    393                     parseResponse(parser, hostAuth);
    394                     // Valid responses will set the address.
    395                     if (hostAuth.mAddress != null) {
    396                         return hostAuth;
    397                     }
    398                 }
    399             }
    400         } catch (final XmlPullParserException e) {
    401             // Parse error.
    402         } catch (final IOException e) {
    403             // Error reading parser.
    404         }
    405         return null;
    406     }
    407 }
    408