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