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