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 
makeAccount(final String username, final String password)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 
EasAutoDiscover(final Context context, final String uri, final int attemptNumber, final String username, final String password)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 
genUri(final String domain, final int attemptNumber)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 
getRequestUri()116     protected String getRequestUri() {
117         return mUri;
118     }
119 
getDomain(final String login)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
getCommand()129     protected String getCommand() {
130         return null;
131     }
132 
133     @Override
getRequestEntity()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      */
makeRequest()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 
getRedirectUri()177     public String getRedirectUri() {
178         return mRedirectUri;
179     }
180 
181     @Override
handleResponse(final EasResponse response)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 
getResultBundle()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      */
parseServer(final XmlPullParser parser, final HostAuth hostAuth)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      */
parseSettings(final XmlPullParser parser, final HostAuth hostAuth)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      */
parseAction(final XmlPullParser parser, final HostAuth hostAuth)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      */
parseUser(final XmlPullParser parser, final HostAuth hostAuth)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      */
parseResponse(final XmlPullParser parser, final HostAuth hostAuth)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      */
parseAutodiscover(final EasResponse resp)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