Home | History | Annotate | Download | only in http
      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