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