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