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
17package android.net.http;
18
19import java.io.EOFException;
20import java.io.InputStream;
21import java.io.IOException;
22import java.util.Iterator;
23import java.util.Map;
24import java.util.Map.Entry;
25import java.util.zip.GZIPInputStream;
26
27import org.apache.http.entity.InputStreamEntity;
28import org.apache.http.Header;
29import org.apache.http.HttpClientConnection;
30import org.apache.http.HttpEntity;
31import org.apache.http.HttpEntityEnclosingRequest;
32import org.apache.http.HttpException;
33import org.apache.http.HttpHost;
34import org.apache.http.HttpRequest;
35import org.apache.http.HttpResponse;
36import org.apache.http.HttpStatus;
37import org.apache.http.HttpVersion;
38import org.apache.http.ParseException;
39import org.apache.http.ProtocolVersion;
40
41import org.apache.http.StatusLine;
42import org.apache.http.message.BasicHttpRequest;
43import org.apache.http.message.BasicHttpEntityEnclosingRequest;
44import org.apache.http.protocol.RequestContent;
45
46/**
47 * Represents an HTTP request for a given host.
48 *
49 * {@hide}
50 */
51
52class 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     */
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     */
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     */
158    void setConnection(Connection connection) {
159        mConnection = connection;
160    }
161
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     */
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     */
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     */
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     */
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     */
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
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
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     */
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     */
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     */
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
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
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     */
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     */
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     */
522    void error(int errorId, int resourceId) {
523        mEventHandler.error(
524                errorId,
525                mConnection.mContext.getText(
526                        resourceId).toString());
527    }
528
529}
530