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