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.compatibility.WebAddress; 20 import android.webkit.CookieManager; 21 22 import org.apache.commons.codec.binary.Base64; 23 24 import java.io.InputStream; 25 import java.lang.Math; 26 import java.security.MessageDigest; 27 import java.security.NoSuchAlgorithmException; 28 import java.util.HashMap; 29 import java.util.Map; 30 import java.util.Random; 31 32 /** 33 * RequestHandle: handles a request session that may include multiple 34 * redirects, HTTP authentication requests, etc. 35 */ 36 public 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 */ RequestHandle(RequestQueue requestQueue, String url, WebAddress uri, String method, Map<String, String> headers, InputStream bodyProvider, int bodyLength, Request request)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 */ RequestHandle(RequestQueue requestQueue, String url, WebAddress uri, String method, Map<String, String> headers, InputStream bodyProvider, int bodyLength, Request request, Connection conn)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 */ cancel()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 */ pauseRequest(boolean pause)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 */ handleSslErrorResponse(boolean proceed)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 */ isRedirectMax()123 public boolean isRedirectMax() { 124 return mRedirectCount >= MAX_REDIRECT_COUNT; 125 } 126 getRedirectCount()127 public int getRedirectCount() { 128 return mRedirectCount; 129 } 130 setRedirectCount(int count)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 */ setupRedirect(String redirectTo, int statusCode, Map<String, String> cacheHeaders)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 */ 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 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 waitUntilComplete()287 public void waitUntilComplete() { 288 mRequest.waitUntilComplete(); 289 } 290 processRequest()291 public void processRequest() { 292 if (mConnection != null) { 293 mConnection.processRequests(mRequest); 294 } 295 } 296 297 /** 298 * @return Digest-scheme authentication response. 299 */ computeDigestAuthResponse(String username, String password, String realm, String nonce, String QOP, String algorithm, String opaque)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 */ authorizationHeader(boolean isProxy)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 */ computeDigest( String A1, String A2, String nonce, String QOP, String nc, String cnonce)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 */ KD(String secret, String data)384 private String KD(String secret, String data) { 385 return H(secret + ":" + data); 386 } 387 388 /** 389 * @return MD5 hash of param. 390 */ H(String param)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 */ bufferToHex(byte[] buffer)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 */ computeCnonce()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 */ doubleQuote(String param)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 */ createAndQueueNewRequest()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