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