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