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