blob: 4f7558ef858a44eedf43df72d2023c8ff058ea0f [file] [log] [blame]
package com.android.exchange.service;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.util.Xml;
import com.android.emailcommon.mail.MessagingException;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.HostAuth;
import com.android.emailcommon.service.EmailServiceProxy;
import com.android.exchange.Eas;
import com.android.exchange.EasResponse;
import com.android.mail.utils.LogUtils;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.HttpPost;
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;
import java.net.URI;
/**
* Performs Autodiscover for Exchange servers. This feature tries to find all the configuration
* options needed based on just a username and password.
*/
public class EasAutoDiscover extends EasServerConnection {
private static final String TAG = Eas.LOG_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";
public EasAutoDiscover(final Context context, final String username, final String password) {
super(context, new Account(), new HostAuth());
mHostAuth.mLogin = username;
mHostAuth.mPassword = password;
mHostAuth.mFlags = HostAuth.FLAG_AUTHENTICATE | HostAuth.FLAG_SSL;
mHostAuth.mPort = 443;
}
/**
* Do all the work of autodiscovery.
* @return A {@link Bundle} with the host information if autodiscovery succeeded. If we failed
* due to an authentication failure, we return a {@link Bundle} with no host info but with
* an appropriate error code. Otherwise, we return null.
*/
public Bundle doAutodiscover() {
final String domain = getDomain();
if (domain == null) {
return null;
}
final StringEntity entity = buildRequestEntity();
if (entity == null) {
return null;
}
try {
final HttpPost post = makePost("https://" + domain + AUTO_DISCOVER_PAGE, entity,
"text/xml", false);
final EasResponse resp = getResponse(post, domain);
if (resp == null) {
return null;
}
try {
// resp is either an authentication error, or a good response.
final int code = resp.getStatus();
if (code == HttpStatus.SC_UNAUTHORIZED) {
final Bundle bundle = new Bundle(1);
bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
MessagingException.AUTODISCOVER_AUTHENTICATION_FAILED);
return bundle;
} else {
final HostAuth hostAuth = parseAutodiscover(resp);
if (hostAuth != null) {
// Fill in the rest of the HostAuth
// We use the user name and password that were successful during
// the autodiscover process
hostAuth.mLogin = mHostAuth.mLogin;
hostAuth.mPassword = mHostAuth.mPassword;
// Note: there is no way we can auto-discover the proper client
// SSL certificate to use, if one is needed.
hostAuth.mPort = 443;
hostAuth.mProtocol = Eas.PROTOCOL;
hostAuth.mFlags = HostAuth.FLAG_SSL | HostAuth.FLAG_AUTHENTICATE;
final Bundle bundle = new Bundle(2);
bundle.putParcelable(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_HOST_AUTH,
hostAuth);
bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
MessagingException.NO_ERROR);
return bundle;
}
}
} finally {
resp.close();
}
} catch (final IllegalArgumentException e) {
// This happens when the domain is malformatted.
// TODO: Fix sanitizing of the domain -- we try to in UI but apparently not correctly.
LogUtils.e(TAG, "ISE with domain: %s", domain);
}
return null;
}
/**
* Get the domain of our account.
* @return The domain of the email address.
*/
private String getDomain() {
final int amp = mHostAuth.mLogin.indexOf('@');
if (amp < 0) {
return null;
}
return mHostAuth.mLogin.substring(amp + 1);
}
/**
* Create the payload of the request.
* @return A {@link StringEntity} for the request XML.
*/
private StringEntity buildRequestEntity() {
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(mHostAuth.mLogin).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;
}
/**
* Perform all requests necessary and get the server response. If the post fails or is
* redirected, we alter the post and retry.
* @param post The initial {@link HttpPost} for this request.
* @param domain The domain for our account.
* @return If this request succeeded or has an unrecoverable authentication error, an
* {@link EasResponse} with the details. For other errors, we return null.
*/
private EasResponse getResponse(final HttpPost post, final String domain) {
EasResponse resp = doPost(post, true);
if (resp == null) {
LogUtils.d(TAG, "Error in autodiscover, trying aternate address");
post.setURI(URI.create("https://autodiscover." + domain + AUTO_DISCOVER_PAGE));
resp = doPost(post, true);
}
return resp;
}
/**
* Perform one attempt to get autodiscover information. Redirection and some authentication
* errors are handled by recursively calls with modified host information.
* @param post The {@link HttpPost} for this request.
* @param canRetry Whether we can retry after an authentication failure.
* @return If this request succeeded or has an unrecoverable authentication error, an
* {@link EasResponse} with the details. For other errors, we return null.
*/
private EasResponse doPost(final HttpPost post, final boolean canRetry) {
final EasResponse resp;
try {
resp = executePost(post);
} catch (final IOException e) {
return null;
}
final int code = resp.getStatus();
if (resp.isRedirectError()) {
final String loc = resp.getRedirectAddress();
if (loc != null && loc.startsWith("http")) {
LogUtils.d(TAG, "Posting autodiscover to redirect: " + loc);
redirectHostAuth(loc);
post.setURI(URI.create(loc));
return doPost(post, canRetry);
}
return null;
}
if (code == HttpStatus.SC_UNAUTHORIZED) {
if (canRetry && mHostAuth.mLogin.contains("@")) {
// Try again using the bare user name
final int atSignIndex = mHostAuth.mLogin.indexOf('@');
mHostAuth.mLogin = mHostAuth.mLogin.substring(0, atSignIndex);
LogUtils.d(TAG, "401 received; trying username: %s", mHostAuth.mLogin);
resetAuthorization(post);
return doPost(post, false);
}
} 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 null;
}
return resp;
}
/**
* 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;
}
}