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