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                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