• Home
  • History
  • Annotate
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package android.net.http;
18 
19 import android.net.compatibility.WebAddress;
20 import android.webkit.CookieManager;
21 
22 import org.apache.commons.codec.binary.Base64;
23 
24 import java.io.InputStream;
25 import java.lang.Math;
26 import java.security.MessageDigest;
27 import java.security.NoSuchAlgorithmException;
28 import java.util.HashMap;
29 import java.util.Map;
30 import java.util.Random;
31 
32 /**
33  * RequestHandle: handles a request session that may include multiple
34  * redirects, HTTP authentication requests, etc.
35  */
36 public 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      */
RequestHandle(RequestQueue requestQueue, String url, WebAddress uri, String method, Map<String, String> headers, InputStream bodyProvider, int bodyLength, Request request)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      */
RequestHandle(RequestQueue requestQueue, String url, WebAddress uri, String method, Map<String, String> headers, InputStream bodyProvider, int bodyLength, Request request, Connection conn)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      */
cancel()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      */
pauseRequest(boolean pause)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      */
handleSslErrorResponse(boolean proceed)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      */
isRedirectMax()123     public boolean isRedirectMax() {
124         return mRedirectCount >= MAX_REDIRECT_COUNT;
125     }
126 
getRedirectCount()127     public int getRedirectCount() {
128         return mRedirectCount;
129     }
130 
setRedirectCount(int count)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      */
setupRedirect(String redirectTo, int statusCode, Map<String, String> cacheHeaders)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      */
setupBasicAuthResponse(boolean isProxy, String username, String password)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      */
setupDigestAuthResponse(boolean isProxy, String username, String password, String realm, String nonce, String QOP, String algorithm, String opaque)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 
setupAuthResponse()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      */
getMethod()267     public String getMethod() {
268         return mMethod;
269     }
270 
271     /**
272      * @return Basic-scheme authentication response: BASE64(username:password).
273      */
computeBasicAuthResponse(String username, String password)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 
waitUntilComplete()287     public void waitUntilComplete() {
288         mRequest.waitUntilComplete();
289     }
290 
processRequest()291     public void processRequest() {
292         if (mConnection != null) {
293             mConnection.processRequests(mRequest);
294         }
295     }
296 
297     /**
298      * @return Digest-scheme authentication response.
299      */
computeDigestAuthResponse(String username, String password, String realm, String nonce, String QOP, String algorithm, String opaque)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      */
authorizationHeader(boolean isProxy)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      */
computeDigest( String A1, String A2, String nonce, String QOP, String nc, String cnonce)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      */
KD(String secret, String data)384     private String KD(String secret, String data) {
385         return H(secret + ":" + data);
386     }
387 
388     /**
389      * @return MD5 hash of param.
390      */
H(String param)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      */
bufferToHex(byte[] buffer)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      */
computeCnonce()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      */
doubleQuote(String param)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      */
createAndQueueNewRequest()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