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