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