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