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                 if (mHostAuth.mPort == -1) {
    217                     mHostAuth.mPort = 443;
    218                 }
    219                 mHostAuth.mProtocol = Eas.PROTOCOL;
    220                 mHostAuth.mFlags = HostAuth.FLAG_SSL | HostAuth.FLAG_AUTHENTICATE;
    221                 return RESULT_OK;
    222             } else {
    223                 return RESULT_HARD_DATA_FAILURE;
    224             }
    225         }
    226     }
    227 
    228     public Bundle getResultBundle() {
    229         final Bundle bundle = new Bundle(2);
    230         final HostAuthCompat hostAuthCompat = new HostAuthCompat(mHostAuth);
    231         bundle.putParcelable(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_HOST_AUTH,
    232                 hostAuthCompat);
    233         bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
    234                 RESULT_OK);
    235         return bundle;
    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                         final Uri uri = Uri.parse(url);
    263                         hostAuth.mAddress = uri.getHost();
    264                         int port = uri.getPort();
    265                         if (port != -1) {
    266                             hostAuth.mPort = port;
    267                         }
    268                     }
    269                 }
    270             }
    271         }
    272     }
    273 
    274     /**
    275      * Parse the Settings element of the server response.
    276      * @param parser The {@link XmlPullParser}.
    277      * @param hostAuth The {@link HostAuth} to populate with the results of parsing.
    278      * @throws XmlPullParserException
    279      * @throws IOException
    280      */
    281     private static void parseSettings(final XmlPullParser parser, final HostAuth hostAuth)
    282             throws XmlPullParserException, IOException {
    283         while (true) {
    284             final int type = parser.next();
    285             if (type == XmlPullParser.END_TAG && parser.getName().equals(ELEMENT_NAME_SETTINGS)) {
    286                 break;
    287             } else if (type == XmlPullParser.START_TAG) {
    288                 final String name = parser.getName();
    289                 if (name.equals(ELEMENT_NAME_SERVER)) {
    290                     parseServer(parser, hostAuth);
    291                 }
    292             }
    293         }
    294     }
    295 
    296     /**
    297      * Parse the Action element of the server response.
    298      * @param parser The {@link XmlPullParser}.
    299      * @param hostAuth The {@link HostAuth} to populate with the results of parsing.
    300      * @throws XmlPullParserException
    301      * @throws IOException
    302      */
    303     private static void parseAction(final XmlPullParser parser, final HostAuth hostAuth)
    304             throws XmlPullParserException, IOException {
    305         while (true) {
    306             final int type = parser.next();
    307             if (type == XmlPullParser.END_TAG && parser.getName().equals(ELEMENT_NAME_ACTION)) {
    308                 break;
    309             } else if (type == XmlPullParser.START_TAG) {
    310                 final String name = parser.getName();
    311                 if (name.equals(ELEMENT_NAME_ERROR)) {
    312                     // Should parse the error
    313                 } else if (name.equals(ELEMENT_NAME_REDIRECT)) {
    314                     LogUtils.d(TAG, "Redirect: " + parser.nextText());
    315                 } else if (name.equals(ELEMENT_NAME_SETTINGS)) {
    316                     parseSettings(parser, hostAuth);
    317                 }
    318             }
    319         }
    320     }
    321 
    322     /**
    323      * Parse the User element of the server response.
    324      * @param parser The {@link XmlPullParser}.
    325      * @param hostAuth The {@link HostAuth} to populate with the results of parsing.
    326      * @throws XmlPullParserException
    327      * @throws IOException
    328      */
    329     private static void parseUser(final XmlPullParser parser, final HostAuth hostAuth)
    330             throws XmlPullParserException, IOException {
    331         while (true) {
    332             int type = parser.next();
    333             if (type == XmlPullParser.END_TAG && parser.getName().equals(ELEMENT_NAME_USER)) {
    334                 break;
    335             } else if (type == XmlPullParser.START_TAG) {
    336                 String name = parser.getName();
    337                 if (name.equals(ELEMENT_NAME_EMAIL_ADDRESS)) {
    338                     final String addr = parser.nextText();
    339                     LogUtils.d(TAG, "Autodiscover, email: %s", addr);
    340                 } else if (name.equals(ELEMENT_NAME_DISPLAY_NAME)) {
    341                     final String dn = parser.nextText();
    342                     LogUtils.d(TAG, "Autodiscover, user: %s", dn);
    343                 }
    344             }
    345         }
    346     }
    347 
    348     /**
    349      * Parse the Response element of the server response.
    350      * @param parser The {@link XmlPullParser}.
    351      * @param hostAuth The {@link HostAuth} to populate with the results of parsing.
    352      * @throws XmlPullParserException
    353      * @throws IOException
    354      */
    355     private static void parseResponse(final XmlPullParser parser, final HostAuth hostAuth)
    356             throws XmlPullParserException, IOException {
    357         while (true) {
    358             final int type = parser.next();
    359             if (type == XmlPullParser.END_TAG && parser.getName().equals(ELEMENT_NAME_RESPONSE)) {
    360                 break;
    361             } else if (type == XmlPullParser.START_TAG) {
    362                 final String name = parser.getName();
    363                 if (name.equals(ELEMENT_NAME_USER)) {
    364                     parseUser(parser, hostAuth);
    365                 } else if (name.equals(ELEMENT_NAME_ACTION)) {
    366                     parseAction(parser, hostAuth);
    367                 }
    368             }
    369         }
    370     }
    371 
    372     /**
    373      * Parse the server response for the final {@link HostAuth}.
    374      * @param resp The {@link EasResponse} from the server.
    375      * @return The final {@link HostAuth} for this server.
    376      */
    377     private static HostAuth parseAutodiscover(final EasResponse resp) {
    378         // The response to Autodiscover is regular XML (not WBXML)
    379         try {
    380             final XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
    381             parser.setInput(resp.getInputStream(), "UTF-8");
    382             if (parser.getEventType() != XmlPullParser.START_DOCUMENT) {
    383                 return null;
    384             }
    385             if (parser.next() != XmlPullParser.START_TAG) {
    386                 return null;
    387             }
    388             if (!parser.getName().equals(ELEMENT_NAME_AUTODISCOVER)) {
    389                 return null;
    390             }
    391 
    392             final HostAuth hostAuth = new HostAuth();
    393             while (true) {
    394                 final int type = parser.nextTag();
    395                 if (type == XmlPullParser.END_TAG && parser.getName()
    396                         .equals(ELEMENT_NAME_AUTODISCOVER)) {
    397                     break;
    398                 } else if (type == XmlPullParser.START_TAG && parser.getName()
    399                         .equals(ELEMENT_NAME_RESPONSE)) {
    400                     parseResponse(parser, hostAuth);
    401                     // Valid responses will set the address.
    402                     if (hostAuth.mAddress != null) {
    403                         return hostAuth;
    404                     }
    405                 }
    406             }
    407         } catch (final XmlPullParserException e) {
    408             // Parse error.
    409         } catch (final IOException e) {
    410             // Error reading parser.
    411         }
    412         return null;
    413     }
    414 }
    415