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 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 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 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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