summaryrefslogtreecommitdiffstats
path: root/android/src/android/net/http/RequestHandle.java
diff options
context:
space:
mode:
Diffstat (limited to 'android/src/android/net/http/RequestHandle.java')
-rw-r--r--android/src/android/net/http/RequestHandle.java478
1 files changed, 478 insertions, 0 deletions
diff --git a/android/src/android/net/http/RequestHandle.java b/android/src/android/net/http/RequestHandle.java
new file mode 100644
index 0000000..46c3869
--- /dev/null
+++ b/android/src/android/net/http/RequestHandle.java
@@ -0,0 +1,478 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.http;
+
+import android.net.ParseException;
+import android.net.WebAddress;
+import android.webkit.CookieManager;
+
+import org.apache.commons.codec.binary.Base64;
+
+import java.io.InputStream;
+import java.lang.Math;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+
+/**
+ * RequestHandle: handles a request session that may include multiple
+ * redirects, HTTP authentication requests, etc.
+ *
+ * {@hide}
+ */
+public class RequestHandle {
+
+ private String mUrl;
+ private WebAddress mUri;
+ private String mMethod;
+ private Map<String, String> mHeaders;
+ private RequestQueue mRequestQueue;
+ private Request mRequest;
+ private InputStream mBodyProvider;
+ private int mBodyLength;
+ private int mRedirectCount = 0;
+ // Used only with synchronous requests.
+ private Connection mConnection;
+
+ private final static String AUTHORIZATION_HEADER = "Authorization";
+ private final static String PROXY_AUTHORIZATION_HEADER = "Proxy-Authorization";
+
+ public final static int MAX_REDIRECT_COUNT = 16;
+
+ /**
+ * Creates a new request session.
+ */
+ public RequestHandle(RequestQueue requestQueue, String url, WebAddress uri,
+ String method, Map<String, String> headers,
+ InputStream bodyProvider, int bodyLength, Request request) {
+
+ if (headers == null) {
+ headers = new HashMap<String, String>();
+ }
+ mHeaders = headers;
+ mBodyProvider = bodyProvider;
+ mBodyLength = bodyLength;
+ mMethod = method == null? "GET" : method;
+
+ mUrl = url;
+ mUri = uri;
+
+ mRequestQueue = requestQueue;
+
+ mRequest = request;
+ }
+
+ /**
+ * Creates a new request session with a given Connection. This connection
+ * is used during a synchronous load to handle this request.
+ */
+ public RequestHandle(RequestQueue requestQueue, String url, WebAddress uri,
+ String method, Map<String, String> headers,
+ InputStream bodyProvider, int bodyLength, Request request,
+ Connection conn) {
+ this(requestQueue, url, uri, method, headers, bodyProvider, bodyLength,
+ request);
+ mConnection = conn;
+ }
+
+ /**
+ * Cancels this request
+ */
+ public void cancel() {
+ if (mRequest != null) {
+ mRequest.cancel();
+ }
+ }
+
+ /**
+ * Pauses the loading of this request. For example, called from the WebCore thread
+ * when the plugin can take no more data.
+ */
+ public void pauseRequest(boolean pause) {
+ if (mRequest != null) {
+ mRequest.setLoadingPaused(pause);
+ }
+ }
+
+ /**
+ * Handles SSL error(s) on the way down from the user (the user
+ * has already provided their feedback).
+ */
+ public void handleSslErrorResponse(boolean proceed) {
+ if (mRequest != null) {
+ mRequest.handleSslErrorResponse(proceed);
+ }
+ }
+
+ /**
+ * @return true if we've hit the max redirect count
+ */
+ public boolean isRedirectMax() {
+ return mRedirectCount >= MAX_REDIRECT_COUNT;
+ }
+
+ public int getRedirectCount() {
+ return mRedirectCount;
+ }
+
+ public void setRedirectCount(int count) {
+ mRedirectCount = count;
+ }
+
+ /**
+ * Create and queue a redirect request.
+ *
+ * @param redirectTo URL to redirect to
+ * @param statusCode HTTP status code returned from original request
+ * @param cacheHeaders Cache header for redirect URL
+ * @return true if setup succeeds, false otherwise (redirect loop
+ * count exceeded, body provider unable to rewind on 307 redirect)
+ */
+ public boolean setupRedirect(String redirectTo, int statusCode,
+ Map<String, String> cacheHeaders) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("RequestHandle.setupRedirect(): redirectCount " +
+ mRedirectCount);
+ }
+
+ // be careful and remove authentication headers, if any
+ mHeaders.remove(AUTHORIZATION_HEADER);
+ mHeaders.remove(PROXY_AUTHORIZATION_HEADER);
+
+ if (++mRedirectCount == MAX_REDIRECT_COUNT) {
+ // Way too many redirects -- fail out
+ if (HttpLog.LOGV) HttpLog.v(
+ "RequestHandle.setupRedirect(): too many redirects " +
+ mRequest);
+ mRequest.error(EventHandler.ERROR_REDIRECT_LOOP,
+ com.android.internal.R.string.httpErrorRedirectLoop);
+ return false;
+ }
+
+ if (mUrl.startsWith("https:") && redirectTo.startsWith("http:")) {
+ // implement http://www.w3.org/Protocols/rfc2616/rfc2616-sec15.html#sec15.1.3
+ if (HttpLog.LOGV) {
+ HttpLog.v("blowing away the referer on an https -> http redirect");
+ }
+ mHeaders.remove("Referer");
+ }
+
+ mUrl = redirectTo;
+ try {
+ mUri = new WebAddress(mUrl);
+ } catch (ParseException e) {
+ e.printStackTrace();
+ }
+
+ // update the "Cookie" header based on the redirected url
+ mHeaders.remove("Cookie");
+ String cookie = CookieManager.getInstance().getCookie(mUri);
+ if (cookie != null && cookie.length() > 0) {
+ mHeaders.put("Cookie", cookie);
+ }
+
+ if ((statusCode == 302 || statusCode == 303) && mMethod.equals("POST")) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("replacing POST with GET on redirect to " + redirectTo);
+ }
+ mMethod = "GET";
+ }
+ /* Only repost content on a 307. If 307, reset the body
+ provider so we can replay the body */
+ if (statusCode == 307) {
+ try {
+ if (mBodyProvider != null) mBodyProvider.reset();
+ } catch (java.io.IOException ex) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("setupRedirect() failed to reset body provider");
+ }
+ return false;
+ }
+
+ } else {
+ mHeaders.remove("Content-Type");
+ mBodyProvider = null;
+ }
+
+ // Update the cache headers for this URL
+ mHeaders.putAll(cacheHeaders);
+
+ createAndQueueNewRequest();
+ return true;
+ }
+
+ /**
+ * Create and queue an HTTP authentication-response (basic) request.
+ */
+ public void setupBasicAuthResponse(boolean isProxy, String username, String password) {
+ String response = computeBasicAuthResponse(username, password);
+ if (HttpLog.LOGV) {
+ HttpLog.v("setupBasicAuthResponse(): response: " + response);
+ }
+ mHeaders.put(authorizationHeader(isProxy), "Basic " + response);
+ setupAuthResponse();
+ }
+
+ /**
+ * Create and queue an HTTP authentication-response (digest) request.
+ */
+ public void setupDigestAuthResponse(boolean isProxy,
+ String username,
+ String password,
+ String realm,
+ String nonce,
+ String QOP,
+ String algorithm,
+ String opaque) {
+
+ String response = computeDigestAuthResponse(
+ username, password, realm, nonce, QOP, algorithm, opaque);
+ if (HttpLog.LOGV) {
+ HttpLog.v("setupDigestAuthResponse(): response: " + response);
+ }
+ mHeaders.put(authorizationHeader(isProxy), "Digest " + response);
+ setupAuthResponse();
+ }
+
+ private void setupAuthResponse() {
+ try {
+ if (mBodyProvider != null) mBodyProvider.reset();
+ } catch (java.io.IOException ex) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("setupAuthResponse() failed to reset body provider");
+ }
+ }
+ createAndQueueNewRequest();
+ }
+
+ /**
+ * @return HTTP request method (GET, PUT, etc).
+ */
+ public String getMethod() {
+ return mMethod;
+ }
+
+ /**
+ * @return Basic-scheme authentication response: BASE64(username:password).
+ */
+ public static String computeBasicAuthResponse(String username, String password) {
+ if (username == null) {
+ throw new NullPointerException("username == null");
+ }
+
+ if (password == null) {
+ throw new NullPointerException("password == null");
+ }
+
+ // encode username:password to base64
+ return new String(Base64.encodeBase64((username + ':' + password).getBytes()));
+ }
+
+ public void waitUntilComplete() {
+ mRequest.waitUntilComplete();
+ }
+
+ public void processRequest() {
+ if (mConnection != null) {
+ mConnection.processRequests(mRequest);
+ }
+ }
+
+ /**
+ * @return Digest-scheme authentication response.
+ */
+ private String computeDigestAuthResponse(String username,
+ String password,
+ String realm,
+ String nonce,
+ String QOP,
+ String algorithm,
+ String opaque) {
+
+ if (username == null) {
+ throw new NullPointerException("username == null");
+ }
+
+ if (password == null) {
+ throw new NullPointerException("password == null");
+ }
+
+ if (realm == null) {
+ throw new NullPointerException("realm == null");
+ }
+
+ String A1 = username + ":" + realm + ":" + password;
+ String A2 = mMethod + ":" + mUrl;
+
+ // because we do not preemptively send authorization headers, nc is always 1
+ String nc = "00000001";
+ String cnonce = computeCnonce();
+ String digest = computeDigest(A1, A2, nonce, QOP, nc, cnonce);
+
+ String response = "";
+ response += "username=" + doubleQuote(username) + ", ";
+ response += "realm=" + doubleQuote(realm) + ", ";
+ response += "nonce=" + doubleQuote(nonce) + ", ";
+ response += "uri=" + doubleQuote(mUrl) + ", ";
+ response += "response=" + doubleQuote(digest) ;
+
+ if (opaque != null) {
+ response += ", opaque=" + doubleQuote(opaque);
+ }
+
+ if (algorithm != null) {
+ response += ", algorithm=" + algorithm;
+ }
+
+ if (QOP != null) {
+ response += ", qop=" + QOP + ", nc=" + nc + ", cnonce=" + doubleQuote(cnonce);
+ }
+
+ return response;
+ }
+
+ /**
+ * @return The right authorization header (dependeing on whether it is a proxy or not).
+ */
+ public static String authorizationHeader(boolean isProxy) {
+ if (!isProxy) {
+ return AUTHORIZATION_HEADER;
+ } else {
+ return PROXY_AUTHORIZATION_HEADER;
+ }
+ }
+
+ /**
+ * @return Double-quoted MD5 digest.
+ */
+ private String computeDigest(
+ String A1, String A2, String nonce, String QOP, String nc, String cnonce) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("computeDigest(): QOP: " + QOP);
+ }
+
+ if (QOP == null) {
+ return KD(H(A1), nonce + ":" + H(A2));
+ } else {
+ if (QOP.equalsIgnoreCase("auth")) {
+ return KD(H(A1), nonce + ":" + nc + ":" + cnonce + ":" + QOP + ":" + H(A2));
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * @return MD5 hash of concat(secret, ":", data).
+ */
+ private String KD(String secret, String data) {
+ return H(secret + ":" + data);
+ }
+
+ /**
+ * @return MD5 hash of param.
+ */
+ private String H(String param) {
+ if (param != null) {
+ try {
+ MessageDigest md5 = MessageDigest.getInstance("MD5");
+
+ byte[] d = md5.digest(param.getBytes());
+ if (d != null) {
+ return bufferToHex(d);
+ }
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * @return HEX buffer representation.
+ */
+ private String bufferToHex(byte[] buffer) {
+ final char hexChars[] =
+ { '0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f' };
+
+ if (buffer != null) {
+ int length = buffer.length;
+ if (length > 0) {
+ StringBuilder hex = new StringBuilder(2 * length);
+
+ for (int i = 0; i < length; ++i) {
+ byte l = (byte) (buffer[i] & 0x0F);
+ byte h = (byte)((buffer[i] & 0xF0) >> 4);
+
+ hex.append(hexChars[h]);
+ hex.append(hexChars[l]);
+ }
+
+ return hex.toString();
+ } else {
+ return "";
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Computes a random cnonce value based on the current time.
+ */
+ private String computeCnonce() {
+ Random rand = new Random();
+ int nextInt = rand.nextInt();
+ nextInt = (nextInt == Integer.MIN_VALUE) ?
+ Integer.MAX_VALUE : Math.abs(nextInt);
+ return Integer.toString(nextInt, 16);
+ }
+
+ /**
+ * "Double-quotes" the argument.
+ */
+ private String doubleQuote(String param) {
+ if (param != null) {
+ return "\"" + param + "\"";
+ }
+
+ return null;
+ }
+
+ /**
+ * Creates and queues new request.
+ */
+ private void createAndQueueNewRequest() {
+ // mConnection is non-null if and only if the requests are synchronous.
+ if (mConnection != null) {
+ RequestHandle newHandle = mRequestQueue.queueSynchronousRequest(
+ mUrl, mUri, mMethod, mHeaders, mRequest.mEventHandler,
+ mBodyProvider, mBodyLength);
+ mRequest = newHandle.mRequest;
+ mConnection = newHandle.mConnection;
+ newHandle.processRequest();
+ return;
+ }
+ mRequest = mRequestQueue.queueRequest(
+ mUrl, mUri, mMethod, mHeaders, mRequest.mEventHandler,
+ mBodyProvider,
+ mBodyLength).mRequest;
+ }
+}