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 */ RequestHandle(RequestQueue requestQueue, String url, WebAddress uri, String method, Map<String, String> headers, InputStream bodyProvider, int bodyLength, Request request)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 */ RequestHandle(RequestQueue requestQueue, String url, WebAddress uri, String method, Map<String, String> headers, InputStream bodyProvider, int bodyLength, Request request, Connection conn)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 */ cancel()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 */ pauseRequest(boolean pause)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 */ handleSslErrorResponse(boolean proceed)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 */ isRedirectMax()126 public boolean isRedirectMax() { 127 return mRedirectCount >= MAX_REDIRECT_COUNT; 128 } 129 getRedirectCount()130 public int getRedirectCount() { 131 return mRedirectCount; 132 } 133 setRedirectCount(int count)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 */ setupRedirect(String redirectTo, int statusCode, Map<String, String> cacheHeaders)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 */ setupBasicAuthResponse(boolean isProxy, String username, String password)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 */ setupDigestAuthResponse(boolean isProxy, String username, String password, String realm, String nonce, String QOP, String algorithm, String opaque)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 setupAuthResponse()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 */ getMethod()267 public String getMethod() { 268 return mMethod; 269 } 270 271 /** 272 * @return Basic-scheme authentication response: BASE64(username:password). 273 */ computeBasicAuthResponse(String username, String password)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 waitUntilComplete()282 public void waitUntilComplete() { 283 mRequest.waitUntilComplete(); 284 } 285 processRequest()286 public void processRequest() { 287 if (mConnection != null) { 288 mConnection.processRequests(mRequest); 289 } 290 } 291 292 /** 293 * @return Digest-scheme authentication response. 294 */ computeDigestAuthResponse(String username, String password, String realm, String nonce, String QOP, String algorithm, String opaque)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 */ authorizationHeader(boolean isProxy)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 */ computeDigest( String A1, String A2, String nonce, String QOP, String nc, String cnonce)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 */ KD(String secret, String data)371 private String KD(String secret, String data) { 372 return H(secret + ":" + data); 373 } 374 375 /** 376 * @return MD5 hash of param. 377 */ H(String param)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 */ bufferToHex(byte[] buffer)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 */ computeCnonce()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 */ doubleQuote(String param)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 */ createAndQueueNewRequest()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