1 package com.android.exchange.eas; 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.provider.Account; 9 import com.android.emailcommon.provider.HostAuth; 10 import com.android.emailcommon.service.EmailServiceProxy; 11 import com.android.emailcommon.service.HostAuthCompat; 12 import com.android.exchange.CommandStatusException; 13 import com.android.exchange.Eas; 14 import com.android.exchange.EasResponse; 15 import com.android.mail.utils.LogUtils; 16 17 import org.apache.http.HttpEntity; 18 import org.apache.http.HttpStatus; 19 import org.apache.http.client.methods.HttpUriRequest; 20 import org.apache.http.entity.StringEntity; 21 import org.xmlpull.v1.XmlPullParser; 22 import org.xmlpull.v1.XmlPullParserException; 23 import org.xmlpull.v1.XmlPullParserFactory; 24 import org.xmlpull.v1.XmlSerializer; 25 26 import java.io.ByteArrayOutputStream; 27 import java.io.IOException; 28 29 public 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 makeAccount(final String username, final String password)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 EasAutoDiscover(final Context context, final String uri, final int attemptNumber, final String username, final String password)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 genUri(final String domain, final int attemptNumber)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 getRequestUri()116 protected String getRequestUri() { 117 return mUri; 118 } 119 getDomain(final String login)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 getCommand()129 protected String getCommand() { 130 return null; 131 } 132 133 @Override getRequestEntity()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 */ makeRequest()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 getRedirectUri()177 public String getRedirectUri() { 178 return mRedirectUri; 179 } 180 181 @Override handleResponse(final EasResponse response)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 mHostAuth.mPort = 443; 217 mHostAuth.mProtocol = Eas.PROTOCOL; 218 mHostAuth.mFlags = HostAuth.FLAG_SSL | HostAuth.FLAG_AUTHENTICATE; 219 return RESULT_OK; 220 } else { 221 return RESULT_HARD_DATA_FAILURE; 222 } 223 } 224 } 225 getResultBundle()226 public Bundle getResultBundle() { 227 final Bundle bundle = new Bundle(2); 228 final HostAuthCompat hostAuthCompat = new HostAuthCompat(mHostAuth); 229 bundle.putParcelable(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_HOST_AUTH, 230 hostAuthCompat); 231 bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE, 232 RESULT_OK); 233 return bundle; 234 } 235 236 /** 237 * Parse the Server element of the server response. 238 * @param parser The {@link XmlPullParser}. 239 * @param hostAuth The {@link HostAuth} to populate with the results of parsing. 240 * @throws XmlPullParserException 241 * @throws IOException 242 */ parseServer(final XmlPullParser parser, final HostAuth hostAuth)243 private static void parseServer(final XmlPullParser parser, final HostAuth hostAuth) 244 throws XmlPullParserException, IOException { 245 boolean mobileSync = false; 246 while (true) { 247 final int type = parser.next(); 248 if (type == XmlPullParser.END_TAG && parser.getName().equals(ELEMENT_NAME_SERVER)) { 249 break; 250 } else if (type == XmlPullParser.START_TAG) { 251 final String name = parser.getName(); 252 if (name.equals(ELEMENT_NAME_TYPE)) { 253 if (parser.nextText().equals(ELEMENT_NAME_MOBILE_SYNC)) { 254 mobileSync = true; 255 } 256 } else if (mobileSync && name.equals(ELEMENT_NAME_URL)) { 257 final String url = parser.nextText(); 258 if (url != null) { 259 LogUtils.d(TAG, "Autodiscover URL: %s", url); 260 hostAuth.mAddress = Uri.parse(url).getHost(); 261 } 262 } 263 } 264 } 265 } 266 267 /** 268 * Parse the Settings element of the server response. 269 * @param parser The {@link XmlPullParser}. 270 * @param hostAuth The {@link HostAuth} to populate with the results of parsing. 271 * @throws XmlPullParserException 272 * @throws IOException 273 */ parseSettings(final XmlPullParser parser, final HostAuth hostAuth)274 private static void parseSettings(final XmlPullParser parser, final HostAuth hostAuth) 275 throws XmlPullParserException, IOException { 276 while (true) { 277 final int type = parser.next(); 278 if (type == XmlPullParser.END_TAG && parser.getName().equals(ELEMENT_NAME_SETTINGS)) { 279 break; 280 } else if (type == XmlPullParser.START_TAG) { 281 final String name = parser.getName(); 282 if (name.equals(ELEMENT_NAME_SERVER)) { 283 parseServer(parser, hostAuth); 284 } 285 } 286 } 287 } 288 289 /** 290 * Parse the Action element of the server response. 291 * @param parser The {@link XmlPullParser}. 292 * @param hostAuth The {@link HostAuth} to populate with the results of parsing. 293 * @throws XmlPullParserException 294 * @throws IOException 295 */ parseAction(final XmlPullParser parser, final HostAuth hostAuth)296 private static void parseAction(final XmlPullParser parser, final HostAuth hostAuth) 297 throws XmlPullParserException, IOException { 298 while (true) { 299 final int type = parser.next(); 300 if (type == XmlPullParser.END_TAG && parser.getName().equals(ELEMENT_NAME_ACTION)) { 301 break; 302 } else if (type == XmlPullParser.START_TAG) { 303 final String name = parser.getName(); 304 if (name.equals(ELEMENT_NAME_ERROR)) { 305 // Should parse the error 306 } else if (name.equals(ELEMENT_NAME_REDIRECT)) { 307 LogUtils.d(TAG, "Redirect: " + parser.nextText()); 308 } else if (name.equals(ELEMENT_NAME_SETTINGS)) { 309 parseSettings(parser, hostAuth); 310 } 311 } 312 } 313 } 314 315 /** 316 * Parse the User element of the server response. 317 * @param parser The {@link XmlPullParser}. 318 * @param hostAuth The {@link HostAuth} to populate with the results of parsing. 319 * @throws XmlPullParserException 320 * @throws IOException 321 */ parseUser(final XmlPullParser parser, final HostAuth hostAuth)322 private static void parseUser(final XmlPullParser parser, final HostAuth hostAuth) 323 throws XmlPullParserException, IOException { 324 while (true) { 325 int type = parser.next(); 326 if (type == XmlPullParser.END_TAG && parser.getName().equals(ELEMENT_NAME_USER)) { 327 break; 328 } else if (type == XmlPullParser.START_TAG) { 329 String name = parser.getName(); 330 if (name.equals(ELEMENT_NAME_EMAIL_ADDRESS)) { 331 final String addr = parser.nextText(); 332 LogUtils.d(TAG, "Autodiscover, email: %s", addr); 333 } else if (name.equals(ELEMENT_NAME_DISPLAY_NAME)) { 334 final String dn = parser.nextText(); 335 LogUtils.d(TAG, "Autodiscover, user: %s", dn); 336 } 337 } 338 } 339 } 340 341 /** 342 * Parse the Response element of the server response. 343 * @param parser The {@link XmlPullParser}. 344 * @param hostAuth The {@link HostAuth} to populate with the results of parsing. 345 * @throws XmlPullParserException 346 * @throws IOException 347 */ parseResponse(final XmlPullParser parser, final HostAuth hostAuth)348 private static void parseResponse(final XmlPullParser parser, final HostAuth hostAuth) 349 throws XmlPullParserException, IOException { 350 while (true) { 351 final int type = parser.next(); 352 if (type == XmlPullParser.END_TAG && parser.getName().equals(ELEMENT_NAME_RESPONSE)) { 353 break; 354 } else if (type == XmlPullParser.START_TAG) { 355 final String name = parser.getName(); 356 if (name.equals(ELEMENT_NAME_USER)) { 357 parseUser(parser, hostAuth); 358 } else if (name.equals(ELEMENT_NAME_ACTION)) { 359 parseAction(parser, hostAuth); 360 } 361 } 362 } 363 } 364 365 /** 366 * Parse the server response for the final {@link HostAuth}. 367 * @param resp The {@link EasResponse} from the server. 368 * @return The final {@link HostAuth} for this server. 369 */ parseAutodiscover(final EasResponse resp)370 private static HostAuth parseAutodiscover(final EasResponse resp) { 371 // The response to Autodiscover is regular XML (not WBXML) 372 try { 373 final XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser(); 374 parser.setInput(resp.getInputStream(), "UTF-8"); 375 if (parser.getEventType() != XmlPullParser.START_DOCUMENT) { 376 return null; 377 } 378 if (parser.next() != XmlPullParser.START_TAG) { 379 return null; 380 } 381 if (!parser.getName().equals(ELEMENT_NAME_AUTODISCOVER)) { 382 return null; 383 } 384 385 final HostAuth hostAuth = new HostAuth(); 386 while (true) { 387 final int type = parser.nextTag(); 388 if (type == XmlPullParser.END_TAG && parser.getName() 389 .equals(ELEMENT_NAME_AUTODISCOVER)) { 390 break; 391 } else if (type == XmlPullParser.START_TAG && parser.getName() 392 .equals(ELEMENT_NAME_RESPONSE)) { 393 parseResponse(parser, hostAuth); 394 // Valid responses will set the address. 395 if (hostAuth.mAddress != null) { 396 return hostAuth; 397 } 398 } 399 } 400 } catch (final XmlPullParserException e) { 401 // Parse error. 402 } catch (final IOException e) { 403 // Error reading parser. 404 } 405 return null; 406 } 407 } 408