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 junit.framework.Assert;
     22 import android.webkit.CookieManager;
     23 
     24 import org.apache.commons.codec.binary.Base64;
     25 
     26 import java.io.InputStream;
     27 import java.lang.Math;
     28 import java.security.MessageDigest;
     29 import java.security.NoSuchAlgorithmException;
     30 import java.util.HashMap;
     31 import java.util.Map;
     32 import java.util.Random;
     33 
     34 /**
     35  * RequestHandle: handles a request session that may include multiple
     36  * redirects, HTTP authentication requests, etc.
     37  *
     38  * {@hide}
     39  */
     40 public class RequestHandle {
     41 
     42     private String        mUrl;
     43     private WebAddress    mUri;
     44     private String        mMethod;
     45     private Map<String, String> mHeaders;
     46     private RequestQueue  mRequestQueue;
     47     private Request       mRequest;
     48     private InputStream   mBodyProvider;
     49     private int           mBodyLength;
     50     private int           mRedirectCount = 0;
     51     // Used only with synchronous requests.
     52     private Connection    mConnection;
     53 
     54     private final static String AUTHORIZATION_HEADER = "Authorization";
     55     private final static String PROXY_AUTHORIZATION_HEADER = "Proxy-Authorization";
     56 
     57     public final static int MAX_REDIRECT_COUNT = 16;
     58 
     59     /**
     60      * Creates a new request session.
     61      */
     62     public RequestHandle(RequestQueue requestQueue, String url, WebAddress uri,
     63             String method, Map<String, String> headers,
     64             InputStream bodyProvider, int bodyLength, Request request) {
     65 
     66         if (headers == null) {
     67             headers = new HashMap<String, String>();
     68         }
     69         mHeaders = headers;
     70         mBodyProvider = bodyProvider;
     71         mBodyLength = bodyLength;
     72         mMethod = method == null? "GET" : method;
     73 
     74         mUrl = url;
     75         mUri = uri;
     76 
     77         mRequestQueue = requestQueue;
     78 
     79         mRequest = request;
     80     }
     81 
     82     /**
     83      * Creates a new request session with a given Connection. This connection
     84      * is used during a synchronous load to handle this request.
     85      */
     86     public RequestHandle(RequestQueue requestQueue, String url, WebAddress uri,
     87             String method, Map<String, String> headers,
     88             InputStream bodyProvider, int bodyLength, Request request,
     89             Connection conn) {
     90         this(requestQueue, url, uri, method, headers, bodyProvider, bodyLength,
     91                 request);
     92         mConnection = conn;
     93     }
     94 
     95     /**
     96      * Cancels this request
     97      */
     98     public void cancel() {
     99         if (mRequest != null) {
    100             mRequest.cancel();
    101         }
    102     }
    103 
    104     /**
    105      * Pauses the loading of this request. For example, called from the WebCore thread
    106      * when the plugin can take no more data.
    107      */
    108     public void pauseRequest(boolean pause) {
    109         if (mRequest != null) {
    110             mRequest.setLoadingPaused(pause);
    111         }
    112     }
    113 
    114     /**
    115      * Handles SSL error(s) on the way down from the user (the user
    116      * has already provided their feedback).
    117      */
    118     public void handleSslErrorResponse(boolean proceed) {
    119         if (mRequest != null) {
    120             mRequest.handleSslErrorResponse(proceed);
    121         }
    122     }
    123 
    124     /**
    125      * @return true if we've hit the max redirect count
    126      */
    127     public boolean isRedirectMax() {
    128         return mRedirectCount >= MAX_REDIRECT_COUNT;
    129     }
    130 
    131     public int getRedirectCount() {
    132         return mRedirectCount;
    133     }
    134 
    135     public void setRedirectCount(int count) {
    136         mRedirectCount = count;
    137     }
    138 
    139     /**
    140      * Create and queue a redirect request.
    141      *
    142      * @param redirectTo URL to redirect to
    143      * @param statusCode HTTP status code returned from original request
    144      * @param cacheHeaders Cache header for redirect URL
    145      * @return true if setup succeeds, false otherwise (redirect loop
    146      * count exceeded, body provider unable to rewind on 307 redirect)
    147      */
    148     public boolean setupRedirect(String redirectTo, int statusCode,
    149             Map<String, String> cacheHeaders) {
    150         if (HttpLog.LOGV) {
    151             HttpLog.v("RequestHandle.setupRedirect(): redirectCount " +
    152                   mRedirectCount);
    153         }
    154 
    155         // be careful and remove authentication headers, if any
    156         mHeaders.remove(AUTHORIZATION_HEADER);
    157         mHeaders.remove(PROXY_AUTHORIZATION_HEADER);
    158 
    159         if (++mRedirectCount == MAX_REDIRECT_COUNT) {
    160             // Way too many redirects -- fail out
    161             if (HttpLog.LOGV) HttpLog.v(
    162                     "RequestHandle.setupRedirect(): too many redirects " +
    163                     mRequest);
    164             mRequest.error(EventHandler.ERROR_REDIRECT_LOOP,
    165                            com.android.internal.R.string.httpErrorRedirectLoop);
    166             return false;
    167         }
    168 
    169         if (mUrl.startsWith("https:") && redirectTo.startsWith("http:")) {
    170             // implement http://www.w3.org/Protocols/rfc2616/rfc2616-sec15.html#sec15.1.3
    171             if (HttpLog.LOGV) {
    172                 HttpLog.v("blowing away the referer on an https -> http redirect");
    173             }
    174             mHeaders.remove("Referer");
    175         }
    176 
    177         mUrl = redirectTo;
    178         try {
    179             mUri = new WebAddress(mUrl);
    180         } catch (ParseException e) {
    181             e.printStackTrace();
    182         }
    183 
    184         // update the "Cookie" header based on the redirected url
    185         mHeaders.remove("Cookie");
    186         String cookie = CookieManager.getInstance().getCookie(mUri);
    187         if (cookie != null && cookie.length() > 0) {
    188             mHeaders.put("Cookie", cookie);
    189         }
    190 
    191         if ((statusCode == 302 || statusCode == 303) && mMethod.equals("POST")) {
    192             if (HttpLog.LOGV) {
    193                 HttpLog.v("replacing POST with GET on redirect to " + redirectTo);
    194             }
    195             mMethod = "GET";
    196         }
    197         /* Only repost content on a 307.  If 307, reset the body
    198            provider so we can replay the body */
    199         if (statusCode == 307) {
    200             try {
    201                 if (mBodyProvider != null) mBodyProvider.reset();
    202             } catch (java.io.IOException ex) {
    203                 if (HttpLog.LOGV) {
    204                     HttpLog.v("setupRedirect() failed to reset body provider");
    205                 }
    206                 return false;
    207             }
    208 
    209         } else {
    210             mHeaders.remove("Content-Type");
    211             mBodyProvider = null;
    212         }
    213 
    214         // Update the cache headers for this URL
    215         mHeaders.putAll(cacheHeaders);
    216 
    217         createAndQueueNewRequest();
    218         return true;
    219     }
    220 
    221     /**
    222      * Create and queue an HTTP authentication-response (basic) request.
    223      */
    224     public void setupBasicAuthResponse(boolean isProxy, String username, String password) {
    225         String response = computeBasicAuthResponse(username, password);
    226         if (HttpLog.LOGV) {
    227             HttpLog.v("setupBasicAuthResponse(): response: " + response);
    228         }
    229         mHeaders.put(authorizationHeader(isProxy), "Basic " + response);
    230         setupAuthResponse();
    231     }
    232 
    233     /**
    234      * Create and queue an HTTP authentication-response (digest) request.
    235      */
    236     public void setupDigestAuthResponse(boolean isProxy,
    237                                         String username,
    238                                         String password,
    239                                         String realm,
    240                                         String nonce,
    241                                         String QOP,
    242                                         String algorithm,
    243                                         String opaque) {
    244 
    245         String response = computeDigestAuthResponse(
    246                 username, password, realm, nonce, QOP, algorithm, opaque);
    247         if (HttpLog.LOGV) {
    248             HttpLog.v("setupDigestAuthResponse(): response: " + response);
    249         }
    250         mHeaders.put(authorizationHeader(isProxy), "Digest " + response);
    251         setupAuthResponse();
    252     }
    253 
    254     private void setupAuthResponse() {
    255         try {
    256             if (mBodyProvider != null) mBodyProvider.reset();
    257         } catch (java.io.IOException ex) {
    258             if (HttpLog.LOGV) {
    259                 HttpLog.v("setupAuthResponse() failed to reset body provider");
    260             }
    261         }
    262         createAndQueueNewRequest();
    263     }
    264 
    265     /**
    266      * @return HTTP request method (GET, PUT, etc).
    267      */
    268     public String getMethod() {
    269         return mMethod;
    270     }
    271 
    272     /**
    273      * @return Basic-scheme authentication response: BASE64(username:password).
    274      */
    275     public static String computeBasicAuthResponse(String username, String password) {
    276         Assert.assertNotNull(username);
    277         Assert.assertNotNull(password);
    278 
    279         // encode username:password to base64
    280         return new String(Base64.encodeBase64((username + ':' + password).getBytes()));
    281     }
    282 
    283     public void waitUntilComplete() {
    284         mRequest.waitUntilComplete();
    285     }
    286 
    287     public void processRequest() {
    288         if (mConnection != null) {
    289             mConnection.processRequests(mRequest);
    290         }
    291     }
    292 
    293     /**
    294      * @return Digest-scheme authentication response.
    295      */
    296     private String computeDigestAuthResponse(String username,
    297                                              String password,
    298                                              String realm,
    299                                              String nonce,
    300                                              String QOP,
    301                                              String algorithm,
    302                                              String opaque) {
    303 
    304         Assert.assertNotNull(username);
    305         Assert.assertNotNull(password);
    306         Assert.assertNotNull(realm);
    307 
    308         String A1 = username + ":" + realm + ":" + password;
    309         String A2 = mMethod  + ":" + mUrl;
    310 
    311         // because we do not preemptively send authorization headers, nc is always 1
    312         String nc = "00000001";
    313         String cnonce = computeCnonce();
    314         String digest = computeDigest(A1, A2, nonce, QOP, nc, cnonce);
    315 
    316         String response = "";
    317         response += "username=" + doubleQuote(username) + ", ";
    318         response += "realm="    + doubleQuote(realm)    + ", ";
    319         response += "nonce="    + doubleQuote(nonce)    + ", ";
    320         response += "uri="      + doubleQuote(mUrl)     + ", ";
    321         response += "response=" + doubleQuote(digest) ;
    322 
    323         if (opaque     != null) {
    324             response += ", opaque=" + doubleQuote(opaque);
    325         }
    326 
    327          if (algorithm != null) {
    328             response += ", algorithm=" +  algorithm;
    329         }
    330 
    331         if (QOP        != null) {
    332             response += ", qop=" + QOP + ", nc=" + nc + ", cnonce=" + doubleQuote(cnonce);
    333         }
    334 
    335         return response;
    336     }
    337 
    338     /**
    339      * @return The right authorization header (dependeing on whether it is a proxy or not).
    340      */
    341     public static String authorizationHeader(boolean isProxy) {
    342         if (!isProxy) {
    343             return AUTHORIZATION_HEADER;
    344         } else {
    345             return PROXY_AUTHORIZATION_HEADER;
    346         }
    347     }
    348 
    349     /**
    350      * @return Double-quoted MD5 digest.
    351      */
    352     private String computeDigest(
    353         String A1, String A2, String nonce, String QOP, String nc, String cnonce) {
    354         if (HttpLog.LOGV) {
    355             HttpLog.v("computeDigest(): QOP: " + QOP);
    356         }
    357 
    358         if (QOP == null) {
    359             return KD(H(A1), nonce + ":" + H(A2));
    360         } else {
    361             if (QOP.equalsIgnoreCase("auth")) {
    362                 return KD(H(A1), nonce + ":" + nc + ":" + cnonce + ":" + QOP + ":" + H(A2));
    363             }
    364         }
    365 
    366         return null;
    367     }
    368 
    369     /**
    370      * @return MD5 hash of concat(secret, ":", data).
    371      */
    372     private String KD(String secret, String data) {
    373         return H(secret + ":" + data);
    374     }
    375 
    376     /**
    377      * @return MD5 hash of param.
    378      */
    379     private String H(String param) {
    380         if (param != null) {
    381             try {
    382                 MessageDigest md5 = MessageDigest.getInstance("MD5");
    383 
    384                 byte[] d = md5.digest(param.getBytes());
    385                 if (d != null) {
    386                     return bufferToHex(d);
    387                 }
    388             } catch (NoSuchAlgorithmException e) {
    389                 throw new RuntimeException(e);
    390             }
    391         }
    392 
    393         return null;
    394     }
    395 
    396     /**
    397      * @return HEX buffer representation.
    398      */
    399     private String bufferToHex(byte[] buffer) {
    400         final char hexChars[] =
    401             { '0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f' };
    402 
    403         if (buffer != null) {
    404             int length = buffer.length;
    405             if (length > 0) {
    406                 StringBuilder hex = new StringBuilder(2 * length);
    407 
    408                 for (int i = 0; i < length; ++i) {
    409                     byte l = (byte) (buffer[i] & 0x0F);
    410                     byte h = (byte)((buffer[i] & 0xF0) >> 4);
    411 
    412                     hex.append(hexChars[h]);
    413                     hex.append(hexChars[l]);
    414                 }
    415 
    416                 return hex.toString();
    417             } else {
    418                 return "";
    419             }
    420         }
    421 
    422         return null;
    423     }
    424 
    425     /**
    426      * Computes a random cnonce value based on the current time.
    427      */
    428     private String computeCnonce() {
    429         Random rand = new Random();
    430         int nextInt = rand.nextInt();
    431         nextInt = (nextInt == Integer.MIN_VALUE) ?
    432                 Integer.MAX_VALUE : Math.abs(nextInt);
    433         return Integer.toString(nextInt, 16);
    434     }
    435 
    436     /**
    437      * "Double-quotes" the argument.
    438      */
    439     private String doubleQuote(String param) {
    440         if (param != null) {
    441             return "\"" + param + "\"";
    442         }
    443 
    444         return null;
    445     }
    446 
    447     /**
    448      * Creates and queues new request.
    449      */
    450     private void createAndQueueNewRequest() {
    451         // mConnection is non-null if and only if the requests are synchronous.
    452         if (mConnection != null) {
    453             RequestHandle newHandle = mRequestQueue.queueSynchronousRequest(
    454                     mUrl, mUri, mMethod, mHeaders, mRequest.mEventHandler,
    455                     mBodyProvider, mBodyLength);
    456             mRequest = newHandle.mRequest;
    457             mConnection = newHandle.mConnection;
    458             newHandle.processRequest();
    459             return;
    460         }
    461         mRequest = mRequestQueue.queueRequest(
    462                 mUrl, mUri, mMethod, mHeaders, mRequest.mEventHandler,
    463                 mBodyProvider,
    464                 mBodyLength).mRequest;
    465     }
    466 }
    467