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