RequestHandle.java revision cf4550c3198d6b3d92cdc52707fe70d7cc0caa9f
1/* 2 * Copyright (C) 2006 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.net.http; 18 19import android.net.ParseException; 20import android.net.WebAddress; 21import android.security.Md5MessageDigest; 22import junit.framework.Assert; 23import android.webkit.CookieManager; 24 25import org.apache.commons.codec.binary.Base64; 26 27import java.io.InputStream; 28import java.lang.Math; 29import java.util.HashMap; 30import java.util.Map; 31import java.util.Random; 32 33/** 34 * RequestHandle: handles a request session that may include multiple 35 * redirects, HTTP authentication requests, etc. 36 * 37 * {@hide} 38 */ 39public class RequestHandle { 40 41 private String mUrl; 42 private WebAddress mUri; 43 private String mMethod; 44 private Map<String, String> mHeaders; 45 46 private RequestQueue mRequestQueue; 47 48 private Request mRequest; 49 50 private InputStream mBodyProvider; 51 private int mBodyLength; 52 53 private int mRedirectCount = 0; 54 55 private final static String AUTHORIZATION_HEADER = "Authorization"; 56 private final static String PROXY_AUTHORIZATION_HEADER = "Proxy-Authorization"; 57 58 public final static int MAX_REDIRECT_COUNT = 16; 59 60 /** 61 * Creates a new request session. 62 */ 63 public RequestHandle(RequestQueue requestQueue, String url, WebAddress uri, 64 String method, Map<String, String> headers, 65 InputStream bodyProvider, int bodyLength, Request request) { 66 67 if (headers == null) { 68 headers = new HashMap<String, String>(); 69 } 70 mHeaders = headers; 71 mBodyProvider = bodyProvider; 72 mBodyLength = bodyLength; 73 mMethod = method == null? "GET" : method; 74 75 mUrl = url; 76 mUri = uri; 77 78 mRequestQueue = requestQueue; 79 80 mRequest = request; 81 } 82 83 /** 84 * Cancels this request 85 */ 86 public void cancel() { 87 if (mRequest != null) { 88 mRequest.cancel(); 89 } 90 } 91 92 /** 93 * Handles SSL error(s) on the way down from the user (the user 94 * has already provided their feedback). 95 */ 96 public void handleSslErrorResponse(boolean proceed) { 97 if (mRequest != null) { 98 mRequest.handleSslErrorResponse(proceed); 99 } 100 } 101 102 /** 103 * @return true if we've hit the max redirect count 104 */ 105 public boolean isRedirectMax() { 106 return mRedirectCount >= MAX_REDIRECT_COUNT; 107 } 108 109 public int getRedirectCount() { 110 return mRedirectCount; 111 } 112 113 public void setRedirectCount(int count) { 114 mRedirectCount = count; 115 } 116 117 /** 118 * Create and queue a redirect request. 119 * 120 * @param redirectTo URL to redirect to 121 * @param statusCode HTTP status code returned from original request 122 * @param cacheHeaders Cache header for redirect URL 123 * @return true if setup succeeds, false otherwise (redirect loop 124 * count exceeded, body provider unable to rewind on 307 redirect) 125 */ 126 public boolean setupRedirect(String redirectTo, int statusCode, 127 Map<String, String> cacheHeaders) { 128 if (HttpLog.LOGV) { 129 HttpLog.v("RequestHandle.setupRedirect(): redirectCount " + 130 mRedirectCount); 131 } 132 133 // be careful and remove authentication headers, if any 134 mHeaders.remove(AUTHORIZATION_HEADER); 135 mHeaders.remove(PROXY_AUTHORIZATION_HEADER); 136 137 if (++mRedirectCount == MAX_REDIRECT_COUNT) { 138 // Way too many redirects -- fail out 139 if (HttpLog.LOGV) HttpLog.v( 140 "RequestHandle.setupRedirect(): too many redirects " + 141 mRequest); 142 mRequest.error(EventHandler.ERROR_REDIRECT_LOOP, 143 com.android.internal.R.string.httpErrorRedirectLoop); 144 return false; 145 } 146 147 if (mUrl.startsWith("https:") && redirectTo.startsWith("http:")) { 148 // implement http://www.w3.org/Protocols/rfc2616/rfc2616-sec15.html#sec15.1.3 149 if (HttpLog.LOGV) { 150 HttpLog.v("blowing away the referer on an https -> http redirect"); 151 } 152 mHeaders.remove("Referer"); 153 } 154 155 mUrl = redirectTo; 156 try { 157 mUri = new WebAddress(mUrl); 158 } catch (ParseException e) { 159 e.printStackTrace(); 160 } 161 162 // update the "Cookie" header based on the redirected url 163 mHeaders.remove("Cookie"); 164 String cookie = CookieManager.getInstance().getCookie(mUri); 165 if (cookie != null && cookie.length() > 0) { 166 mHeaders.put("Cookie", cookie); 167 } 168 169 if ((statusCode == 302 || statusCode == 303) && mMethod.equals("POST")) { 170 if (HttpLog.LOGV) { 171 HttpLog.v("replacing POST with GET on redirect to " + redirectTo); 172 } 173 mMethod = "GET"; 174 } 175 /* Only repost content on a 307. If 307, reset the body 176 provider so we can replay the body */ 177 if (statusCode == 307) { 178 try { 179 if (mBodyProvider != null) mBodyProvider.reset(); 180 } catch (java.io.IOException ex) { 181 if (HttpLog.LOGV) { 182 HttpLog.v("setupAuthResponse() failed to reset body provider"); 183 } 184 return false; 185 } 186 187 } else { 188 mHeaders.remove("Content-Type"); 189 mBodyProvider = null; 190 } 191 192 // Update the cache headers for this URL 193 mHeaders.putAll(cacheHeaders); 194 195 createAndQueueNewRequest(); 196 return true; 197 } 198 199 /** 200 * Create and queue an HTTP authentication-response (basic) request. 201 */ 202 public void setupBasicAuthResponse(boolean isProxy, String username, String password) { 203 String response = computeBasicAuthResponse(username, password); 204 if (HttpLog.LOGV) { 205 HttpLog.v("setupBasicAuthResponse(): response: " + response); 206 } 207 mHeaders.put(authorizationHeader(isProxy), "Basic " + response); 208 setupAuthResponse(); 209 } 210 211 /** 212 * Create and queue an HTTP authentication-response (digest) request. 213 */ 214 public void setupDigestAuthResponse(boolean isProxy, 215 String username, 216 String password, 217 String realm, 218 String nonce, 219 String QOP, 220 String algorithm, 221 String opaque) { 222 223 String response = computeDigestAuthResponse( 224 username, password, realm, nonce, QOP, algorithm, opaque); 225 if (HttpLog.LOGV) { 226 HttpLog.v("setupDigestAuthResponse(): response: " + response); 227 } 228 mHeaders.put(authorizationHeader(isProxy), "Digest " + response); 229 setupAuthResponse(); 230 } 231 232 private void setupAuthResponse() { 233 try { 234 if (mBodyProvider != null) mBodyProvider.reset(); 235 } catch (java.io.IOException ex) { 236 if (HttpLog.LOGV) { 237 HttpLog.v("setupAuthResponse() failed to reset body provider"); 238 } 239 } 240 createAndQueueNewRequest(); 241 } 242 243 /** 244 * @return HTTP request method (GET, PUT, etc). 245 */ 246 public String getMethod() { 247 return mMethod; 248 } 249 250 /** 251 * @return Basic-scheme authentication response: BASE64(username:password). 252 */ 253 public static String computeBasicAuthResponse(String username, String password) { 254 Assert.assertNotNull(username); 255 Assert.assertNotNull(password); 256 257 // encode username:password to base64 258 return new String(Base64.encodeBase64((username + ':' + password).getBytes())); 259 } 260 261 public void waitUntilComplete() { 262 mRequest.waitUntilComplete(); 263 } 264 265 /** 266 * @return Digest-scheme authentication response. 267 */ 268 private String computeDigestAuthResponse(String username, 269 String password, 270 String realm, 271 String nonce, 272 String QOP, 273 String algorithm, 274 String opaque) { 275 276 Assert.assertNotNull(username); 277 Assert.assertNotNull(password); 278 Assert.assertNotNull(realm); 279 280 String A1 = username + ":" + realm + ":" + password; 281 String A2 = mMethod + ":" + mUrl; 282 283 // because we do not preemptively send authorization headers, nc is always 1 284 String nc = "000001"; 285 String cnonce = computeCnonce(); 286 String digest = computeDigest(A1, A2, nonce, QOP, nc, cnonce); 287 288 String response = ""; 289 response += "username=" + doubleQuote(username) + ", "; 290 response += "realm=" + doubleQuote(realm) + ", "; 291 response += "nonce=" + doubleQuote(nonce) + ", "; 292 response += "uri=" + doubleQuote(mUrl) + ", "; 293 response += "response=" + doubleQuote(digest) ; 294 295 if (opaque != null) { 296 response += ", opaque=" + doubleQuote(opaque); 297 } 298 299 if (algorithm != null) { 300 response += ", algorithm=" + algorithm; 301 } 302 303 if (QOP != null) { 304 response += ", qop=" + QOP + ", nc=" + nc + ", cnonce=" + doubleQuote(cnonce); 305 } 306 307 return response; 308 } 309 310 /** 311 * @return The right authorization header (dependeing on whether it is a proxy or not). 312 */ 313 public static String authorizationHeader(boolean isProxy) { 314 if (!isProxy) { 315 return AUTHORIZATION_HEADER; 316 } else { 317 return PROXY_AUTHORIZATION_HEADER; 318 } 319 } 320 321 /** 322 * @return Double-quoted MD5 digest. 323 */ 324 private String computeDigest( 325 String A1, String A2, String nonce, String QOP, String nc, String cnonce) { 326 if (HttpLog.LOGV) { 327 HttpLog.v("computeDigest(): QOP: " + QOP); 328 } 329 330 if (QOP == null) { 331 return KD(H(A1), nonce + ":" + H(A2)); 332 } else { 333 if (QOP.equalsIgnoreCase("auth")) { 334 return KD(H(A1), nonce + ":" + nc + ":" + cnonce + ":" + QOP + ":" + H(A2)); 335 } 336 } 337 338 return null; 339 } 340 341 /** 342 * @return MD5 hash of concat(secret, ":", data). 343 */ 344 private String KD(String secret, String data) { 345 return H(secret + ":" + data); 346 } 347 348 /** 349 * @return MD5 hash of param. 350 */ 351 private String H(String param) { 352 if (param != null) { 353 Md5MessageDigest md5 = new Md5MessageDigest(); 354 355 byte[] d = md5.digest(param.getBytes()); 356 if (d != null) { 357 return bufferToHex(d); 358 } 359 } 360 361 return null; 362 } 363 364 /** 365 * @return HEX buffer representation. 366 */ 367 private String bufferToHex(byte[] buffer) { 368 final char hexChars[] = 369 { '0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f' }; 370 371 if (buffer != null) { 372 int length = buffer.length; 373 if (length > 0) { 374 StringBuilder hex = new StringBuilder(2 * length); 375 376 for (int i = 0; i < length; ++i) { 377 byte l = (byte) (buffer[i] & 0x0F); 378 byte h = (byte)((buffer[i] & 0xF0) >> 4); 379 380 hex.append(hexChars[h]); 381 hex.append(hexChars[l]); 382 } 383 384 return hex.toString(); 385 } else { 386 return ""; 387 } 388 } 389 390 return null; 391 } 392 393 /** 394 * Computes a random cnonce value based on the current time. 395 */ 396 private String computeCnonce() { 397 Random rand = new Random(); 398 int nextInt = rand.nextInt(); 399 nextInt = (nextInt == Integer.MIN_VALUE) ? 400 Integer.MAX_VALUE : Math.abs(nextInt); 401 return Integer.toString(nextInt, 16); 402 } 403 404 /** 405 * "Double-quotes" the argument. 406 */ 407 private String doubleQuote(String param) { 408 if (param != null) { 409 return "\"" + param + "\""; 410 } 411 412 return null; 413 } 414 415 /** 416 * Creates and queues new request. 417 */ 418 private void createAndQueueNewRequest() { 419 mRequest = mRequestQueue.queueRequest( 420 mUrl, mUri, mMethod, mHeaders, mRequest.mEventHandler, 421 mBodyProvider, 422 mBodyLength, mRequest.mHighPriority).mRequest; 423 } 424} 425