• 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 java.io.EOFException;
20 import java.io.InputStream;
21 import java.io.IOException;
22 import java.util.Iterator;
23 import java.util.Map;
24 import java.util.Map.Entry;
25 import java.util.zip.GZIPInputStream;
26 
27 import org.apache.http.entity.InputStreamEntity;
28 import org.apache.http.Header;
29 import org.apache.http.HttpEntity;
30 import org.apache.http.HttpEntityEnclosingRequest;
31 import org.apache.http.HttpException;
32 import org.apache.http.HttpHost;
33 import org.apache.http.HttpRequest;
34 import org.apache.http.HttpStatus;
35 import org.apache.http.ParseException;
36 import org.apache.http.ProtocolVersion;
37 
38 import org.apache.http.StatusLine;
39 import org.apache.http.message.BasicHttpRequest;
40 import org.apache.http.message.BasicHttpEntityEnclosingRequest;
41 import org.apache.http.protocol.RequestContent;
42 
43 /**
44  * Represents an HTTP request for a given host.
45  */
46 
47 public class Request {
48 
49     /** The eventhandler to call as the request progresses */
50     EventHandler mEventHandler;
51 
52     private Connection mConnection;
53 
54     /** The Apache http request */
55     BasicHttpRequest mHttpRequest;
56 
57     /** The path component of this request */
58     String mPath;
59 
60     /** Host serving this request */
61     HttpHost mHost;
62 
63     /** Set if I'm using a proxy server */
64     HttpHost mProxyHost;
65 
66     /** True if request has been cancelled */
67     volatile boolean mCancelled = false;
68 
69     int mFailCount = 0;
70 
71     // This will be used to set the Range field if we retry a connection. This
72     // is http/1.1 feature.
73     private int mReceivedBytes = 0;
74 
75     private InputStream mBodyProvider;
76     private int mBodyLength;
77 
78     private final static String HOST_HEADER = "Host";
79     private final static String ACCEPT_ENCODING_HEADER = "Accept-Encoding";
80     private final static String CONTENT_LENGTH_HEADER = "content-length";
81 
82     /* Used to synchronize waitUntilComplete() requests */
83     private final Object mClientResource = new Object();
84 
85     /** True if loading should be paused **/
86     private boolean mLoadingPaused = false;
87 
88     /**
89      * Processor used to set content-length and transfer-encoding
90      * headers.
91      */
92     private static RequestContent requestContentProcessor =
93             new RequestContent();
94 
95     /**
96      * Instantiates a new Request.
97      * @param method GET/POST/PUT
98      * @param host The server that will handle this request
99      * @param path path part of URI
100      * @param bodyProvider InputStream providing HTTP body, null if none
101      * @param bodyLength length of body, must be 0 if bodyProvider is null
102      * @param eventHandler request will make progress callbacks on
103      * this interface
104      * @param headers reqeust headers
105      */
Request(String method, HttpHost host, HttpHost proxyHost, String path, InputStream bodyProvider, int bodyLength, EventHandler eventHandler, Map<String, String> headers)106     Request(String method, HttpHost host, HttpHost proxyHost, String path,
107             InputStream bodyProvider, int bodyLength,
108             EventHandler eventHandler,
109             Map<String, String> headers) {
110         mEventHandler = eventHandler;
111         mHost = host;
112         mProxyHost = proxyHost;
113         mPath = path;
114         mBodyProvider = bodyProvider;
115         mBodyLength = bodyLength;
116 
117         if (bodyProvider == null && !"POST".equalsIgnoreCase(method)) {
118             mHttpRequest = new BasicHttpRequest(method, getUri());
119         } else {
120             mHttpRequest = new BasicHttpEntityEnclosingRequest(
121                     method, getUri());
122             // it is ok to have null entity for BasicHttpEntityEnclosingRequest.
123             // By using BasicHttpEntityEnclosingRequest, it will set up the
124             // correct content-length, content-type and content-encoding.
125             if (bodyProvider != null) {
126                 setBodyProvider(bodyProvider, bodyLength);
127             }
128         }
129         addHeader(HOST_HEADER, getHostPort());
130 
131         /* FIXME: if webcore will make the root document a
132            high-priority request, we can ask for gzip encoding only on
133            high priority reqs (saving the trouble for images, etc) */
134         addHeader(ACCEPT_ENCODING_HEADER, "gzip");
135         addHeaders(headers);
136     }
137 
138     /**
139      * @param pause True if the load should be paused.
140      */
setLoadingPaused(boolean pause)141     synchronized void setLoadingPaused(boolean pause) {
142         mLoadingPaused = pause;
143 
144         // Wake up the paused thread if we're unpausing the load.
145         if (!mLoadingPaused) {
146             notify();
147         }
148     }
149 
150     /**
151      * @param connection Request served by this connection
152      */
setConnection(Connection connection)153     void setConnection(Connection connection) {
154         mConnection = connection;
155     }
156 
getEventHandler()157     /* package */ EventHandler getEventHandler() {
158         return mEventHandler;
159     }
160 
161     /**
162      * Add header represented by given pair to request.  Header will
163      * be formatted in request as "name: value\r\n".
164      * @param name of header
165      * @param value of header
166      */
addHeader(String name, String value)167     void addHeader(String name, String value) {
168         if (name == null) {
169             String damage = "Null http header name";
170             HttpLog.e(damage);
171             throw new NullPointerException(damage);
172         }
173         if (value == null || value.length() == 0) {
174             String damage = "Null or empty value for header \"" + name + "\"";
175             HttpLog.e(damage);
176             throw new RuntimeException(damage);
177         }
178         mHttpRequest.addHeader(name, value);
179     }
180 
181     /**
182      * Add all headers in given map to this request.  This is a helper
183      * method: it calls addHeader for each pair in the map.
184      */
addHeaders(Map<String, String> headers)185     void addHeaders(Map<String, String> headers) {
186         if (headers == null) {
187             return;
188         }
189 
190         Entry<String, String> entry;
191         Iterator<Entry<String, String>> i = headers.entrySet().iterator();
192         while (i.hasNext()) {
193             entry = i.next();
194             addHeader(entry.getKey(), entry.getValue());
195         }
196     }
197 
198     /**
199      * Send the request line and headers
200      */
sendRequest(AndroidHttpClientConnection httpClientConnection)201     void sendRequest(AndroidHttpClientConnection httpClientConnection)
202             throws HttpException, IOException {
203 
204         if (mCancelled) return; // don't send cancelled requests
205 
206         if (HttpLog.LOGV) {
207             HttpLog.v("Request.sendRequest() " + mHost.getSchemeName() + "://" + getHostPort());
208             // HttpLog.v(mHttpRequest.getRequestLine().toString());
209             if (false) {
210                 Iterator i = mHttpRequest.headerIterator();
211                 while (i.hasNext()) {
212                     Header header = (Header)i.next();
213                     HttpLog.v(header.getName() + ": " + header.getValue());
214                 }
215             }
216         }
217 
218         requestContentProcessor.process(mHttpRequest,
219                                         mConnection.getHttpContext());
220         httpClientConnection.sendRequestHeader(mHttpRequest);
221         if (mHttpRequest instanceof HttpEntityEnclosingRequest) {
222             httpClientConnection.sendRequestEntity(
223                     (HttpEntityEnclosingRequest) mHttpRequest);
224         }
225 
226         if (HttpLog.LOGV) {
227             HttpLog.v("Request.requestSent() " + mHost.getSchemeName() + "://" + getHostPort() + mPath);
228         }
229     }
230 
231 
232     /**
233      * Receive a single http response.
234      *
235      * @param httpClientConnection the request to receive the response for.
236      */
readResponse(AndroidHttpClientConnection httpClientConnection)237     void readResponse(AndroidHttpClientConnection httpClientConnection)
238             throws IOException, ParseException {
239 
240         if (mCancelled) return; // don't send cancelled requests
241 
242         StatusLine statusLine = null;
243         boolean hasBody = false;
244         httpClientConnection.flush();
245         int statusCode = 0;
246 
247         Headers header = new Headers();
248         do {
249             statusLine = httpClientConnection.parseResponseHeader(header);
250             statusCode = statusLine.getStatusCode();
251         } while (statusCode < HttpStatus.SC_OK);
252         if (HttpLog.LOGV) HttpLog.v(
253                 "Request.readResponseStatus() " +
254                 statusLine.toString().length() + " " + statusLine);
255 
256         ProtocolVersion v = statusLine.getProtocolVersion();
257         mEventHandler.status(v.getMajor(), v.getMinor(),
258                 statusCode, statusLine.getReasonPhrase());
259         mEventHandler.headers(header);
260         HttpEntity entity = null;
261         hasBody = canResponseHaveBody(mHttpRequest, statusCode);
262 
263         if (hasBody)
264             entity = httpClientConnection.receiveResponseEntity(header);
265 
266         // restrict the range request to the servers claiming that they are
267         // accepting ranges in bytes
268         boolean supportPartialContent = "bytes".equalsIgnoreCase(header
269                 .getAcceptRanges());
270 
271         if (entity != null) {
272             InputStream is = entity.getContent();
273 
274             // process gzip content encoding
275             Header contentEncoding = entity.getContentEncoding();
276             InputStream nis = null;
277             byte[] buf = null;
278             int count = 0;
279             try {
280                 if (contentEncoding != null &&
281                     contentEncoding.getValue().equals("gzip")) {
282                     nis = new GZIPInputStream(is);
283                 } else {
284                     nis = is;
285                 }
286 
287                 /* accumulate enough data to make it worth pushing it
288                  * up the stack */
289                 buf = mConnection.getBuf();
290                 int len = 0;
291                 int lowWater = buf.length / 2;
292                 while (len != -1) {
293                     synchronized(this) {
294                         while (mLoadingPaused) {
295                             // Put this (network loading) thread to sleep if WebCore
296                             // has asked us to. This can happen with plugins for
297                             // example, if we are streaming data but the plugin has
298                             // filled its internal buffers.
299                             try {
300                                 wait();
301                             } catch (InterruptedException e) {
302                                 HttpLog.e("Interrupted exception whilst "
303                                     + "network thread paused at WebCore's request."
304                                     + " " + e.getMessage());
305                             }
306                         }
307                     }
308 
309                     len = nis.read(buf, count, buf.length - count);
310 
311                     if (len != -1) {
312                         count += len;
313                         if (supportPartialContent) mReceivedBytes += len;
314                     }
315                     if (len == -1 || count >= lowWater) {
316                         if (HttpLog.LOGV) HttpLog.v("Request.readResponse() " + count);
317                         mEventHandler.data(buf, count);
318                         count = 0;
319                     }
320                 }
321             } catch (EOFException e) {
322                 /* InflaterInputStream throws an EOFException when the
323                    server truncates gzipped content.  Handle this case
324                    as we do truncated non-gzipped content: no error */
325                 if (count > 0) {
326                     // if there is uncommited content, we should commit them
327                     mEventHandler.data(buf, count);
328                 }
329                 if (HttpLog.LOGV) HttpLog.v( "readResponse() handling " + e);
330             } catch(IOException e) {
331                 // don't throw if we have a non-OK status code
332                 if (statusCode == HttpStatus.SC_OK
333                         || statusCode == HttpStatus.SC_PARTIAL_CONTENT) {
334                     if (supportPartialContent && count > 0) {
335                         // if there is uncommited content, we should commit them
336                         // as we will continue the request
337                         mEventHandler.data(buf, count);
338                     }
339                     throw e;
340                 }
341             } finally {
342                 if (nis != null) {
343                     nis.close();
344                 }
345             }
346         }
347         mConnection.setCanPersist(entity, statusLine.getProtocolVersion(),
348                 header.getConnectionType());
349         mEventHandler.endData();
350         complete();
351 
352         if (HttpLog.LOGV) HttpLog.v("Request.readResponse(): done " +
353                                     mHost.getSchemeName() + "://" + getHostPort() + mPath);
354     }
355 
356     /**
357      * Data will not be sent to or received from server after cancel()
358      * call.  Does not close connection--use close() below for that.
359      *
360      * Called by RequestHandle from non-network thread
361      */
cancel()362     synchronized void cancel() {
363         if (HttpLog.LOGV) {
364             HttpLog.v("Request.cancel(): " + getUri());
365         }
366 
367         // Ensure that the network thread is not blocked by a hanging request from WebCore to
368         // pause the load.
369         mLoadingPaused = false;
370         notify();
371 
372         mCancelled = true;
373         if (mConnection != null) {
374             mConnection.cancel();
375         }
376     }
377 
getHostPort()378     String getHostPort() {
379         String myScheme = mHost.getSchemeName();
380         int myPort = mHost.getPort();
381 
382         // Only send port when we must... many servers can't deal with it
383         if (myPort != 80 && myScheme.equals("http") ||
384             myPort != 443 && myScheme.equals("https")) {
385             return mHost.toHostString();
386         } else {
387             return mHost.getHostName();
388         }
389     }
390 
getUri()391     String getUri() {
392         if (mProxyHost == null ||
393             mHost.getSchemeName().equals("https")) {
394             return mPath;
395         }
396         return mHost.getSchemeName() + "://" + getHostPort() + mPath;
397     }
398 
399     /**
400      * for debugging
401      */
toString()402     public String toString() {
403         return mPath;
404     }
405 
406 
407     /**
408      * If this request has been sent once and failed, it must be reset
409      * before it can be sent again.
410      */
reset()411     void reset() {
412         /* clear content-length header */
413         mHttpRequest.removeHeaders(CONTENT_LENGTH_HEADER);
414 
415         if (mBodyProvider != null) {
416             try {
417                 mBodyProvider.reset();
418             } catch (IOException ex) {
419                 if (HttpLog.LOGV) HttpLog.v(
420                         "failed to reset body provider " +
421                         getUri());
422             }
423             setBodyProvider(mBodyProvider, mBodyLength);
424         }
425 
426         if (mReceivedBytes > 0) {
427             // reset the fail count as we continue the request
428             mFailCount = 0;
429             // set the "Range" header to indicate that the retry will continue
430             // instead of restarting the request
431             HttpLog.v("*** Request.reset() to range:" + mReceivedBytes);
432             mHttpRequest.setHeader("Range", "bytes=" + mReceivedBytes + "-");
433         }
434     }
435 
436     /**
437      * Pause thread request completes.  Used for synchronous requests,
438      * and testing
439      */
waitUntilComplete()440     void waitUntilComplete() {
441         synchronized (mClientResource) {
442             try {
443                 if (HttpLog.LOGV) HttpLog.v("Request.waitUntilComplete()");
444                 mClientResource.wait();
445                 if (HttpLog.LOGV) HttpLog.v("Request.waitUntilComplete() done waiting");
446             } catch (InterruptedException e) {
447             }
448         }
449     }
450 
complete()451     void complete() {
452         synchronized (mClientResource) {
453             mClientResource.notifyAll();
454         }
455     }
456 
457     /**
458      * Decide whether a response comes with an entity.
459      * The implementation in this class is based on RFC 2616.
460      * Unknown methods and response codes are supposed to
461      * indicate responses with an entity.
462      * <br/>
463      * Derived executors can override this method to handle
464      * methods and response codes not specified in RFC 2616.
465      *
466      * @param request   the request, to obtain the executed method
467      * @param response  the response, to obtain the status code
468      */
469 
canResponseHaveBody(final HttpRequest request, final int status)470     private static boolean canResponseHaveBody(final HttpRequest request,
471                                                final int status) {
472 
473         if ("HEAD".equalsIgnoreCase(request.getRequestLine().getMethod())) {
474             return false;
475         }
476         return status >= HttpStatus.SC_OK
477             && status != HttpStatus.SC_NO_CONTENT
478             && status != HttpStatus.SC_NOT_MODIFIED;
479     }
480 
481     /**
482      * Supply an InputStream that provides the body of a request.  It's
483      * not great that the caller must also provide the length of the data
484      * returned by that InputStream, but the client needs to know up
485      * front, and I'm not sure how to get this out of the InputStream
486      * itself without a costly readthrough.  I'm not sure skip() would
487      * do what we want.  If you know a better way, please let me know.
488      */
setBodyProvider(InputStream bodyProvider, int bodyLength)489     private void setBodyProvider(InputStream bodyProvider, int bodyLength) {
490         if (!bodyProvider.markSupported()) {
491             throw new IllegalArgumentException(
492                     "bodyProvider must support mark()");
493         }
494         // Mark beginning of stream
495         bodyProvider.mark(Integer.MAX_VALUE);
496 
497         ((BasicHttpEntityEnclosingRequest)mHttpRequest).setEntity(
498                 new InputStreamEntity(bodyProvider, bodyLength));
499     }
500 
501 
502     /**
503      * Handles SSL error(s) on the way down from the user (the user
504      * has already provided their feedback).
505      */
handleSslErrorResponse(boolean proceed)506     void handleSslErrorResponse(boolean proceed) {
507         HttpsConnection connection = (HttpsConnection)(mConnection);
508         if (connection != null) {
509             connection.restartConnection(proceed);
510         }
511     }
512 
513     /**
514      * Helper: calls error() on eventhandler with appropriate message
515      * This should not be called before the mConnection is set.
516      */
error(int errorId, String errorMessage)517     void error(int errorId, String errorMessage) {
518         mEventHandler.error(errorId, errorMessage);
519     }
520 
521 }
522