| /* |
| * 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.compatibility.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. |
| */ |
| 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, |
| "The page contains too many server redirects."); |
| 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 (IllegalArgumentException e) { |
| e.printStackTrace(); |
| } |
| |
| // update the "Cookie" header based on the redirected url |
| mHeaders.remove("Cookie"); |
| String cookie = null; |
| if (mUri != null) { |
| cookie = CookieManager.getInstance().getCookie(mUri.toString()); |
| } |
| 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; |
| } |
| } |