RequestHandle.java revision 5b494c1ca4c3cf0e0992c59fe34ae66c81e6dcce
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
17package android.net.http;
18
19import android.net.ParseException;
20import android.net.WebAddress;
21import android.security.Md5MessageDigest;
22import junit.framework.Assert;
23import android.webkit.CookieManager;
24
25import org.apache.commons.codec.binary.Base64;
26
27import java.io.InputStream;
28import java.lang.Math;
29import java.util.HashMap;
30import java.util.Map;
31import 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 */
39public 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("setupAuthResponse() 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        mRequest = mRequestQueue.queueRequest(
447                mUrl, mUri, mMethod, mHeaders, mRequest.mEventHandler,
448                mBodyProvider,
449                mBodyLength).mRequest;
450    }
451}
452