summaryrefslogtreecommitdiffstats
path: root/src/com/android/exchange/eas/EasAutoDiscover.java
blob: cbcb032b4c2f59035668dc5b60eab002817b75e8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
package com.android.exchange.eas;

import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.util.Xml;

import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.HostAuth;
import com.android.emailcommon.service.EmailServiceProxy;
import com.android.emailcommon.service.HostAuthCompat;
import com.android.exchange.CommandStatusException;
import com.android.exchange.Eas;
import com.android.exchange.EasResponse;
import com.android.mail.utils.LogUtils;

import org.apache.http.HttpEntity;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.entity.StringEntity;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import org.xmlpull.v1.XmlSerializer;

import java.io.ByteArrayOutputStream;
import java.io.IOException;

public class EasAutoDiscover extends EasOperation {

    public final static int ATTEMPT_PRIMARY = 0;
    public final static int ATTEMPT_ALTERNATE = 1;
    public final static int ATTEMPT_UNAUTHENTICATED_GET = 2;
    public final static int ATTEMPT_MAX = 2;

    public final static int RESULT_OK = 1;
    public final static int RESULT_SC_UNAUTHORIZED = RESULT_OP_SPECIFIC_ERROR_RESULT - 0;
    public final static int RESULT_REDIRECT = RESULT_OP_SPECIFIC_ERROR_RESULT - 1;
    public final static int RESULT_BAD_RESPONSE = RESULT_OP_SPECIFIC_ERROR_RESULT - 2;
    public final static int RESULT_FATAL_SERVER_ERROR = RESULT_OP_SPECIFIC_ERROR_RESULT - 3;

    private final static String TAG = LogUtils.TAG;

    private static final String AUTO_DISCOVER_SCHEMA_PREFIX =
            "http://schemas.microsoft.com/exchange/autodiscover/mobilesync/";
    private static final String AUTO_DISCOVER_PAGE = "/autodiscover/autodiscover.xml";

    // Set of string constants for parsing the autodiscover response.
    // TODO: Merge this into Tags.java? It's not quite the same but conceptually belongs there.
    private static final String ELEMENT_NAME_SERVER = "Server";
    private static final String ELEMENT_NAME_TYPE = "Type";
    private static final String ELEMENT_NAME_MOBILE_SYNC = "MobileSync";
    private static final String ELEMENT_NAME_URL = "Url";
    private static final String ELEMENT_NAME_SETTINGS = "Settings";
    private static final String ELEMENT_NAME_ACTION = "Action";
    private static final String ELEMENT_NAME_ERROR = "Error";
    private static final String ELEMENT_NAME_REDIRECT = "Redirect";
    private static final String ELEMENT_NAME_USER = "User";
    private static final String ELEMENT_NAME_EMAIL_ADDRESS = "EMailAddress";
    private static final String ELEMENT_NAME_DISPLAY_NAME = "DisplayName";
    private static final String ELEMENT_NAME_RESPONSE = "Response";
    private static final String ELEMENT_NAME_AUTODISCOVER = "Autodiscover";

    private final int mAttemptNumber;
    private final String mUri;
    private final String mUsername;
    private final String mPassword;
    private HostAuth mHostAuth;
    private String mRedirectUri;

    public EasAutoDiscover(final Context context, final String uri, final int attemptNumber,
                           final String username, final String password) {
        // We don't actually need an account or a hostAuth, but the EasServerConnection requires
        // one. Just create dummy values.
        super(context, -1);
        mAttemptNumber = attemptNumber;
        mUri = uri;
        mUsername = username;
        mPassword = password;
        mHostAuth = new HostAuth();
        mHostAuth.mLogin = mUsername;
        mHostAuth.mPassword = mPassword;
        mHostAuth.mPort = 443;
        mHostAuth.mProtocol = Eas.PROTOCOL;
        mHostAuth.mFlags = HostAuth.FLAG_SSL | HostAuth.FLAG_AUTHENTICATE;
        setAccount(new Account(), mHostAuth);
    }

    public static String genUri(final String domain, final int attemptNumber) {
        // Try the following uris in order, as per
        // http://msdn.microsoft.com/en-us/library/office/jj900169(v=exchg.150).aspx
        // TODO: That document also describes a fallback strategy to query DNS for an SRV record,
        // but this would require additional DNS lookup services that are not currently available
        // in the android platform,
        switch (attemptNumber) {
            case ATTEMPT_PRIMARY:
                return "https://" + domain + AUTO_DISCOVER_PAGE;
            case ATTEMPT_ALTERNATE:
                return "https://autodiscover." + domain + AUTO_DISCOVER_PAGE;
            case ATTEMPT_UNAUTHENTICATED_GET:
                return "http://autodiscover." + domain + AUTO_DISCOVER_PAGE;
            default:
                LogUtils.wtf(TAG, "Illegal attempt number %d", attemptNumber);
                return null;
        }
    }

    protected String getRequestUri() {
        return mUri;
    }

    public static String getDomain(final String login) {
        final int amp = login.indexOf('@');
        if (amp < 0) {
            return null;
        }
        return login.substring(amp + 1);
    }

    @Override
    protected String getCommand() {
        return null;
    }

    @Override
    protected HttpEntity getRequestEntity() throws IOException, MessageInvalidException {
        try {
            final XmlSerializer s = Xml.newSerializer();
            final ByteArrayOutputStream os = new ByteArrayOutputStream(1024);
            s.setOutput(os, "UTF-8");
            s.startDocument("UTF-8", false);
            s.startTag(null, "Autodiscover");
            s.attribute(null, "xmlns", AUTO_DISCOVER_SCHEMA_PREFIX + "requestschema/2006");
            s.startTag(null, "Request");
            s.startTag(null, "EMailAddress").text(mUsername).endTag(null, "EMailAddress");
            s.startTag(null, "AcceptableResponseSchema");
            s.text(AUTO_DISCOVER_SCHEMA_PREFIX + "responseschema/2006");
            s.endTag(null, "AcceptableResponseSchema");
            s.endTag(null, "Request");
            s.endTag(null, "Autodiscover");
            s.endDocument();
            return new StringEntity(os.toString());
        } catch (final IOException e) {
            // For all exception types, we can simply punt on autodiscover.
        } catch (final IllegalArgumentException e) {
        } catch (final IllegalStateException e) {
        }
        return null;
    }

    /**
     * Create the request object for this operation.
     * The default is to use a POST, but some use other request types (e.g. Options).
     * @return An {@link org.apache.http.client.methods.HttpUriRequest}.
     * @throws IOException
     */
    protected HttpUriRequest makeRequest() throws IOException, MessageInvalidException {
        final String requestUri = getRequestUri();
        HttpUriRequest req;
        if (mAttemptNumber == ATTEMPT_UNAUTHENTICATED_GET) {
            req = mConnection.makeGet(requestUri);
        } else {
            req = mConnection.makePost(requestUri, getRequestEntity(),
                    getRequestContentType(), addPolicyKeyHeaderToRequest());
        }
        return req;
    }

    public String getRedirectUri() {
        return mRedirectUri;
    }

    @Override
    protected int handleResponse(final EasResponse response) throws
            IOException, CommandStatusException {
        // resp is either an authentication error, or a good response.
        final int code = response.getStatus();

        if (response.isRedirectError()) {
            final String loc = response.getRedirectAddress();
            if (loc != null && loc.startsWith("http")) {
                LogUtils.d(TAG, "Posting autodiscover to redirect: " + loc);
                mRedirectUri = loc;
                return RESULT_REDIRECT;
            } else {
                LogUtils.w(TAG, "Invalid redirect %s", loc);
                return RESULT_FATAL_SERVER_ERROR;
            }
        }

        if (code == HttpStatus.SC_UNAUTHORIZED) {
            LogUtils.w(TAG, "Autodiscover received SC_UNAUTHORIZED");
            return RESULT_SC_UNAUTHORIZED;
        } else if (code != HttpStatus.SC_OK) {
            // We'll try the next address if this doesn't work
            LogUtils.d(TAG, "Bad response code when posting autodiscover: %d", code);
            return RESULT_BAD_RESPONSE;
        } else {
            mHostAuth = parseAutodiscover(response);
            if (mHostAuth != null) {
                // Fill in the rest of the HostAuth
                // We use the user name and password that were successful during
                // the autodiscover process
                mHostAuth.mLogin = mUsername;
                mHostAuth.mPassword = mPassword;
                // Note: there is no way we can auto-discover the proper client
                // SSL certificate to use, if one is needed.
                mHostAuth.mPort = 443;
                mHostAuth.mProtocol = Eas.PROTOCOL;
                mHostAuth.mFlags = HostAuth.FLAG_SSL | HostAuth.FLAG_AUTHENTICATE;
                return RESULT_OK;
            } else {
                return RESULT_HARD_DATA_FAILURE;
            }
        }
    }

    public Bundle getResultBundle() {
        final Bundle bundle = new Bundle(2);
        final HostAuthCompat hostAuthCompat = new HostAuthCompat(mHostAuth);
        bundle.putParcelable(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_HOST_AUTH,
                hostAuthCompat);
        bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
                RESULT_OK);
        return bundle;
    }

    /**
     * Parse the Server element of the server response.
     * @param parser The {@link XmlPullParser}.
     * @param hostAuth The {@link HostAuth} to populate with the results of parsing.
     * @throws XmlPullParserException
     * @throws IOException
     */
    private static void parseServer(final XmlPullParser parser, final HostAuth hostAuth)
            throws XmlPullParserException, IOException {
        boolean mobileSync = false;
        while (true) {
            final int type = parser.next();
            if (type == XmlPullParser.END_TAG && parser.getName().equals(ELEMENT_NAME_SERVER)) {
                break;
            } else if (type == XmlPullParser.START_TAG) {
                final String name = parser.getName();
                if (name.equals(ELEMENT_NAME_TYPE)) {
                    if (parser.nextText().equals(ELEMENT_NAME_MOBILE_SYNC)) {
                        mobileSync = true;
                    }
                } else if (mobileSync && name.equals(ELEMENT_NAME_URL)) {
                    final String url = parser.nextText();
                    if (url != null) {
                        LogUtils.d(TAG, "Autodiscover URL: %s", url);
                        hostAuth.mAddress = Uri.parse(url).getHost();
                    }
                }
            }
        }
    }

    /**
     * Parse the Settings element of the server response.
     * @param parser The {@link XmlPullParser}.
     * @param hostAuth The {@link HostAuth} to populate with the results of parsing.
     * @throws XmlPullParserException
     * @throws IOException
     */
    private static void parseSettings(final XmlPullParser parser, final HostAuth hostAuth)
            throws XmlPullParserException, IOException {
        while (true) {
            final int type = parser.next();
            if (type == XmlPullParser.END_TAG && parser.getName().equals(ELEMENT_NAME_SETTINGS)) {
                break;
            } else if (type == XmlPullParser.START_TAG) {
                final String name = parser.getName();
                if (name.equals(ELEMENT_NAME_SERVER)) {
                    parseServer(parser, hostAuth);
                }
            }
        }
    }

    /**
     * Parse the Action element of the server response.
     * @param parser The {@link XmlPullParser}.
     * @param hostAuth The {@link HostAuth} to populate with the results of parsing.
     * @throws XmlPullParserException
     * @throws IOException
     */
    private static void parseAction(final XmlPullParser parser, final HostAuth hostAuth)
            throws XmlPullParserException, IOException {
        while (true) {
            final int type = parser.next();
            if (type == XmlPullParser.END_TAG && parser.getName().equals(ELEMENT_NAME_ACTION)) {
                break;
            } else if (type == XmlPullParser.START_TAG) {
                final String name = parser.getName();
                if (name.equals(ELEMENT_NAME_ERROR)) {
                    // Should parse the error
                } else if (name.equals(ELEMENT_NAME_REDIRECT)) {
                    LogUtils.d(TAG, "Redirect: " + parser.nextText());
                } else if (name.equals(ELEMENT_NAME_SETTINGS)) {
                    parseSettings(parser, hostAuth);
                }
            }
        }
    }

    /**
     * Parse the User element of the server response.
     * @param parser The {@link XmlPullParser}.
     * @param hostAuth The {@link HostAuth} to populate with the results of parsing.
     * @throws XmlPullParserException
     * @throws IOException
     */
    private static void parseUser(final XmlPullParser parser, final HostAuth hostAuth)
            throws XmlPullParserException, IOException {
        while (true) {
            int type = parser.next();
            if (type == XmlPullParser.END_TAG && parser.getName().equals(ELEMENT_NAME_USER)) {
                break;
            } else if (type == XmlPullParser.START_TAG) {
                String name = parser.getName();
                if (name.equals(ELEMENT_NAME_EMAIL_ADDRESS)) {
                    final String addr = parser.nextText();
                    LogUtils.d(TAG, "Autodiscover, email: %s", addr);
                } else if (name.equals(ELEMENT_NAME_DISPLAY_NAME)) {
                    final String dn = parser.nextText();
                    LogUtils.d(TAG, "Autodiscover, user: %s", dn);
                }
            }
        }
    }

    /**
     * Parse the Response element of the server response.
     * @param parser The {@link XmlPullParser}.
     * @param hostAuth The {@link HostAuth} to populate with the results of parsing.
     * @throws XmlPullParserException
     * @throws IOException
     */
    private static void parseResponse(final XmlPullParser parser, final HostAuth hostAuth)
            throws XmlPullParserException, IOException {
        while (true) {
            final int type = parser.next();
            if (type == XmlPullParser.END_TAG && parser.getName().equals(ELEMENT_NAME_RESPONSE)) {
                break;
            } else if (type == XmlPullParser.START_TAG) {
                final String name = parser.getName();
                if (name.equals(ELEMENT_NAME_USER)) {
                    parseUser(parser, hostAuth);
                } else if (name.equals(ELEMENT_NAME_ACTION)) {
                    parseAction(parser, hostAuth);
                }
            }
        }
    }

    /**
     * Parse the server response for the final {@link HostAuth}.
     * @param resp The {@link EasResponse} from the server.
     * @return The final {@link HostAuth} for this server.
     */
    private static HostAuth parseAutodiscover(final EasResponse resp) {
        // The response to Autodiscover is regular XML (not WBXML)
        try {
            final XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
            parser.setInput(resp.getInputStream(), "UTF-8");
            if (parser.getEventType() != XmlPullParser.START_DOCUMENT) {
                return null;
            }
            if (parser.next() != XmlPullParser.START_TAG) {
                return null;
            }
            if (!parser.getName().equals(ELEMENT_NAME_AUTODISCOVER)) {
                return null;
            }

            final HostAuth hostAuth = new HostAuth();
            while (true) {
                final int type = parser.nextTag();
                if (type == XmlPullParser.END_TAG && parser.getName()
                        .equals(ELEMENT_NAME_AUTODISCOVER)) {
                    break;
                } else if (type == XmlPullParser.START_TAG && parser.getName()
                        .equals(ELEMENT_NAME_RESPONSE)) {
                    parseResponse(parser, hostAuth);
                    // Valid responses will set the address.
                    if (hostAuth.mAddress != null) {
                        return hostAuth;
                    }
                }
            }
        } catch (final XmlPullParserException e) {
            // Parse error.
        } catch (final IOException e) {
            // Error reading parser.
        }
        return null;
    }
}