1 package com.android.exchange.service; 2 3 import android.content.Context; 4 import android.net.Uri; 5 import android.os.Bundle; 6 import android.util.Xml; 7 8 import com.android.emailcommon.mail.MessagingException; 9 import com.android.emailcommon.provider.Account; 10 import com.android.emailcommon.provider.HostAuth; 11 import com.android.emailcommon.service.EmailServiceProxy; 12 import com.android.exchange.Eas; 13 import com.android.exchange.EasResponse; 14 import com.android.mail.utils.LogUtils; 15 16 import org.apache.http.HttpStatus; 17 import org.apache.http.client.methods.HttpPost; 18 import org.apache.http.entity.StringEntity; 19 import org.xmlpull.v1.XmlPullParser; 20 import org.xmlpull.v1.XmlPullParserException; 21 import org.xmlpull.v1.XmlPullParserFactory; 22 import org.xmlpull.v1.XmlSerializer; 23 24 import java.io.ByteArrayOutputStream; 25 import java.io.IOException; 26 import java.net.URI; 27 28 /** 29 * Performs Autodiscover for Exchange servers. This feature tries to find all the configuration 30 * options needed based on just a username and password. 31 */ 32 public class EasAutoDiscover extends EasServerConnection { 33 private static final String TAG = Eas.LOG_TAG; 34 35 private static final String AUTO_DISCOVER_SCHEMA_PREFIX = 36 "http://schemas.microsoft.com/exchange/autodiscover/mobilesync/"; 37 private static final String AUTO_DISCOVER_PAGE = "/autodiscover/autodiscover.xml"; 38 39 // Set of string constants for parsing the autodiscover response. 40 // TODO: Merge this into Tags.java? It's not quite the same but conceptually belongs there. 41 private static final String ELEMENT_NAME_SERVER = "Server"; 42 private static final String ELEMENT_NAME_TYPE = "Type"; 43 private static final String ELEMENT_NAME_MOBILE_SYNC = "MobileSync"; 44 private static final String ELEMENT_NAME_URL = "Url"; 45 private static final String ELEMENT_NAME_SETTINGS = "Settings"; 46 private static final String ELEMENT_NAME_ACTION = "Action"; 47 private static final String ELEMENT_NAME_ERROR = "Error"; 48 private static final String ELEMENT_NAME_REDIRECT = "Redirect"; 49 private static final String ELEMENT_NAME_USER = "User"; 50 private static final String ELEMENT_NAME_EMAIL_ADDRESS = "EMailAddress"; 51 private static final String ELEMENT_NAME_DISPLAY_NAME = "DisplayName"; 52 private static final String ELEMENT_NAME_RESPONSE = "Response"; 53 private static final String ELEMENT_NAME_AUTODISCOVER = "Autodiscover"; 54 55 public EasAutoDiscover(final Context context, final String username, final String password) { 56 super(context, new Account(), new HostAuth()); 57 mHostAuth.mLogin = username; 58 mHostAuth.mPassword = password; 59 mHostAuth.mFlags = HostAuth.FLAG_AUTHENTICATE | HostAuth.FLAG_SSL; 60 mHostAuth.mPort = 443; 61 } 62 63 /** 64 * Do all the work of autodiscovery. 65 * @return A {@link Bundle} with the host information if autodiscovery succeeded. If we failed 66 * due to an authentication failure, we return a {@link Bundle} with no host info but with 67 * an appropriate error code. Otherwise, we return null. 68 */ 69 public Bundle doAutodiscover() { 70 final String domain = getDomain(); 71 if (domain == null) { 72 return null; 73 } 74 75 final StringEntity entity = buildRequestEntity(); 76 if (entity == null) { 77 return null; 78 } 79 try { 80 final HttpPost post = makePost("https://" + domain + AUTO_DISCOVER_PAGE, entity, 81 "text/xml", false); 82 final EasResponse resp = getResponse(post, domain); 83 if (resp == null) { 84 return null; 85 } 86 87 try { 88 // resp is either an authentication error, or a good response. 89 final int code = resp.getStatus(); 90 if (code == HttpStatus.SC_UNAUTHORIZED) { 91 final Bundle bundle = new Bundle(1); 92 bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE, 93 MessagingException.AUTODISCOVER_AUTHENTICATION_FAILED); 94 return bundle; 95 } else { 96 final HostAuth hostAuth = parseAutodiscover(resp); 97 if (hostAuth != null) { 98 // Fill in the rest of the HostAuth 99 // We use the user name and password that were successful during 100 // the autodiscover process 101 hostAuth.mLogin = mHostAuth.mLogin; 102 hostAuth.mPassword = mHostAuth.mPassword; 103 // Note: there is no way we can auto-discover the proper client 104 // SSL certificate to use, if one is needed. 105 hostAuth.mPort = 443; 106 hostAuth.mProtocol = Eas.PROTOCOL; 107 hostAuth.mFlags = HostAuth.FLAG_SSL | HostAuth.FLAG_AUTHENTICATE; 108 final Bundle bundle = new Bundle(2); 109 bundle.putParcelable(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_HOST_AUTH, 110 hostAuth); 111 bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE, 112 MessagingException.NO_ERROR); 113 return bundle; 114 } 115 } 116 } finally { 117 resp.close(); 118 } 119 } catch (final IllegalArgumentException e) { 120 // This happens when the domain is malformatted. 121 // TODO: Fix sanitizing of the domain -- we try to in UI but apparently not correctly. 122 LogUtils.e(TAG, "ISE with domain: %s", domain); 123 } 124 return null; 125 } 126 127 /** 128 * Get the domain of our account. 129 * @return The domain of the email address. 130 */ 131 private String getDomain() { 132 final int amp = mHostAuth.mLogin.indexOf('@'); 133 if (amp < 0) { 134 return null; 135 } 136 return mHostAuth.mLogin.substring(amp + 1); 137 } 138 139 /** 140 * Create the payload of the request. 141 * @return A {@link StringEntity} for the request XML. 142 */ 143 private StringEntity buildRequestEntity() { 144 try { 145 final XmlSerializer s = Xml.newSerializer(); 146 final ByteArrayOutputStream os = new ByteArrayOutputStream(1024); 147 s.setOutput(os, "UTF-8"); 148 s.startDocument("UTF-8", false); 149 s.startTag(null, "Autodiscover"); 150 s.attribute(null, "xmlns", AUTO_DISCOVER_SCHEMA_PREFIX + "requestschema/2006"); 151 s.startTag(null, "Request"); 152 s.startTag(null, "EMailAddress").text(mHostAuth.mLogin).endTag(null, "EMailAddress"); 153 s.startTag(null, "AcceptableResponseSchema"); 154 s.text(AUTO_DISCOVER_SCHEMA_PREFIX + "responseschema/2006"); 155 s.endTag(null, "AcceptableResponseSchema"); 156 s.endTag(null, "Request"); 157 s.endTag(null, "Autodiscover"); 158 s.endDocument(); 159 return new StringEntity(os.toString()); 160 } catch (final IOException e) { 161 // For all exception types, we can simply punt on autodiscover. 162 } catch (final IllegalArgumentException e) { 163 } catch (final IllegalStateException e) { 164 } 165 166 return null; 167 } 168 169 /** 170 * Perform all requests necessary and get the server response. If the post fails or is 171 * redirected, we alter the post and retry. 172 * @param post The initial {@link HttpPost} for this request. 173 * @param domain The domain for our account. 174 * @return If this request succeeded or has an unrecoverable authentication error, an 175 * {@link EasResponse} with the details. For other errors, we return null. 176 */ 177 private EasResponse getResponse(final HttpPost post, final String domain) { 178 EasResponse resp = doPost(post, true); 179 if (resp == null) { 180 LogUtils.d(TAG, "Error in autodiscover, trying aternate address"); 181 post.setURI(URI.create("https://autodiscover." + domain + AUTO_DISCOVER_PAGE)); 182 resp = doPost(post, true); 183 } 184 return resp; 185 } 186 187 /** 188 * Perform one attempt to get autodiscover information. Redirection and some authentication 189 * errors are handled by recursively calls with modified host information. 190 * @param post The {@link HttpPost} for this request. 191 * @param canRetry Whether we can retry after an authentication failure. 192 * @return If this request succeeded or has an unrecoverable authentication error, an 193 * {@link EasResponse} with the details. For other errors, we return null. 194 */ 195 private EasResponse doPost(final HttpPost post, final boolean canRetry) { 196 final EasResponse resp; 197 try { 198 resp = executePost(post); 199 } catch (final IOException e) { 200 return null; 201 } 202 203 final int code = resp.getStatus(); 204 205 if (resp.isRedirectError()) { 206 final String loc = resp.getRedirectAddress(); 207 if (loc != null && loc.startsWith("http")) { 208 LogUtils.d(TAG, "Posting autodiscover to redirect: " + loc); 209 redirectHostAuth(loc); 210 post.setURI(URI.create(loc)); 211 return doPost(post, canRetry); 212 } 213 return null; 214 } 215 216 if (code == HttpStatus.SC_UNAUTHORIZED) { 217 if (canRetry && mHostAuth.mLogin.contains("@")) { 218 // Try again using the bare user name 219 final int atSignIndex = mHostAuth.mLogin.indexOf('@'); 220 mHostAuth.mLogin = mHostAuth.mLogin.substring(0, atSignIndex); 221 LogUtils.d(TAG, "401 received; trying username: %s", mHostAuth.mLogin); 222 resetAuthorization(post); 223 return doPost(post, false); 224 } 225 } else if (code != HttpStatus.SC_OK) { 226 // We'll try the next address if this doesn't work 227 LogUtils.d(TAG, "Bad response code when posting autodiscover: %d", code); 228 return null; 229 } 230 231 return resp; 232 } 233 234 /** 235 * Parse the Server element of the server response. 236 * @param parser The {@link XmlPullParser}. 237 * @param hostAuth The {@link HostAuth} to populate with the results of parsing. 238 * @throws XmlPullParserException 239 * @throws IOException 240 */ 241 private static void parseServer(final XmlPullParser parser, final HostAuth hostAuth) 242 throws XmlPullParserException, IOException { 243 boolean mobileSync = false; 244 while (true) { 245 final int type = parser.next(); 246 if (type == XmlPullParser.END_TAG && parser.getName().equals(ELEMENT_NAME_SERVER)) { 247 break; 248 } else if (type == XmlPullParser.START_TAG) { 249 final String name = parser.getName(); 250 if (name.equals(ELEMENT_NAME_TYPE)) { 251 if (parser.nextText().equals(ELEMENT_NAME_MOBILE_SYNC)) { 252 mobileSync = true; 253 } 254 } else if (mobileSync && name.equals(ELEMENT_NAME_URL)) { 255 final String url = parser.nextText(); 256 if (url != null) { 257 LogUtils.d(TAG, "Autodiscover URL: %s", url); 258 hostAuth.mAddress = Uri.parse(url).getHost(); 259 } 260 } 261 } 262 } 263 } 264 265 /** 266 * Parse the Settings element of the server response. 267 * @param parser The {@link XmlPullParser}. 268 * @param hostAuth The {@link HostAuth} to populate with the results of parsing. 269 * @throws XmlPullParserException 270 * @throws IOException 271 */ 272 private static void parseSettings(final XmlPullParser parser, final HostAuth hostAuth) 273 throws XmlPullParserException, IOException { 274 while (true) { 275 final int type = parser.next(); 276 if (type == XmlPullParser.END_TAG && parser.getName().equals(ELEMENT_NAME_SETTINGS)) { 277 break; 278 } else if (type == XmlPullParser.START_TAG) { 279 final String name = parser.getName(); 280 if (name.equals(ELEMENT_NAME_SERVER)) { 281 parseServer(parser, hostAuth); 282 } 283 } 284 } 285 } 286 287 /** 288 * Parse the Action element of the server response. 289 * @param parser The {@link XmlPullParser}. 290 * @param hostAuth The {@link HostAuth} to populate with the results of parsing. 291 * @throws XmlPullParserException 292 * @throws IOException 293 */ 294 private static void parseAction(final XmlPullParser parser, final HostAuth hostAuth) 295 throws XmlPullParserException, IOException { 296 while (true) { 297 final int type = parser.next(); 298 if (type == XmlPullParser.END_TAG && parser.getName().equals(ELEMENT_NAME_ACTION)) { 299 break; 300 } else if (type == XmlPullParser.START_TAG) { 301 final String name = parser.getName(); 302 if (name.equals(ELEMENT_NAME_ERROR)) { 303 // Should parse the error 304 } else if (name.equals(ELEMENT_NAME_REDIRECT)) { 305 LogUtils.d(TAG, "Redirect: " + parser.nextText()); 306 } else if (name.equals(ELEMENT_NAME_SETTINGS)) { 307 parseSettings(parser, hostAuth); 308 } 309 } 310 } 311 } 312 313 /** 314 * Parse the User element of the server response. 315 * @param parser The {@link XmlPullParser}. 316 * @param hostAuth The {@link HostAuth} to populate with the results of parsing. 317 * @throws XmlPullParserException 318 * @throws IOException 319 */ 320 private static void parseUser(final XmlPullParser parser, final HostAuth hostAuth) 321 throws XmlPullParserException, IOException { 322 while (true) { 323 int type = parser.next(); 324 if (type == XmlPullParser.END_TAG && parser.getName().equals(ELEMENT_NAME_USER)) { 325 break; 326 } else if (type == XmlPullParser.START_TAG) { 327 String name = parser.getName(); 328 if (name.equals(ELEMENT_NAME_EMAIL_ADDRESS)) { 329 final String addr = parser.nextText(); 330 LogUtils.d(TAG, "Autodiscover, email: %s", addr); 331 } else if (name.equals(ELEMENT_NAME_DISPLAY_NAME)) { 332 final String dn = parser.nextText(); 333 LogUtils.d(TAG, "Autodiscover, user: %s", dn); 334 } 335 } 336 } 337 } 338 339 /** 340 * Parse the Response element of the server response. 341 * @param parser The {@link XmlPullParser}. 342 * @param hostAuth The {@link HostAuth} to populate with the results of parsing. 343 * @throws XmlPullParserException 344 * @throws IOException 345 */ 346 private static void parseResponse(final XmlPullParser parser, final HostAuth hostAuth) 347 throws XmlPullParserException, IOException { 348 while (true) { 349 final int type = parser.next(); 350 if (type == XmlPullParser.END_TAG && parser.getName().equals(ELEMENT_NAME_RESPONSE)) { 351 break; 352 } else if (type == XmlPullParser.START_TAG) { 353 final String name = parser.getName(); 354 if (name.equals(ELEMENT_NAME_USER)) { 355 parseUser(parser, hostAuth); 356 } else if (name.equals(ELEMENT_NAME_ACTION)) { 357 parseAction(parser, hostAuth); 358 } 359 } 360 } 361 } 362 363 /** 364 * Parse the server response for the final {@link HostAuth}. 365 * @param resp The {@link EasResponse} from the server. 366 * @return The final {@link HostAuth} for this server. 367 */ 368 private static HostAuth parseAutodiscover(final EasResponse resp) { 369 // The response to Autodiscover is regular XML (not WBXML) 370 try { 371 final XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser(); 372 parser.setInput(resp.getInputStream(), "UTF-8"); 373 if (parser.getEventType() != XmlPullParser.START_DOCUMENT) { 374 return null; 375 } 376 if (parser.next() != XmlPullParser.START_TAG) { 377 return null; 378 } 379 if (!parser.getName().equals(ELEMENT_NAME_AUTODISCOVER)) { 380 return null; 381 } 382 383 final HostAuth hostAuth = new HostAuth(); 384 while (true) { 385 final int type = parser.nextTag(); 386 if (type == XmlPullParser.END_TAG && parser.getName() 387 .equals(ELEMENT_NAME_AUTODISCOVER)) { 388 break; 389 } else if (type == XmlPullParser.START_TAG && parser.getName() 390 .equals(ELEMENT_NAME_RESPONSE)) { 391 parseResponse(parser, hostAuth); 392 // Valid responses will set the address. 393 if (hostAuth.mAddress != null) { 394 return hostAuth; 395 } 396 } 397 } 398 } catch (final XmlPullParserException e) { 399 // Parse error. 400 } catch (final IOException e) { 401 // Error reading parser. 402 } 403 return null; 404 } 405 } 406