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