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 package okhttp3.internal.cache;
18
19 import java.io.IOException;
20 import javax.annotation.Nullable;
21 import okhttp3.Headers;
22 import okhttp3.Interceptor;
23 import okhttp3.Protocol;
24 import okhttp3.Request;
25 import okhttp3.Response;
26 import okhttp3.internal.Internal;
27 import okhttp3.internal.Util;
28 import okhttp3.internal.http.ExchangeCodec;
29 import okhttp3.internal.http.HttpHeaders;
30 import okhttp3.internal.http.HttpMethod;
31 import okhttp3.internal.http.RealResponseBody;
32 import okio.Buffer;
33 import okio.BufferedSink;
34 import okio.BufferedSource;
35 import okio.Okio;
36 import okio.Sink;
37 import okio.Source;
38 import okio.Timeout;
39
40 import static java.net.HttpURLConnection.HTTP_NOT_MODIFIED;
41 import static java.util.concurrent.TimeUnit.MILLISECONDS;
42 import static okhttp3.internal.Util.closeQuietly;
43 import static okhttp3.internal.Util.discard;
44
45 /** Serves requests from the cache and writes responses to the cache. */
46 public final class CacheInterceptor implements Interceptor {
47   final @Nullable InternalCache cache;
48
49   public CacheInterceptor(@Nullable InternalCache cache) {
50     this.cache = cache;
51   }
52
53   @Override public Response intercept(Chain chain) throws IOException {
54     Response cacheCandidate = cache != null
55         ? cache.get(chain.request())
56         : null;
57
58     long now = System.currentTimeMillis();
59
60     CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
61     Request networkRequest = strategy.networkRequest;
62     Response cacheResponse = strategy.cacheResponse;
63
64     if (cache != null) {
65       cache.trackResponse(strategy);
66     }
67
68     if (cacheCandidate != null && cacheResponse == null) {
69       closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
70     }
71
72     // If we're forbidden from using the network and the cache is insufficient, fail.
73     if (networkRequest == null && cacheResponse == null) {
74       return new Response.Builder()
75           .request(chain.request())
76           .protocol(Protocol.HTTP_1_1)
77           .code(504)
78           .message("Unsatisfiable Request (only-if-cached)")
79           .body(Util.EMPTY_RESPONSE)
80           .sentRequestAtMillis(-1L)
81           .receivedResponseAtMillis(System.currentTimeMillis())
82           .build();
83     }
84
85     // If we don't need the network, we're done.
86     if (networkRequest == null) {
87       return cacheResponse.newBuilder()
88           .cacheResponse(stripBody(cacheResponse))
89           .build();
90     }
91
92     Response networkResponse = null;
93     try {
94       networkResponse = chain.proceed(networkRequest);
95     } finally {
96       // If we're crashing on I/O or otherwise, don't leak the cache body.
97       if (networkResponse == null && cacheCandidate != null) {
98         closeQuietly(cacheCandidate.body());
99       }
100     }
101
102     // If we have a cache response too, then we're doing a conditional get.
103     if (cacheResponse != null) {
104       if (networkResponse.code() == HTTP_NOT_MODIFIED) {
105         Response response = cacheResponse.newBuilder()
106             .headers(combine(cacheResponse.headers(), networkResponse.headers()))
107             .sentRequestAtMillis(networkResponse.sentRequestAtMillis())
108             .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
109             .cacheResponse(stripBody(cacheResponse))
110             .networkResponse(stripBody(networkResponse))
111             .build();
112         networkResponse.body().close();
113
114         // Update the cache after combining headers but before stripping the
115         // Content-Encoding header (as performed by initContentStream()).
116         cache.trackConditionalCacheHit();
117         cache.update(cacheResponse, response);
118         return response;
119       } else {
120         closeQuietly(cacheResponse.body());
121       }
122     }
123
124     Response response = networkResponse.newBuilder()
125         .cacheResponse(stripBody(cacheResponse))
126         .networkResponse(stripBody(networkResponse))
127         .build();
128
129     if (cache != null) {
130       if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
131         // Offer this request to the cache.
132         CacheRequest cacheRequest = cache.put(response);
133         return cacheWritingResponse(cacheRequest, response);
134       }
135
136       if (HttpMethod.invalidatesCache(networkRequest.method())) {
137         try {
138           cache.remove(networkRequest);
139         } catch (IOException ignored) {
140           // The cache cannot be written.
141         }
142       }
143     }
144
145     return response;
146   }
147
148   private static Response stripBody(Response response) {
149     return response != null && response.body() != null
150         ? response.newBuilder().body(null).build()
151         : response;
152   }
153
154   /**
155    * Returns a new source that writes bytes to {@code cacheRequest} as they are read by the source
156    * consumer. This is careful to discard bytes left over when the stream is closed; otherwise we
157    * may never exhaust the source stream and therefore not complete the cached response.
158    */

159   private Response cacheWritingResponse(final CacheRequest cacheRequest, Response response)
160       throws IOException {
161     // Some apps return a null body; for compatibility we treat that like a null cache request.
162     if (cacheRequest == nullreturn response;
163     Sink cacheBodyUnbuffered = cacheRequest.body();
164     if (cacheBodyUnbuffered == nullreturn response;
165
166     final BufferedSource source = response.body().source();
167     final BufferedSink cacheBody = Okio.buffer(cacheBodyUnbuffered);
168
169     Source cacheWritingSource = new Source() {
170       boolean cacheRequestClosed;
171
172       @Override public long read(Buffer sink, long byteCount) throws IOException {
173         long bytesRead;
174         try {
175           bytesRead = source.read(sink, byteCount);
176         } catch (IOException e) {
177           if (!cacheRequestClosed) {
178             cacheRequestClosed = true;
179             cacheRequest.abort(); // Failed to write a complete cache response.
180           }
181           throw e;
182         }
183
184         if (bytesRead == -1) {
185           if (!cacheRequestClosed) {
186             cacheRequestClosed = true;
187             cacheBody.close(); // The cache response is complete!
188           }
189           return -1;
190         }
191
192         sink.copyTo(cacheBody.buffer(), sink.size() - bytesRead, bytesRead);
193         cacheBody.emitCompleteSegments();
194         return bytesRead;
195       }
196
197       @Override public Timeout timeout() {
198         return source.timeout();
199       }
200
201       @Override public void close() throws IOException {
202         if (!cacheRequestClosed
203             && !discard(this, ExchangeCodec.DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS)) {
204           cacheRequestClosed = true;
205           cacheRequest.abort();
206         }
207         source.close();
208       }
209     };
210
211     String contentType = response.header("Content-Type");
212     long contentLength = response.body().contentLength();
213     return response.newBuilder()
214         .body(new RealResponseBody(contentType, contentLength, Okio.buffer(cacheWritingSource)))
215         .build();
216   }
217
218   /** Combines cached headers with a network headers as defined by RFC 7234, 4.3.4. */
219   private static Headers combine(Headers cachedHeaders, Headers networkHeaders) {
220     Headers.Builder result = new Headers.Builder();
221
222     for (int i = 0, size = cachedHeaders.size(); i < size; i++) {
223       String fieldName = cachedHeaders.name(i);
224       String value = cachedHeaders.value(i);
225       if ("Warning".equalsIgnoreCase(fieldName) && value.startsWith("1")) {
226         continue// Drop 100-level freshness warnings.
227       }
228       if (isContentSpecificHeader(fieldName)
229           || !isEndToEnd(fieldName)
230           || networkHeaders.get(fieldName) == null) {
231         Internal.instance.addLenient(result, fieldName, value);
232       }
233     }
234
235     for (int i = 0, size = networkHeaders.size(); i < size; i++) {
236       String fieldName = networkHeaders.name(i);
237       if (!isContentSpecificHeader(fieldName) && isEndToEnd(fieldName)) {
238         Internal.instance.addLenient(result, fieldName, networkHeaders.value(i));
239       }
240     }
241
242     return result.build();
243   }
244
245   /**
246    * Returns true if {@code fieldName} is an end-to-end HTTP header, as defined by RFC 2616,
247    * 13.5.1.
248    */

249   static boolean isEndToEnd(String fieldName) {
250     return !"Connection".equalsIgnoreCase(fieldName)
251         && !"Keep-Alive".equalsIgnoreCase(fieldName)
252         && !"Proxy-Authenticate".equalsIgnoreCase(fieldName)
253         && !"Proxy-Authorization".equalsIgnoreCase(fieldName)
254         && !"TE".equalsIgnoreCase(fieldName)
255         && !"Trailers".equalsIgnoreCase(fieldName)
256         && !"Transfer-Encoding".equalsIgnoreCase(fieldName)
257         && !"Upgrade".equalsIgnoreCase(fieldName);
258   }
259
260   /**
261    * Returns true if {@code fieldName} is content specific and therefore should always be used
262    * from cached headers.
263    */

264   static boolean isContentSpecificHeader(String fieldName) {
265     return "Content-Length".equalsIgnoreCase(fieldName)
266         || "Content-Encoding".equalsIgnoreCase(fieldName)
267         || "Content-Type".equalsIgnoreCase(fieldName);
268   }
269 }
270