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