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