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 android.security.Md5MessageDigest; 22 import junit.framework.Assert; 23 import android.webkit.CookieManager; 24 25 import org.apache.commons.codec.binary.Base64; 26 27 import java.io.InputStream; 28 import java.lang.Math; 29 import java.util.HashMap; 30 import java.util.Map; 31 import 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 */ 39 public 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 * Pauses the loading of this request. For example, called from the WebCore thread 105 * when the plugin can take no more data. 106 */ 107 public void pauseRequest(boolean pause) { 108 if (mRequest != null) { 109 mRequest.setLoadingPaused(pause); 110 } 111 } 112 113 /** 114 * Handles SSL error(s) on the way down from the user (the user 115 * has already provided their feedback). 116 */ 117 public void handleSslErrorResponse(boolean proceed) { 118 if (mRequest != null) { 119 mRequest.handleSslErrorResponse(proceed); 120 } 121 } 122 123 /** 124 * @return true if we've hit the max redirect count 125 */ 126 public boolean isRedirectMax() { 127 return mRedirectCount >= MAX_REDIRECT_COUNT; 128 } 129 130 public int getRedirectCount() { 131 return mRedirectCount; 132 } 133 134 public void setRedirectCount(int count) { 135 mRedirectCount = count; 136 } 137 138 /** 139 * Create and queue a redirect request. 140 * 141 * @param redirectTo URL to redirect to 142 * @param statusCode HTTP status code returned from original request 143 * @param cacheHeaders Cache header for redirect URL 144 * @return true if setup succeeds, false otherwise (redirect loop 145 * count exceeded, body provider unable to rewind on 307 redirect) 146 */ 147 public boolean setupRedirect(String redirectTo, int statusCode, 148 Map<String, String> cacheHeaders) { 149 if (HttpLog.LOGV) { 150 HttpLog.v("RequestHandle.setupRedirect(): redirectCount " + 151 mRedirectCount); 152 } 153 154 // be careful and remove authentication headers, if any 155 mHeaders.remove(AUTHORIZATION_HEADER); 156 mHeaders.remove(PROXY_AUTHORIZATION_HEADER); 157 158 if (++mRedirectCount == MAX_REDIRECT_COUNT) { 159 // Way too many redirects -- fail out 160 if (HttpLog.LOGV) HttpLog.v( 161 "RequestHandle.setupRedirect(): too many redirects " + 162 mRequest); 163 mRequest.error(EventHandler.ERROR_REDIRECT_LOOP, 164 com.android.internal.R.string.httpErrorRedirectLoop); 165 return false; 166 } 167 168 if (mUrl.startsWith("https:") && redirectTo.startsWith("http:")) { 169 // implement http://www.w3.org/Protocols/rfc2616/rfc2616-sec15.html#sec15.1.3 170 if (HttpLog.LOGV) { 171 HttpLog.v("blowing away the referer on an https -> http redirect"); 172 } 173 mHeaders.remove("Referer"); 174 } 175 176 mUrl = redirectTo; 177 try { 178 mUri = new WebAddress(mUrl); 179 } catch (ParseException e) { 180 e.printStackTrace(); 181 } 182 183 // update the "Cookie" header based on the redirected url 184 mHeaders.remove("Cookie"); 185 String cookie = CookieManager.getInstance().getCookie(mUri); 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 Assert.assertNotNull(username); 276 Assert.assertNotNull(password); 277 278 // encode username:password to base64 279 return new String(Base64.encodeBase64((username + ':' + password).getBytes())); 280 } 281 282 public void waitUntilComplete() { 283 mRequest.waitUntilComplete(); 284 } 285 286 public void processRequest() { 287 if (mConnection != null) { 288 mConnection.processRequests(mRequest); 289 } 290 } 291 292 /** 293 * @return Digest-scheme authentication response. 294 */ 295 private String computeDigestAuthResponse(String username, 296 String password, 297 String realm, 298 String nonce, 299 String QOP, 300 String algorithm, 301 String opaque) { 302 303 Assert.assertNotNull(username); 304 Assert.assertNotNull(password); 305 Assert.assertNotNull(realm); 306 307 String A1 = username + ":" + realm + ":" + password; 308 String A2 = mMethod + ":" + mUrl; 309 310 // because we do not preemptively send authorization headers, nc is always 1 311 String nc = "000001"; 312 String cnonce = computeCnonce(); 313 String digest = computeDigest(A1, A2, nonce, QOP, nc, cnonce); 314 315 String response = ""; 316 response += "username=" + doubleQuote(username) + ", "; 317 response += "realm=" + doubleQuote(realm) + ", "; 318 response += "nonce=" + doubleQuote(nonce) + ", "; 319 response += "uri=" + doubleQuote(mUrl) + ", "; 320 response += "response=" + doubleQuote(digest) ; 321 322 if (opaque != null) { 323 response += ", opaque=" + doubleQuote(opaque); 324 } 325 326 if (algorithm != null) { 327 response += ", algorithm=" + algorithm; 328 } 329 330 if (QOP != null) { 331 response += ", qop=" + QOP + ", nc=" + nc + ", cnonce=" + doubleQuote(cnonce); 332 } 333 334 return response; 335 } 336 337 /** 338 * @return The right authorization header (dependeing on whether it is a proxy or not). 339 */ 340 public static String authorizationHeader(boolean isProxy) { 341 if (!isProxy) { 342 return AUTHORIZATION_HEADER; 343 } else { 344 return PROXY_AUTHORIZATION_HEADER; 345 } 346 } 347 348 /** 349 * @return Double-quoted MD5 digest. 350 */ 351 private String computeDigest( 352 String A1, String A2, String nonce, String QOP, String nc, String cnonce) { 353 if (HttpLog.LOGV) { 354 HttpLog.v("computeDigest(): QOP: " + QOP); 355 } 356 357 if (QOP == null) { 358 return KD(H(A1), nonce + ":" + H(A2)); 359 } else { 360 if (QOP.equalsIgnoreCase("auth")) { 361 return KD(H(A1), nonce + ":" + nc + ":" + cnonce + ":" + QOP + ":" + H(A2)); 362 } 363 } 364 365 return null; 366 } 367 368 /** 369 * @return MD5 hash of concat(secret, ":", data). 370 */ 371 private String KD(String secret, String data) { 372 return H(secret + ":" + data); 373 } 374 375 /** 376 * @return MD5 hash of param. 377 */ 378 private String H(String param) { 379 if (param != null) { 380 Md5MessageDigest md5 = new Md5MessageDigest(); 381 382 byte[] d = md5.digest(param.getBytes()); 383 if (d != null) { 384 return bufferToHex(d); 385 } 386 } 387 388 return null; 389 } 390 391 /** 392 * @return HEX buffer representation. 393 */ 394 private String bufferToHex(byte[] buffer) { 395 final char hexChars[] = 396 { '0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f' }; 397 398 if (buffer != null) { 399 int length = buffer.length; 400 if (length > 0) { 401 StringBuilder hex = new StringBuilder(2 * length); 402 403 for (int i = 0; i < length; ++i) { 404 byte l = (byte) (buffer[i] & 0x0F); 405 byte h = (byte)((buffer[i] & 0xF0) >> 4); 406 407 hex.append(hexChars[h]); 408 hex.append(hexChars[l]); 409 } 410 411 return hex.toString(); 412 } else { 413 return ""; 414 } 415 } 416 417 return null; 418 } 419 420 /** 421 * Computes a random cnonce value based on the current time. 422 */ 423 private String computeCnonce() { 424 Random rand = new Random(); 425 int nextInt = rand.nextInt(); 426 nextInt = (nextInt == Integer.MIN_VALUE) ? 427 Integer.MAX_VALUE : Math.abs(nextInt); 428 return Integer.toString(nextInt, 16); 429 } 430 431 /** 432 * "Double-quotes" the argument. 433 */ 434 private String doubleQuote(String param) { 435 if (param != null) { 436 return "\"" + param + "\""; 437 } 438 439 return null; 440 } 441 442 /** 443 * Creates and queues new request. 444 */ 445 private void createAndQueueNewRequest() { 446 // mConnection is non-null if and only if the requests are synchronous. 447 if (mConnection != null) { 448 RequestHandle newHandle = mRequestQueue.queueSynchronousRequest( 449 mUrl, mUri, mMethod, mHeaders, mRequest.mEventHandler, 450 mBodyProvider, mBodyLength); 451 mRequest = newHandle.mRequest; 452 mConnection = newHandle.mConnection; 453 newHandle.processRequest(); 454 return; 455 } 456 mRequest = mRequestQueue.queueRequest( 457 mUrl, mUri, mMethod, mHeaders, mRequest.mEventHandler, 458 mBodyProvider, 459 mBodyLength).mRequest; 460 } 461 } 462