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.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      */
     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      */
     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      */
     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      */
    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      */
    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      */
    123     public boolean isRedirectMax() {
    124         return mRedirectCount >= MAX_REDIRECT_COUNT;
    125     }
    126 
    127     public int getRedirectCount() {
    128         return mRedirectCount;
    129     }
    130 
    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      */
    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      */
    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         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 
    287     public void waitUntilComplete() {
    288         mRequest.waitUntilComplete();
    289     }
    290 
    291     public void processRequest() {
    292         if (mConnection != null) {
    293             mConnection.processRequests(mRequest);
    294         }
    295     }
    296 
    297     /**
    298      * @return Digest-scheme authentication response.
    299      */
    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      */
    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      */
    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      */
    384     private String KD(String secret, String data) {
    385         return H(secret + ":" + data);
    386     }
    387 
    388     /**
    389      * @return MD5 hash of param.
    390      */
    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      */
    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      */
    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      */
    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      */
    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