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