RequestHandle.java revision 54b6cfa9a9e5b861a9930af873580d6dc20f773c
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)
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        mHeaders.remove("Content-Type");
168        mBodyProvider = null;
169
170        // Update the cache headers for this URL
171        mHeaders.putAll(cacheHeaders);
172
173        createAndQueueNewRequest();
174        return true;
175    }
176
177    /**
178     * Create and queue an HTTP authentication-response (basic) request.
179     */
180    public void setupBasicAuthResponse(boolean isProxy, String username, String password) {
181        String response = computeBasicAuthResponse(username, password);
182        if (HttpLog.LOGV) {
183            HttpLog.v("setupBasicAuthResponse(): response: " + response);
184        }
185        mHeaders.put(authorizationHeader(isProxy), "Basic " + response);
186        setupAuthResponse();
187    }
188
189    /**
190     * Create and queue an HTTP authentication-response (digest) request.
191     */
192    public void setupDigestAuthResponse(boolean isProxy,
193                                        String username,
194                                        String password,
195                                        String realm,
196                                        String nonce,
197                                        String QOP,
198                                        String algorithm,
199                                        String opaque) {
200
201        String response = computeDigestAuthResponse(
202                username, password, realm, nonce, QOP, algorithm, opaque);
203        if (HttpLog.LOGV) {
204            HttpLog.v("setupDigestAuthResponse(): response: " + response);
205        }
206        mHeaders.put(authorizationHeader(isProxy), "Digest " + response);
207        setupAuthResponse();
208    }
209
210    private void setupAuthResponse() {
211        try {
212            if (mBodyProvider != null) mBodyProvider.reset();
213        } catch (java.io.IOException ex) {
214            if (HttpLog.LOGV) {
215                HttpLog.v("setupAuthResponse() failed to reset body provider");
216            }
217        }
218        createAndQueueNewRequest();
219    }
220
221    /**
222     * @return HTTP request method (GET, PUT, etc).
223     */
224    public String getMethod() {
225        return mMethod;
226    }
227
228    /**
229     * @return Basic-scheme authentication response: BASE64(username:password).
230     */
231    public static String computeBasicAuthResponse(String username, String password) {
232        Assert.assertNotNull(username);
233        Assert.assertNotNull(password);
234
235        // encode username:password to base64
236        return new String(Base64.encodeBase64((username + ':' + password).getBytes()));
237    }
238
239    public void waitUntilComplete() {
240        mRequest.waitUntilComplete();
241    }
242
243    /**
244     * @return Digest-scheme authentication response.
245     */
246    private String computeDigestAuthResponse(String username,
247                                             String password,
248                                             String realm,
249                                             String nonce,
250                                             String QOP,
251                                             String algorithm,
252                                             String opaque) {
253
254        Assert.assertNotNull(username);
255        Assert.assertNotNull(password);
256        Assert.assertNotNull(realm);
257
258        String A1 = username + ":" + realm + ":" + password;
259        String A2 = mMethod  + ":" + mUrl;
260
261        // because we do not preemptively send authorization headers, nc is always 1
262        String nc = "000001";
263        String cnonce = computeCnonce();
264        String digest = computeDigest(A1, A2, nonce, QOP, nc, cnonce);
265
266        String response = "";
267        response += "username=" + doubleQuote(username) + ", ";
268        response += "realm="    + doubleQuote(realm)    + ", ";
269        response += "nonce="    + doubleQuote(nonce)    + ", ";
270        response += "uri="      + doubleQuote(mUrl)     + ", ";
271        response += "response=" + doubleQuote(digest) ;
272
273        if (opaque     != null) {
274            response += ", opaque=" + doubleQuote(opaque);
275        }
276
277         if (algorithm != null) {
278            response += ", algorithm=" +  algorithm;
279        }
280
281        if (QOP        != null) {
282            response += ", qop=" + QOP + ", nc=" + nc + ", cnonce=" + doubleQuote(cnonce);
283        }
284
285        return response;
286    }
287
288    /**
289     * @return The right authorization header (dependeing on whether it is a proxy or not).
290     */
291    public static String authorizationHeader(boolean isProxy) {
292        if (!isProxy) {
293            return AUTHORIZATION_HEADER;
294        } else {
295            return PROXY_AUTHORIZATION_HEADER;
296        }
297    }
298
299    /**
300     * @return Double-quoted MD5 digest.
301     */
302    private String computeDigest(
303        String A1, String A2, String nonce, String QOP, String nc, String cnonce) {
304        if (HttpLog.LOGV) {
305            HttpLog.v("computeDigest(): QOP: " + QOP);
306        }
307
308        if (QOP == null) {
309            return KD(H(A1), nonce + ":" + H(A2));
310        } else {
311            if (QOP.equalsIgnoreCase("auth")) {
312                return KD(H(A1), nonce + ":" + nc + ":" + cnonce + ":" + QOP + ":" + H(A2));
313            }
314        }
315
316        return null;
317    }
318
319    /**
320     * @return MD5 hash of concat(secret, ":", data).
321     */
322    private String KD(String secret, String data) {
323        return H(secret + ":" + data);
324    }
325
326    /**
327     * @return MD5 hash of param.
328     */
329    private String H(String param) {
330        if (param != null) {
331            Md5MessageDigest md5 = new Md5MessageDigest();
332
333            byte[] d = md5.digest(param.getBytes());
334            if (d != null) {
335                return bufferToHex(d);
336            }
337        }
338
339        return null;
340    }
341
342    /**
343     * @return HEX buffer representation.
344     */
345    private String bufferToHex(byte[] buffer) {
346        final char hexChars[] =
347            { '0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f' };
348
349        if (buffer != null) {
350            int length = buffer.length;
351            if (length > 0) {
352                StringBuilder hex = new StringBuilder(2 * length);
353
354                for (int i = 0; i < length; ++i) {
355                    byte l = (byte) (buffer[i] & 0x0F);
356                    byte h = (byte)((buffer[i] & 0xF0) >> 4);
357
358                    hex.append(hexChars[h]);
359                    hex.append(hexChars[l]);
360                }
361
362                return hex.toString();
363            } else {
364                return "";
365            }
366        }
367
368        return null;
369    }
370
371    /**
372     * Computes a random cnonce value based on the current time.
373     */
374    private String computeCnonce() {
375        Random rand = new Random();
376        int nextInt = rand.nextInt();
377        nextInt = (nextInt == Integer.MIN_VALUE) ?
378                Integer.MAX_VALUE : Math.abs(nextInt);
379        return Integer.toString(nextInt, 16);
380    }
381
382    /**
383     * "Double-quotes" the argument.
384     */
385    private String doubleQuote(String param) {
386        if (param != null) {
387            return "\"" + param + "\"";
388        }
389
390        return null;
391    }
392
393    /**
394     * Creates and queues new request.
395     */
396    private void createAndQueueNewRequest() {
397        mRequest = mRequestQueue.queueRequest(
398                mUrl, mUri, mMethod, mHeaders, mRequest.mEventHandler,
399                mBodyProvider,
400                mBodyLength, mRequest.mHighPriority).mRequest;
401    }
402}
403