1package com.android.exchange.eas;
2
3import android.content.Context;
4import android.net.Uri;
5import android.os.Bundle;
6import android.util.Xml;
7
8import com.android.emailcommon.provider.Account;
9import com.android.emailcommon.provider.HostAuth;
10import com.android.emailcommon.service.EmailServiceProxy;
11import com.android.emailcommon.service.HostAuthCompat;
12import com.android.exchange.CommandStatusException;
13import com.android.exchange.Eas;
14import com.android.exchange.EasResponse;
15import com.android.mail.utils.LogUtils;
16
17import org.apache.http.HttpEntity;
18import org.apache.http.HttpStatus;
19import org.apache.http.client.methods.HttpUriRequest;
20import org.apache.http.entity.StringEntity;
21import org.xmlpull.v1.XmlPullParser;
22import org.xmlpull.v1.XmlPullParserException;
23import org.xmlpull.v1.XmlPullParserFactory;
24import org.xmlpull.v1.XmlSerializer;
25
26import java.io.ByteArrayOutputStream;
27import java.io.IOException;
28
29public 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