HttpEngine.java revision 757afaa7afe96791a3cc612c9e3c4597a7321c7e
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
211        /*
212         * The raw response source may require the network, but the request
213         * headers may forbid network use. In that case, dispose of the network
214         * response and use a BAD_GATEWAY response instead.
215         */
216        if (cacheRequestHeaders.onlyIfCached && responseSource.requiresConnection()) {
217            if (responseSource == ResponseSource.CONDITIONAL_CACHE) {
218                IoUtils.closeQuietly(cachedResponseBody);
219            }
220            this.responseSource = ResponseSource.CACHE;
221            this.cacheResponse = BAD_GATEWAY_RESPONSE;
222            setResponse(RawHeaders.fromMultimap(cacheResponse.getHeaders()),
223                    cacheResponse.getBody());
224        }
225
226        if (responseSource.requiresConnection()) {
227            sendSocketRequest();
228        } else if (connection != null) {
229            HttpConnectionPool.INSTANCE.recycle(connection);
230            connection = null;
231        }
232    }
233
234    /**
235     * Initialize the source for this response. It may be corrected later if the
236     * request headers forbids network use.
237     */
238    private void initResponseSource(RequestHeaders cacheRequestHeaders) throws IOException {
239        responseSource = ResponseSource.NETWORK;
240        if (!policy.getUseCaches() || responseCache == null) {
241            return;
242        }
243
244        CacheResponse candidate = responseCache.get(uri, method, rawRequestHeaders.toMultimap());
245        if (candidate == null) {
246            return;
247        }
248
249        Map<String, List<String>> responseHeadersMap = candidate.getHeaders();
250        cachedResponseBody = candidate.getBody();
251        if (!acceptCacheResponseType(candidate)
252                || responseHeadersMap == null
253                || cachedResponseBody == null) {
254            IoUtils.closeQuietly(cachedResponseBody);
255            return;
256        }
257
258        RawHeaders rawHeaders = RawHeaders.fromMultimap(responseHeadersMap);
259        cachedResponseHeaders = new ResponseHeaders(uri, rawHeaders);
260        long now = System.currentTimeMillis();
261        this.responseSource = cachedResponseHeaders.chooseResponseSource(now, cacheRequestHeaders);
262        if (responseSource == ResponseSource.CACHE) {
263            this.cacheResponse = candidate;
264            setResponse(rawHeaders, cachedResponseBody);
265        } else if (responseSource == ResponseSource.CONDITIONAL_CACHE) {
266            this.cacheResponse = candidate;
267        } else if (responseSource == ResponseSource.NETWORK) {
268            IoUtils.closeQuietly(cachedResponseBody);
269        } else {
270            throw new AssertionError();
271        }
272    }
273
274    private void sendSocketRequest() throws IOException {
275        if (connection == null) {
276            connect();
277        }
278
279        if (socketOut != null || requestOut != null || socketIn != null) {
280            throw new IllegalStateException();
281        }
282
283        socketOut = connection.getOutputStream();
284        requestOut = socketOut;
285        socketIn = connection.getInputStream();
286
287        if (hasRequestBody()) {
288            initRequestBodyOut();
289        }
290    }
291
292    /**
293     * Connect to the origin server either directly or via a proxy.
294     */
295    protected void connect() throws IOException {
296        if (connection == null) {
297            connection = openSocketConnection();
298        }
299    }
300
301    protected final HttpConnection openSocketConnection() throws IOException {
302        HttpConnection result = HttpConnection.connect(
303                uri, policy.getProxy(), requiresTunnel(), policy.getConnectTimeout());
304        Proxy proxy = result.getAddress().getProxy();
305        if (proxy != null) {
306            policy.setProxy(proxy);
307        }
308        result.setSoTimeout(policy.getReadTimeout());
309        return result;
310    }
311
312    protected void initRequestBodyOut() throws IOException {
313        int contentLength = -1;
314        String contentLengthString = rawRequestHeaders.get("Content-Length");
315        if (contentLengthString != null) {
316            contentLength = Integer.parseInt(contentLengthString);
317        }
318
319        String encoding = rawRequestHeaders.get("Transfer-Encoding");
320        int chunkLength = policy.getChunkLength();
321        if (chunkLength > 0 || "chunked".equalsIgnoreCase(encoding)) {
322            sendChunked = true;
323            contentLength = -1;
324            if (chunkLength == -1) {
325                chunkLength = DEFAULT_CHUNK_LENGTH;
326            }
327        }
328
329        if (socketOut == null) {
330            throw new IllegalStateException("No socket to write to; was a POST cached?");
331        }
332
333        if (httpMinorVersion == 0) {
334            sendChunked = false;
335        }
336
337        int fixedContentLength = policy.getFixedContentLength();
338        if (requestBodyOut != null) {
339            // request body was already initialized by the predecessor HTTP engine
340        } else if (fixedContentLength != -1) {
341            writeRequestHeaders(fixedContentLength);
342            requestBodyOut = new FixedLengthOutputStream(requestOut, fixedContentLength);
343        } else if (sendChunked) {
344            writeRequestHeaders(-1);
345            requestBodyOut = new ChunkedOutputStream(requestOut, chunkLength);
346        } else if (contentLength != -1) {
347            writeRequestHeaders(contentLength);
348            requestBodyOut = new RetryableOutputStream(contentLength);
349        } else {
350            requestBodyOut = new RetryableOutputStream();
351        }
352    }
353
354    /**
355     * @param body the response body, or null if it doesn't exist or isn't
356     *     available.
357     */
358    private void setResponse(RawHeaders headers, InputStream body) throws IOException {
359        if (this.responseBodyIn != null) {
360            throw new IllegalStateException();
361        }
362        this.rawResponseHeaders = headers;
363        this.httpMinorVersion = rawResponseHeaders.getHttpMinorVersion();
364        if (body != null) {
365            initContentStream(body);
366        }
367    }
368
369    private boolean hasRequestBody() {
370        return method == POST || method == PUT;
371    }
372
373    /**
374     * Returns the request body or null if this request doesn't have a body.
375     */
376    public final OutputStream getRequestBody() {
377        if (responseSource == null) {
378            throw new IllegalStateException();
379        }
380        return requestBodyOut;
381    }
382
383    public final boolean hasResponse() {
384        return rawResponseHeaders != null;
385    }
386
387    public final RawHeaders getRequestHeaders() {
388        return rawRequestHeaders;
389    }
390
391    public final RawHeaders getResponseHeaders() {
392        if (rawResponseHeaders == null) {
393            throw new IllegalStateException();
394        }
395        return rawResponseHeaders;
396    }
397
398    public final InputStream getResponseBody() {
399        if (rawResponseHeaders == null) {
400            throw new IllegalStateException();
401        }
402        return responseBodyIn;
403    }
404
405    public final CacheResponse getCacheResponse() {
406        if (rawResponseHeaders == null) {
407            throw new IllegalStateException();
408        }
409        return cacheResponse;
410    }
411
412    public final HttpConnection getConnection() {
413        return connection;
414    }
415
416    /**
417     * Returns true if {@code cacheResponse} is of the right type. This
418     * condition is necessary but not sufficient for the cached response to
419     * be used.
420     */
421    protected boolean acceptCacheResponseType(CacheResponse cacheResponse) {
422        return true;
423    }
424
425    private void maybeCache() throws IOException {
426        // Are we caching at all?
427        if (!policy.getUseCaches() || responseCache == null) {
428            return;
429        }
430
431        // Should we cache this response for this request?
432        RequestHeaders requestCacheHeaders = new RequestHeaders(uri, rawRequestHeaders);
433        ResponseHeaders responseCacheHeaders = new ResponseHeaders(uri, rawResponseHeaders);
434        if (!responseCacheHeaders.isCacheable(requestCacheHeaders)) {
435            return;
436        }
437
438        // Offer this request to the cache.
439        cacheRequest = responseCache.put(uri, getHttpConnectionToCache());
440    }
441
442    protected HttpURLConnection getHttpConnectionToCache() {
443        return policy;
444    }
445
446    /**
447     * Cause the socket connection to be released to the connection pool when
448     * it is no longer needed. If it is already unneeded, it will be pooled
449     * immediately.
450     */
451    public final void automaticallyReleaseConnectionToPool() {
452        automaticallyReleaseConnectionToPool = true;
453        if (connection != null && connectionReleased) {
454            HttpConnectionPool.INSTANCE.recycle(connection);
455            connection = null;
456        }
457    }
458
459    /**
460     * Releases this engine so that its resources may be either reused or
461     * closed.
462     */
463    public final void release(boolean reusable) {
464        // If the response body comes from the cache, close it.
465        if (responseBodyIn == cachedResponseBody) {
466            IoUtils.closeQuietly(responseBodyIn);
467        }
468
469        if (!connectionReleased && connection != null) {
470            connectionReleased = true;
471
472            // We cannot reuse sockets that have incomplete output.
473            if (requestBodyOut != null && !requestBodyOut.closed) {
474                reusable = false;
475            }
476
477            // If the headers specify that the connection shouldn't be reused, don't reuse it.
478            if (hasConnectionCloseHeaders()) {
479                reusable = false;
480            }
481
482            if (responseBodyIn instanceof UnknownLengthHttpInputStream) {
483                reusable = false;
484            }
485
486            if (reusable && responseBodyIn != null) {
487                // We must discard the response body before the connection can be reused.
488                try {
489                    Streams.skipAll(responseBodyIn);
490                } catch (IOException e) {
491                    reusable = false;
492                }
493            }
494
495            if (!reusable) {
496                connection.closeSocketAndStreams();
497                connection = null;
498            } else if (automaticallyReleaseConnectionToPool) {
499                HttpConnectionPool.INSTANCE.recycle(connection);
500                connection = null;
501            }
502        }
503    }
504
505    private void initContentStream(InputStream transferStream) throws IOException {
506        if (transparentGzip
507                && "gzip".equalsIgnoreCase(rawResponseHeaders.get("Content-Encoding"))) {
508            /*
509             * If the response was transparently gzipped, remove the gzip header field
510             * so clients don't double decompress. http://b/3009828
511             */
512            rawResponseHeaders.removeAll("Content-Encoding");
513            responseBodyIn = new GZIPInputStream(transferStream);
514        } else {
515            responseBodyIn = transferStream;
516        }
517    }
518
519    private InputStream getTransferStream() throws IOException {
520        if (!hasResponseBody()) {
521            return new FixedLengthInputStream(socketIn, cacheRequest, this, 0);
522        }
523
524        if ("chunked".equalsIgnoreCase(rawResponseHeaders.get("Transfer-Encoding"))) {
525            return new ChunkedInputStream(socketIn, cacheRequest, this);
526        }
527
528        String contentLength = rawResponseHeaders.get("Content-Length");
529        if (contentLength != null) {
530            try {
531                int length = Integer.parseInt(contentLength);
532                return new FixedLengthInputStream(socketIn, cacheRequest, this, length);
533            } catch (NumberFormatException ignored) {
534            }
535        }
536
537        /*
538         * Wrap the input stream from the HttpConnection (rather than
539         * just returning "socketIn" directly here), so that we can control
540         * its use after the reference escapes.
541         */
542        return new UnknownLengthHttpInputStream(socketIn, cacheRequest, this);
543    }
544
545    private void readResponseHeaders() throws IOException {
546        RawHeaders headers;
547        do {
548            headers = new RawHeaders();
549            headers.setStatusLine(Streams.readAsciiLine(socketIn));
550            readHeaders(headers);
551            setResponse(headers, null);
552        } while (headers.getResponseCode() == HTTP_CONTINUE);
553    }
554
555    /**
556     * Returns true if the response must have a (possibly 0-length) body.
557     * See RFC 2616 section 4.3.
558     */
559    public final boolean hasResponseBody() {
560        int responseCode = rawResponseHeaders.getResponseCode();
561        if (method != HEAD
562                && method != CONNECT
563                && (responseCode < HTTP_CONTINUE || responseCode >= 200)
564                && responseCode != HttpURLConnectionImpl.HTTP_NO_CONTENT
565                && responseCode != HttpURLConnectionImpl.HTTP_NOT_MODIFIED) {
566            return true;
567        }
568
569        /*
570         * If the Content-Length or Transfer-Encoding headers disagree with the
571         * response code, the response is malformed. For best compatibility, we
572         * honor the headers.
573         */
574        String contentLength = rawResponseHeaders.get("Content-Length");
575        if (contentLength != null && Integer.parseInt(contentLength) > 0) {
576            return true;
577        }
578        if ("chunked".equalsIgnoreCase(rawResponseHeaders.get("Transfer-Encoding"))) {
579            return true;
580        }
581
582        return false;
583    }
584
585    /**
586     * Trailers are headers included after the last chunk of a response encoded
587     * with chunked encoding.
588     */
589    final void readTrailers() throws IOException {
590        readHeaders(rawResponseHeaders);
591    }
592
593    private void readHeaders(RawHeaders headers) throws IOException {
594        // parse the result headers until the first blank line
595        String line;
596        while (!(line = Streams.readAsciiLine(socketIn)).isEmpty()) {
597            headers.addLine(line);
598        }
599
600        CookieHandler cookieHandler = CookieHandler.getDefault();
601        if (cookieHandler != null) {
602            cookieHandler.put(uri, headers.toMultimap());
603        }
604    }
605
606    /**
607     * Prepares the HTTP headers and sends them to the server.
608     *
609     * <p>For streaming requests with a body, headers must be prepared
610     * <strong>before</strong> the output stream has been written to. Otherwise
611     * the body would need to be buffered!
612     *
613     * <p>For non-streaming requests with a body, headers must be prepared
614     * <strong>after</strong> the output stream has been written to and closed.
615     * This ensures that the {@code Content-Length} header field receives the
616     * proper value.
617     *
618     * @param contentLength the number of bytes in the request body, or -1 if
619     *      the request body length is unknown.
620     */
621    private void writeRequestHeaders(int contentLength) throws IOException {
622        if (sentRequestMillis != -1) {
623            throw new IllegalStateException();
624        }
625
626        RawHeaders headersToSend = getNetworkRequestHeaders();
627        byte[] bytes = headersToSend.toHeaderString().getBytes(Charsets.ISO_8859_1);
628
629        if (contentLength != -1 && bytes.length + contentLength <= MAX_REQUEST_BUFFER_LENGTH) {
630            requestOut = new BufferedOutputStream(socketOut, bytes.length + contentLength);
631        }
632
633        sentRequestMillis = System.currentTimeMillis();
634        requestOut.write(bytes);
635    }
636
637    /**
638     * Returns the headers to send on a network request.
639     *
640     * <p>This adds the content length and content-type headers, which are
641     * neither needed nor known when querying the response cache.
642     *
643     * <p>It updates the status line, which may need to be fully qualified if
644     * the connection is using a proxy.
645     */
646    protected RawHeaders getNetworkRequestHeaders() throws IOException {
647        rawRequestHeaders.setStatusLine(getRequestLine());
648
649        int fixedContentLength = policy.getFixedContentLength();
650        if (fixedContentLength != -1) {
651            rawRequestHeaders.addIfAbsent("Content-Length", Integer.toString(fixedContentLength));
652        } else if (sendChunked) {
653            rawRequestHeaders.addIfAbsent("Transfer-Encoding", "chunked");
654        } else if (requestBodyOut instanceof RetryableOutputStream) {
655            int size = ((RetryableOutputStream) requestBodyOut).contentLength();
656            rawRequestHeaders.addIfAbsent("Content-Length", Integer.toString(size));
657        }
658
659        return rawRequestHeaders;
660    }
661
662    /**
663     * Populates requestHeaders with defaults and cookies.
664     *
665     * <p>This client doesn't specify a default {@code Accept} header because it
666     * doesn't know what content types the application is interested in.
667     */
668    private void prepareRawRequestHeaders() throws IOException {
669        rawRequestHeaders.setStatusLine(getRequestLine());
670
671        if (rawRequestHeaders.get("User-Agent") == null) {
672            rawRequestHeaders.add("User-Agent", getDefaultUserAgent());
673        }
674
675        if (rawRequestHeaders.get("Host") == null) {
676            rawRequestHeaders.add("Host", getOriginAddress(policy.getURL()));
677        }
678
679        if (httpMinorVersion > 0) {
680            rawRequestHeaders.addIfAbsent("Connection", "Keep-Alive");
681        }
682
683        if (rawRequestHeaders.get("Accept-Encoding") == null) {
684            transparentGzip = true;
685            rawRequestHeaders.add("Accept-Encoding", "gzip");
686        }
687
688        if (hasRequestBody()) {
689            rawRequestHeaders.addIfAbsent("Content-Type", "application/x-www-form-urlencoded");
690        }
691
692        long ifModifiedSince = policy.getIfModifiedSince();
693        if (ifModifiedSince != 0) {
694            rawRequestHeaders.add("If-Modified-Since", HttpDate.format(new Date(ifModifiedSince)));
695        }
696
697        CookieHandler cookieHandler = CookieHandler.getDefault();
698        if (cookieHandler != null) {
699            Map<String, List<String>> allCookieHeaders
700                    = cookieHandler.get(uri, rawRequestHeaders.toMultimap());
701            for (Map.Entry<String, List<String>> entry : allCookieHeaders.entrySet()) {
702                String key = entry.getKey();
703                if ("Cookie".equalsIgnoreCase(key) || "Cookie2".equalsIgnoreCase(key)) {
704                    rawRequestHeaders.addAll(key, entry.getValue());
705                }
706            }
707        }
708    }
709
710    private String getRequestLine() {
711        String protocol = (httpMinorVersion == 0) ? "HTTP/1.0" : "HTTP/1.1";
712        return method + " " + requestString() + " " + protocol;
713    }
714
715    private String requestString() {
716        URL url = policy.getURL();
717        if (includeAuthorityInRequestLine()) {
718            return url.toString();
719        } else {
720            String fileOnly = url.getFile();
721            if (fileOnly == null || fileOnly.isEmpty()) {
722                fileOnly = "/";
723            }
724            return fileOnly;
725        }
726    }
727
728    /**
729     * Returns true if the request line should contain the full URL with host
730     * and port (like "GET http://android.com/foo HTTP/1.1") or only the path
731     * (like "GET /foo HTTP/1.1").
732     *
733     * <p>This is non-final because for HTTPS it's never necessary to supply the
734     * full URL, even if a proxy is in use.
735     */
736    protected boolean includeAuthorityInRequestLine() {
737        return policy.usingProxy();
738    }
739
740    protected final String getDefaultUserAgent() {
741        String agent = System.getProperty("http.agent");
742        return agent != null ? agent : ("Java" + System.getProperty("java.version"));
743    }
744
745    private boolean hasConnectionCloseHeaders() {
746        return (rawResponseHeaders != null
747                && "close".equalsIgnoreCase(rawResponseHeaders.get("Connection")))
748                || ("close".equalsIgnoreCase(rawRequestHeaders.get("Connection")));
749    }
750
751    protected final String getOriginAddress(URL url) {
752        int port = url.getPort();
753        String result = url.getHost();
754        if (port > 0 && port != policy.getDefaultPort()) {
755            result = result + ":" + port;
756        }
757        return result;
758    }
759
760    protected boolean requiresTunnel() {
761        return false;
762    }
763
764    /**
765     * Flushes the remaining request header and body, parses the HTTP response
766     * headers and starts reading the HTTP response body if it exists.
767     */
768    public final void readResponse() throws IOException {
769        if (hasResponse()) {
770            return;
771        }
772
773        if (responseSource == null) {
774            throw new IllegalStateException("readResponse() without sendRequest()");
775        }
776
777        if (!responseSource.requiresConnection()) {
778            return;
779        }
780
781        if (sentRequestMillis == -1) {
782            int contentLength = requestBodyOut instanceof RetryableOutputStream
783                    ? ((RetryableOutputStream) requestBodyOut).contentLength()
784                    : -1;
785            writeRequestHeaders(contentLength);
786        }
787
788        if (requestBodyOut != null) {
789            requestBodyOut.close();
790            if (requestBodyOut instanceof RetryableOutputStream) {
791                ((RetryableOutputStream) requestBodyOut).writeToSocket(requestOut);
792            }
793        }
794
795        requestOut.flush();
796        requestOut = socketOut;
797
798        readResponseHeaders();
799        rawResponseHeaders.add(ResponseHeaders.SENT_MILLIS, Long.toString(sentRequestMillis));
800        rawResponseHeaders.add(ResponseHeaders.RECEIVED_MILLIS,
801                Long.toString(System.currentTimeMillis()));
802
803        if (responseSource == ResponseSource.CONDITIONAL_CACHE) {
804            if (cachedResponseHeaders.validate(new ResponseHeaders(uri, rawResponseHeaders))) {
805                // discard the network response
806                release(true);
807
808                // use the cache response
809                setResponse(cachedResponseHeaders.headers, cachedResponseBody);
810                return;
811            } else {
812                IoUtils.closeQuietly(cachedResponseBody);
813            }
814        }
815
816        if (hasResponseBody()) {
817            maybeCache(); // reentrant. this calls into user code which may call back into this!
818        }
819
820        initContentStream(getTransferStream());
821    }
822}
823