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.HttpEntity;
30import org.apache.http.HttpEntityEnclosingRequest;
31import org.apache.http.HttpException;
32import org.apache.http.HttpHost;
33import org.apache.http.HttpRequest;
34import org.apache.http.HttpStatus;
35import org.apache.http.ParseException;
36import org.apache.http.ProtocolVersion;
37
38import org.apache.http.StatusLine;
39import org.apache.http.message.BasicHttpRequest;
40import org.apache.http.message.BasicHttpEntityEnclosingRequest;
41import org.apache.http.protocol.RequestContent;
42
43/**
44 * Represents an HTTP request for a given host.
45 *
46 * {@hide}
47 */
48
49class 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     */
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     */
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     */
155    void setConnection(Connection connection) {
156        mConnection = connection;
157    }
158
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     */
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     */
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     */
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     */
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     */
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
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
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     */
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     */
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     */
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
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
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     */
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     */
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     */
519    void error(int errorId, int resourceId) {
520        mEventHandler.error(
521                errorId,
522                mConnection.mContext.getText(
523                        resourceId).toString());
524    }
525
526}
527