HttpEngine.java revision ca8ae42fd6a787757897a680108bdcf7b0d37f13
1/*
2 *  Licensed to the Apache Software Foundation (ASF) under one or more
3 *  contributor license agreements.  See the NOTICE file distributed with
4 *  this work for additional information regarding copyright ownership.
5 *  The ASF licenses this file to You under the Apache License, Version 2.0
6 *  (the "License"); you may not use this file except in compliance with
7 *  the License.  You may obtain a copy of the License at
8 *
9 *     http://www.apache.org/licenses/LICENSE-2.0
10 *
11 *  Unless required by applicable law or agreed to in writing, software
12 *  distributed under the License is distributed on an "AS IS" BASIS,
13 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 *  See the License for the specific language governing permissions and
15 *  limitations under the License.
16 */
17
18package libcore.net.http;
19
20import java.io.BufferedOutputStream;
21import java.io.ByteArrayInputStream;
22import java.io.IOException;
23import java.io.InputStream;
24import java.io.OutputStream;
25import java.net.CacheRequest;
26import java.net.CacheResponse;
27import java.net.CookieHandler;
28import java.net.HttpURLConnection;
29import java.net.Proxy;
30import java.net.ResponseCache;
31import java.net.URI;
32import java.net.URISyntaxException;
33import java.net.URL;
34import java.nio.charset.Charsets;
35import java.util.Collections;
36import java.util.Date;
37import java.util.HashMap;
38import java.util.List;
39import java.util.Map;
40import java.util.zip.GZIPInputStream;
41import libcore.io.IoUtils;
42import libcore.io.Streams;
43import libcore.util.EmptyArray;
44
45/**
46 * Handles a single HTTP request/response pair. Each HTTP engine follows this
47 * lifecycle:
48 * <ol>
49 *     <li>It is created.
50 *     <li>The HTTP request message is sent with sendRequest(). Once the request
51 *         is sent it is an error to modify the request headers. After
52 *         sendRequest() has been called the request body can be written to if
53 *         it exists.
54 *     <li>The HTTP response message is read with readResponse(). After the
55 *         response has been read the response headers and body can be read.
56 *         All responses have a response body input stream, though in some
57 *         instances this stream is empty.
58 * </ol>
59 *
60 * <p>The request and response may be served by the HTTP response cache, by the
61 * network, or by both in the event of a conditional GET.
62 *
63 * <p>This class may hold a socket connection that needs to be released or
64 * recycled. By default, this socket connection is held when the last byte of
65 * the response is consumed. To release the connection when it is no longer
66 * required, use {@link #automaticallyReleaseConnectionToPool()}.
67 */
68public class HttpEngine {
69    private static final CacheResponse BAD_GATEWAY_RESPONSE = new CacheResponse() {
70        @Override public Map<String, List<String>> getHeaders() throws IOException {
71            Map<String, List<String>> result = new HashMap<String, List<String>>();
72            result.put(null, Collections.singletonList("HTTP/1.1 502 Bad Gateway"));
73            return result;
74        }
75        @Override public InputStream getBody() throws IOException {
76            return new ByteArrayInputStream(EmptyArray.BYTE);
77        }
78    };
79
80    /**
81     * The maximum number of bytes to buffer when sending headers and a request
82     * body. When the headers and body can be sent in a single write, the
83     * request completes sooner. In one WiFi benchmark, using a large enough
84     * buffer sped up some uploads by half.
85     */
86    private static final int MAX_REQUEST_BUFFER_LENGTH = 32768;
87
88    public static final int DEFAULT_CHUNK_LENGTH = 1024;
89
90    public static final String OPTIONS = "OPTIONS";
91    public static final String GET = "GET";
92    public static final String HEAD = "HEAD";
93    public static final String POST = "POST";
94    public static final String PUT = "PUT";
95    public static final String DELETE = "DELETE";
96    public static final String TRACE = "TRACE";
97    public static final String CONNECT = "CONNECT";
98
99    public static final int HTTP_CONTINUE = 100;
100
101    /**
102     * HTTP 1.1 doesn't specify how many redirects to follow, but HTTP/1.0
103     * recommended 5. http://www.w3.org/Protocols/HTTP/1.0/spec.html#Code3xx
104     */
105    public static final int MAX_REDIRECTS = 5;
106
107    protected final HttpURLConnectionImpl policy;
108
109    protected final String method;
110
111    private ResponseSource responseSource;
112
113    protected HttpConnection connection;
114    private InputStream socketIn;
115    private OutputStream socketOut;
116
117    /**
118     * This stream buffers the request headers and the request body when their
119     * combined size is less than MAX_REQUEST_BUFFER_LENGTH. By combining them
120     * we can save socket writes, which in turn saves a packet transmission.
121     * This is socketOut if the request size is large or unknown.
122     */
123    private OutputStream requestOut;
124    private AbstractHttpOutputStream requestBodyOut;
125
126    private InputStream responseBodyIn;
127
128    private final ResponseCache responseCache = ResponseCache.getDefault();
129    private CacheResponse cacheResponse;
130    private CacheRequest cacheRequest;
131
132    /** The time when the request headers were written, or -1 if they haven't been written yet. */
133    private long sentRequestMillis = -1;
134
135    /**
136     * True if this client added an "Accept-Encoding: gzip" header field and is
137     * therefore responsible for also decompressing the transfer stream.
138     */
139    private boolean transparentGzip;
140
141    boolean sendChunked;
142
143    /**
144     * The version this client will use. Either 0 for HTTP/1.0, or 1 for
145     * HTTP/1.1. Upon receiving a non-HTTP/1.1 response, this client
146     * automatically sets its version to HTTP/1.0.
147     */
148    // TODO: is HTTP minor version tracked across HttpEngines?
149    private int httpMinorVersion = 1; // Assume HTTP/1.1
150
151    private final URI uri;
152
153    private final RawHeaders rawRequestHeaders;
154
155    /** Null until a response is received from the network or the cache */
156    private RawHeaders rawResponseHeaders;
157
158    /*
159     * The cache response currently being validated on a conditional get. Null
160     * if the cached response doesn't exist or doesn't need validation. If the
161     * conditional get succeeds, these will be used for the response headers and
162     * body. If it fails, these be closed and set to null.
163     */
164    private ResponseHeaders cachedResponseHeaders;
165    private InputStream cachedResponseBody;
166
167    /**
168     * True if the socket connection should be released to the connection pool
169     * when the response has been fully read.
170     */
171    private boolean automaticallyReleaseConnectionToPool;
172
173    /** True if the socket connection is no longer needed by this engine. */
174    private boolean connectionReleased;
175
176    /**
177     * @param connection the connection used for an intermediate response
178     *     immediately prior to this request/response pair, such as a same-host
179     *     redirect. This engine assumes ownership of the connection and must
180     *     release it when it is unneeded.
181     */
182    public HttpEngine(HttpURLConnectionImpl policy, String method, RawHeaders requestHeaders,
183            HttpConnection connection, RetryableOutputStream requestBodyOut) throws IOException {
184        this.policy = policy;
185        this.method = method;
186        this.rawRequestHeaders = new RawHeaders(requestHeaders);
187        this.connection = connection;
188        this.requestBodyOut = requestBodyOut;
189
190        try {
191            uri = policy.getURL().toURILenient();
192        } catch (URISyntaxException e) {
193            throw new IOException(e);
194        }
195    }
196
197    /**
198     * Figures out what the response source will be, and opens a socket to that
199     * source if necessary. Prepares the request headers and gets ready to start
200     * writing the request body if it exists.
201     */
202    public final void sendRequest() throws IOException {
203        if (responseSource != null) {
204            return;
205        }
206
207        prepareRawRequestHeaders();
208        RequestHeaders cacheRequestHeaders = new RequestHeaders(uri, rawRequestHeaders);
209        initResponseSource(cacheRequestHeaders);
210        if (responseCache instanceof DiskResponseCache) {
211            ((DiskResponseCache) responseCache).trackResponse(responseSource);
212        }
213
214        /*
215         * The raw response source may require the network, but the request
216         * headers may forbid network use. In that case, dispose of the network
217         * response and use a BAD_GATEWAY response instead.
218         */
219        if (cacheRequestHeaders.onlyIfCached && responseSource.requiresConnection()) {
220            if (responseSource == ResponseSource.CONDITIONAL_CACHE) {
221                IoUtils.closeQuietly(cachedResponseBody);
222            }
223            this.responseSource = ResponseSource.CACHE;
224            this.cacheResponse = BAD_GATEWAY_RESPONSE;
225            setResponse(RawHeaders.fromMultimap(cacheResponse.getHeaders()),
226                    cacheResponse.getBody());
227        }
228
229        if (responseSource.requiresConnection()) {
230            sendSocketRequest();
231        } else if (connection != null) {
232            HttpConnectionPool.INSTANCE.recycle(connection);
233            connection = null;
234        }
235    }
236
237    /**
238     * Initialize the source for this response. It may be corrected later if the
239     * request headers forbids network use.
240     */
241    private void initResponseSource(RequestHeaders cacheRequestHeaders) throws IOException {
242        responseSource = ResponseSource.NETWORK;
243        if (!policy.getUseCaches() || responseCache == null) {
244            return;
245        }
246
247        CacheResponse candidate = responseCache.get(uri, method, rawRequestHeaders.toMultimap());
248        if (candidate == null) {
249            return;
250        }
251
252        Map<String, List<String>> responseHeadersMap = candidate.getHeaders();
253        cachedResponseBody = candidate.getBody();
254        if (!acceptCacheResponseType(candidate)
255                || responseHeadersMap == null
256                || cachedResponseBody == null) {
257            IoUtils.closeQuietly(cachedResponseBody);
258            return;
259        }
260
261        RawHeaders rawHeaders = RawHeaders.fromMultimap(responseHeadersMap);
262        cachedResponseHeaders = new ResponseHeaders(uri, rawHeaders);
263        long now = System.currentTimeMillis();
264        this.responseSource = cachedResponseHeaders.chooseResponseSource(now, cacheRequestHeaders);
265        if (responseSource == ResponseSource.CACHE) {
266            this.cacheResponse = candidate;
267            setResponse(rawHeaders, cachedResponseBody);
268        } else if (responseSource == ResponseSource.CONDITIONAL_CACHE) {
269            this.cacheResponse = candidate;
270        } else if (responseSource == ResponseSource.NETWORK) {
271            IoUtils.closeQuietly(cachedResponseBody);
272        } else {
273            throw new AssertionError();
274        }
275    }
276
277    private void sendSocketRequest() throws IOException {
278        if (connection == null) {
279            connect();
280        }
281
282        if (socketOut != null || requestOut != null || socketIn != null) {
283            throw new IllegalStateException();
284        }
285
286        socketOut = connection.getOutputStream();
287        requestOut = socketOut;
288        socketIn = connection.getInputStream();
289
290        if (hasRequestBody()) {
291            initRequestBodyOut();
292        }
293    }
294
295    /**
296     * Connect to the origin server either directly or via a proxy.
297     */
298    protected void connect() throws IOException {
299        if (connection == null) {
300            connection = openSocketConnection();
301        }
302    }
303
304    protected final HttpConnection openSocketConnection() throws IOException {
305        HttpConnection result = HttpConnection.connect(
306                uri, policy.getProxy(), requiresTunnel(), policy.getConnectTimeout());
307        Proxy proxy = result.getAddress().getProxy();
308        if (proxy != null) {
309            policy.setProxy(proxy);
310        }
311        result.setSoTimeout(policy.getReadTimeout());
312        return result;
313    }
314
315    protected void initRequestBodyOut() throws IOException {
316        int contentLength = -1;
317        String contentLengthString = rawRequestHeaders.get("Content-Length");
318        if (contentLengthString != null) {
319            contentLength = Integer.parseInt(contentLengthString);
320        }
321
322        String encoding = rawRequestHeaders.get("Transfer-Encoding");
323        int chunkLength = policy.getChunkLength();
324        if (chunkLength > 0 || "chunked".equalsIgnoreCase(encoding)) {
325            sendChunked = true;
326            contentLength = -1;
327            if (chunkLength == -1) {
328                chunkLength = DEFAULT_CHUNK_LENGTH;
329            }
330        }
331
332        if (socketOut == null) {
333            throw new IllegalStateException("No socket to write to; was a POST cached?");
334        }
335
336        if (httpMinorVersion == 0) {
337            sendChunked = false;
338        }
339
340        int fixedContentLength = policy.getFixedContentLength();
341        if (requestBodyOut != null) {
342            // request body was already initialized by the predecessor HTTP engine
343        } else if (fixedContentLength != -1) {
344            writeRequestHeaders(fixedContentLength);
345            requestBodyOut = new FixedLengthOutputStream(requestOut, fixedContentLength);
346        } else if (sendChunked) {
347            writeRequestHeaders(-1);
348            requestBodyOut = new ChunkedOutputStream(requestOut, chunkLength);
349        } else if (contentLength != -1) {
350            writeRequestHeaders(contentLength);
351            requestBodyOut = new RetryableOutputStream(contentLength);
352        } else {
353            requestBodyOut = new RetryableOutputStream();
354        }
355    }
356
357    /**
358     * @param body the response body, or null if it doesn't exist or isn't
359     *     available.
360     */
361    private void setResponse(RawHeaders headers, InputStream body) throws IOException {
362        if (this.responseBodyIn != null) {
363            throw new IllegalStateException();
364        }
365        this.rawResponseHeaders = headers;
366        this.httpMinorVersion = rawResponseHeaders.getHttpMinorVersion();
367        if (body != null) {
368            initContentStream(body);
369        }
370    }
371
372    private boolean hasRequestBody() {
373        return method == POST || method == PUT;
374    }
375
376    /**
377     * Returns the request body or null if this request doesn't have a body.
378     */
379    public final OutputStream getRequestBody() {
380        if (responseSource == null) {
381            throw new IllegalStateException();
382        }
383        return requestBodyOut;
384    }
385
386    public final boolean hasResponse() {
387        return rawResponseHeaders != null;
388    }
389
390    public final RawHeaders getRequestHeaders() {
391        return rawRequestHeaders;
392    }
393
394    public final RawHeaders getResponseHeaders() {
395        if (rawResponseHeaders == null) {
396            throw new IllegalStateException();
397        }
398        return rawResponseHeaders;
399    }
400
401    public final InputStream getResponseBody() {
402        if (rawResponseHeaders == null) {
403            throw new IllegalStateException();
404        }
405        return responseBodyIn;
406    }
407
408    public final CacheResponse getCacheResponse() {
409        if (rawResponseHeaders == null) {
410            throw new IllegalStateException();
411        }
412        return cacheResponse;
413    }
414
415    public final HttpConnection getConnection() {
416        return connection;
417    }
418
419    /**
420     * Returns true if {@code cacheResponse} is of the right type. This
421     * condition is necessary but not sufficient for the cached response to
422     * be used.
423     */
424    protected boolean acceptCacheResponseType(CacheResponse cacheResponse) {
425        return true;
426    }
427
428    private void maybeCache() throws IOException {
429        // Are we caching at all?
430        if (!policy.getUseCaches() || responseCache == null) {
431            return;
432        }
433
434        // Should we cache this response for this request?
435        RequestHeaders requestCacheHeaders = new RequestHeaders(uri, rawRequestHeaders);
436        ResponseHeaders responseCacheHeaders = new ResponseHeaders(uri, rawResponseHeaders);
437        if (!responseCacheHeaders.isCacheable(requestCacheHeaders)) {
438            return;
439        }
440
441        // Offer this request to the cache.
442        cacheRequest = responseCache.put(uri, getHttpConnectionToCache());
443    }
444
445    protected HttpURLConnection getHttpConnectionToCache() {
446        return policy;
447    }
448
449    /**
450     * Cause the socket connection to be released to the connection pool when
451     * it is no longer needed. If it is already unneeded, it will be pooled
452     * immediately.
453     */
454    public final void automaticallyReleaseConnectionToPool() {
455        automaticallyReleaseConnectionToPool = true;
456        if (connection != null && connectionReleased) {
457            HttpConnectionPool.INSTANCE.recycle(connection);
458            connection = null;
459        }
460    }
461
462    /**
463     * Releases this engine so that its resources may be either reused or
464     * closed.
465     */
466    public final void release(boolean reusable) {
467        // If the response body comes from the cache, close it.
468        if (responseBodyIn == cachedResponseBody) {
469            IoUtils.closeQuietly(responseBodyIn);
470        }
471
472        if (!connectionReleased && connection != null) {
473            connectionReleased = true;
474
475            // We cannot reuse sockets that have incomplete output.
476            if (requestBodyOut != null && !requestBodyOut.closed) {
477                reusable = false;
478            }
479
480            // If the headers specify that the connection shouldn't be reused, don't reuse it.
481            if (hasConnectionCloseHeaders()) {
482                reusable = false;
483            }
484
485            if (responseBodyIn instanceof UnknownLengthHttpInputStream) {
486                reusable = false;
487            }
488
489            if (reusable && responseBodyIn != null) {
490                // We must discard the response body before the connection can be reused.
491                try {
492                    Streams.skipAll(responseBodyIn);
493                } catch (IOException e) {
494                    reusable = false;
495                }
496            }
497
498            if (!reusable) {
499                connection.closeSocketAndStreams();
500                connection = null;
501            } else if (automaticallyReleaseConnectionToPool) {
502                HttpConnectionPool.INSTANCE.recycle(connection);
503                connection = null;
504            }
505        }
506    }
507
508    private void initContentStream(InputStream transferStream) throws IOException {
509        if (transparentGzip
510                && "gzip".equalsIgnoreCase(rawResponseHeaders.get("Content-Encoding"))) {
511            /*
512             * If the response was transparently gzipped, remove the gzip header field
513             * so clients don't double decompress. http://b/3009828
514             */
515            rawResponseHeaders.removeAll("Content-Encoding");
516            responseBodyIn = new GZIPInputStream(transferStream);
517        } else {
518            responseBodyIn = transferStream;
519        }
520    }
521
522    private InputStream getTransferStream() throws IOException {
523        if (!hasResponseBody()) {
524            return new FixedLengthInputStream(socketIn, cacheRequest, this, 0);
525        }
526
527        if ("chunked".equalsIgnoreCase(rawResponseHeaders.get("Transfer-Encoding"))) {
528            return new ChunkedInputStream(socketIn, cacheRequest, this);
529        }
530
531        String contentLength = rawResponseHeaders.get("Content-Length");
532        if (contentLength != null) {
533            try {
534                int length = Integer.parseInt(contentLength);
535                return new FixedLengthInputStream(socketIn, cacheRequest, this, length);
536            } catch (NumberFormatException ignored) {
537            }
538        }
539
540        /*
541         * Wrap the input stream from the HttpConnection (rather than
542         * just returning "socketIn" directly here), so that we can control
543         * its use after the reference escapes.
544         */
545        return new UnknownLengthHttpInputStream(socketIn, cacheRequest, this);
546    }
547
548    private void readResponseHeaders() throws IOException {
549        RawHeaders headers;
550        do {
551            headers = new RawHeaders();
552            headers.setStatusLine(Streams.readAsciiLine(socketIn));
553            readHeaders(headers);
554            setResponse(headers, null);
555        } while (headers.getResponseCode() == HTTP_CONTINUE);
556    }
557
558    /**
559     * Returns true if the response must have a (possibly 0-length) body.
560     * See RFC 2616 section 4.3.
561     */
562    public final boolean hasResponseBody() {
563        int responseCode = rawResponseHeaders.getResponseCode();
564        if (method != HEAD
565                && method != CONNECT
566                && (responseCode < HTTP_CONTINUE || responseCode >= 200)
567                && responseCode != HttpURLConnectionImpl.HTTP_NO_CONTENT
568                && responseCode != HttpURLConnectionImpl.HTTP_NOT_MODIFIED) {
569            return true;
570        }
571
572        /*
573         * If the Content-Length or Transfer-Encoding headers disagree with the
574         * response code, the response is malformed. For best compatibility, we
575         * honor the headers.
576         */
577        String contentLength = rawResponseHeaders.get("Content-Length");
578        if (contentLength != null && Integer.parseInt(contentLength) > 0) {
579            return true;
580        }
581        if ("chunked".equalsIgnoreCase(rawResponseHeaders.get("Transfer-Encoding"))) {
582            return true;
583        }
584
585        return false;
586    }
587
588    /**
589     * Trailers are headers included after the last chunk of a response encoded
590     * with chunked encoding.
591     */
592    final void readTrailers() throws IOException {
593        readHeaders(rawResponseHeaders);
594    }
595
596    private void readHeaders(RawHeaders headers) throws IOException {
597        // parse the result headers until the first blank line
598        String line;
599        while (!(line = Streams.readAsciiLine(socketIn)).isEmpty()) {
600            headers.addLine(line);
601        }
602
603        CookieHandler cookieHandler = CookieHandler.getDefault();
604        if (cookieHandler != null) {
605            cookieHandler.put(uri, headers.toMultimap());
606        }
607    }
608
609    /**
610     * Prepares the HTTP headers and sends them to the server.
611     *
612     * <p>For streaming requests with a body, headers must be prepared
613     * <strong>before</strong> the output stream has been written to. Otherwise
614     * the body would need to be buffered!
615     *
616     * <p>For non-streaming requests with a body, headers must be prepared
617     * <strong>after</strong> the output stream has been written to and closed.
618     * This ensures that the {@code Content-Length} header field receives the
619     * proper value.
620     *
621     * @param contentLength the number of bytes in the request body, or -1 if
622     *      the request body length is unknown.
623     */
624    private void writeRequestHeaders(int contentLength) throws IOException {
625        if (sentRequestMillis != -1) {
626            throw new IllegalStateException();
627        }
628
629        RawHeaders headersToSend = getNetworkRequestHeaders();
630        byte[] bytes = headersToSend.toHeaderString().getBytes(Charsets.ISO_8859_1);
631
632        if (contentLength != -1 && bytes.length + contentLength <= MAX_REQUEST_BUFFER_LENGTH) {
633            requestOut = new BufferedOutputStream(socketOut, bytes.length + contentLength);
634        }
635
636        sentRequestMillis = System.currentTimeMillis();
637        requestOut.write(bytes);
638    }
639
640    /**
641     * Returns the headers to send on a network request.
642     *
643     * <p>This adds the content length and content-type headers, which are
644     * neither needed nor known when querying the response cache.
645     *
646     * <p>It updates the status line, which may need to be fully qualified if
647     * the connection is using a proxy.
648     */
649    protected RawHeaders getNetworkRequestHeaders() throws IOException {
650        rawRequestHeaders.setStatusLine(getRequestLine());
651
652        int fixedContentLength = policy.getFixedContentLength();
653        if (fixedContentLength != -1) {
654            rawRequestHeaders.addIfAbsent("Content-Length", Integer.toString(fixedContentLength));
655        } else if (sendChunked) {
656            rawRequestHeaders.addIfAbsent("Transfer-Encoding", "chunked");
657        } else if (requestBodyOut instanceof RetryableOutputStream) {
658            int size = ((RetryableOutputStream) requestBodyOut).contentLength();
659            rawRequestHeaders.addIfAbsent("Content-Length", Integer.toString(size));
660        }
661
662        return rawRequestHeaders;
663    }
664
665    /**
666     * Populates requestHeaders with defaults and cookies.
667     *
668     * <p>This client doesn't specify a default {@code Accept} header because it
669     * doesn't know what content types the application is interested in.
670     */
671    private void prepareRawRequestHeaders() throws IOException {
672        rawRequestHeaders.setStatusLine(getRequestLine());
673
674        if (rawRequestHeaders.get("User-Agent") == null) {
675            rawRequestHeaders.add("User-Agent", getDefaultUserAgent());
676        }
677
678        if (rawRequestHeaders.get("Host") == null) {
679            rawRequestHeaders.add("Host", getOriginAddress(policy.getURL()));
680        }
681
682        if (httpMinorVersion > 0) {
683            rawRequestHeaders.addIfAbsent("Connection", "Keep-Alive");
684        }
685
686        if (rawRequestHeaders.get("Accept-Encoding") == null) {
687            transparentGzip = true;
688            rawRequestHeaders.add("Accept-Encoding", "gzip");
689        }
690
691        if (hasRequestBody()) {
692            rawRequestHeaders.addIfAbsent("Content-Type", "application/x-www-form-urlencoded");
693        }
694
695        long ifModifiedSince = policy.getIfModifiedSince();
696        if (ifModifiedSince != 0) {
697            rawRequestHeaders.add("If-Modified-Since", HttpDate.format(new Date(ifModifiedSince)));
698        }
699
700        CookieHandler cookieHandler = CookieHandler.getDefault();
701        if (cookieHandler != null) {
702            Map<String, List<String>> allCookieHeaders
703                    = cookieHandler.get(uri, rawRequestHeaders.toMultimap());
704            for (Map.Entry<String, List<String>> entry : allCookieHeaders.entrySet()) {
705                String key = entry.getKey();
706                if ("Cookie".equalsIgnoreCase(key) || "Cookie2".equalsIgnoreCase(key)) {
707                    rawRequestHeaders.addAll(key, entry.getValue());
708                }
709            }
710        }
711    }
712
713    private String getRequestLine() {
714        String protocol = (httpMinorVersion == 0) ? "HTTP/1.0" : "HTTP/1.1";
715        return method + " " + requestString() + " " + protocol;
716    }
717
718    private String requestString() {
719        URL url = policy.getURL();
720        if (includeAuthorityInRequestLine()) {
721            return url.toString();
722        } else {
723            String fileOnly = url.getFile();
724            if (fileOnly == null || fileOnly.isEmpty()) {
725                fileOnly = "/";
726            }
727            return fileOnly;
728        }
729    }
730
731    /**
732     * Returns true if the request line should contain the full URL with host
733     * and port (like "GET http://android.com/foo HTTP/1.1") or only the path
734     * (like "GET /foo HTTP/1.1").
735     *
736     * <p>This is non-final because for HTTPS it's never necessary to supply the
737     * full URL, even if a proxy is in use.
738     */
739    protected boolean includeAuthorityInRequestLine() {
740        return policy.usingProxy();
741    }
742
743    protected final String getDefaultUserAgent() {
744        String agent = System.getProperty("http.agent");
745        return agent != null ? agent : ("Java" + System.getProperty("java.version"));
746    }
747
748    private boolean hasConnectionCloseHeaders() {
749        return (rawResponseHeaders != null
750                && "close".equalsIgnoreCase(rawResponseHeaders.get("Connection")))
751                || ("close".equalsIgnoreCase(rawRequestHeaders.get("Connection")));
752    }
753
754    protected final String getOriginAddress(URL url) {
755        int port = url.getPort();
756        String result = url.getHost();
757        if (port > 0 && port != policy.getDefaultPort()) {
758            result = result + ":" + port;
759        }
760        return result;
761    }
762
763    protected boolean requiresTunnel() {
764        return false;
765    }
766
767    /**
768     * Flushes the remaining request header and body, parses the HTTP response
769     * headers and starts reading the HTTP response body if it exists.
770     */
771    public final void readResponse() throws IOException {
772        if (hasResponse()) {
773            return;
774        }
775
776        if (responseSource == null) {
777            throw new IllegalStateException("readResponse() without sendRequest()");
778        }
779
780        if (!responseSource.requiresConnection()) {
781            return;
782        }
783
784        if (sentRequestMillis == -1) {
785            int contentLength = requestBodyOut instanceof RetryableOutputStream
786                    ? ((RetryableOutputStream) requestBodyOut).contentLength()
787                    : -1;
788            writeRequestHeaders(contentLength);
789        }
790
791        if (requestBodyOut != null) {
792            requestBodyOut.close();
793            if (requestBodyOut instanceof RetryableOutputStream) {
794                ((RetryableOutputStream) requestBodyOut).writeToSocket(requestOut);
795            }
796        }
797
798        requestOut.flush();
799        requestOut = socketOut;
800
801        readResponseHeaders();
802        rawResponseHeaders.add(ResponseHeaders.SENT_MILLIS, Long.toString(sentRequestMillis));
803        rawResponseHeaders.add(ResponseHeaders.RECEIVED_MILLIS,
804                Long.toString(System.currentTimeMillis()));
805
806        if (responseSource == ResponseSource.CONDITIONAL_CACHE) {
807            if (cachedResponseHeaders.validate(new ResponseHeaders(uri, rawResponseHeaders))) {
808                if (responseCache instanceof DiskResponseCache) {
809                    ((DiskResponseCache) responseCache).trackConditionalCacheHit();
810                }
811                // discard the network response and use the cache response
812                release(true);
813                setResponse(cachedResponseHeaders.headers, cachedResponseBody);
814                return;
815            } else {
816                IoUtils.closeQuietly(cachedResponseBody);
817            }
818        }
819
820        if (hasResponseBody()) {
821            maybeCache(); // reentrant. this calls into user code which may call back into this!
822        }
823
824        initContentStream(getTransferStream());
825    }
826}
827