1 /*
2 * Copyright (c) 2024 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16 #include "net_http_cache_strategy.h"
17
18 #include <algorithm>
19 #include <cstring>
20
21 #include "constant.h"
22 #include "net_http_utils.h"
23 #include "netstack_log.h"
24
25 static constexpr int64_t ONE_DAY_MILLISECONDS = 24 * 60 * 60 * 1000L;
26 static constexpr int64_t CONVERT_TO_MILLISECONDS = 1000;
27 static constexpr const char *KEY_RANGE = "range";
28
29 // RFC 7234
30
31 namespace OHOS::NetStack::Http {
HttpCacheStrategy(HttpRequest & requestOptions)32 HttpCacheStrategy::HttpCacheStrategy(HttpRequest &requestOptions) : requestOptions_(requestOptions)
33 {
34 cacheRequest_.ParseRequestHeader(requestOptions_.GetHeader());
35 cacheRequest_.SetRequestTime(requestOptions_.GetRequestTime());
36 }
37
CouldUseCache()38 bool HttpCacheStrategy::CouldUseCache()
39 {
40 return requestOptions_.GetMethod() == HTTP_METHOD_GET ||
41 requestOptions_.GetMethod() == HTTP_METHOD_HEAD ||
42 requestOptions_.GetHeader().find(KEY_RANGE) != requestOptions_.GetHeader().end();
43 }
44
RunStrategy(HttpResponse & response)45 CacheStatus HttpCacheStrategy::RunStrategy(HttpResponse &response)
46 {
47 cacheResponse_.ParseCacheResponseHeader(response.GetHeader());
48 cacheResponse_.SetRespCode(static_cast<ResponseCode>(response.GetResponseCode()));
49 cacheResponse_.SetResponseTime(response.GetResponseTime());
50 cacheResponse_.SetRequestTime(response.GetRequestTime());
51 return RunStrategyInternal(response);
52 }
53
IsCacheable(const HttpResponse & response)54 bool HttpCacheStrategy::IsCacheable(const HttpResponse &response)
55 {
56 if (!CouldUseCache()) {
57 return false;
58 }
59 HttpCacheResponse tempCacheResponse;
60 tempCacheResponse.ParseCacheResponseHeader(response.GetHeader());
61 tempCacheResponse.SetRespCode(static_cast<ResponseCode>(response.GetResponseCode()));
62 return IsCacheable(tempCacheResponse);
63 }
64
CacheResponseAgeMillis()65 int64_t HttpCacheStrategy::CacheResponseAgeMillis()
66 {
67 // 4.2.3. Calculating Age
68
69 /* The following data is used for the age calculation:
70
71 age_value
72 The term "age_value" denotes the value of the Age header field (Section 5.1), in a form appropriate for arithmetic
73 operation; or 0, if not available.
74
75 date_value
76 The term "date_value" denotes the value of the Date header field, in a form appropriate for arithmetic operations.
77 See Section 7.1.1.2 of [RFC7231] for the definition of the Date header field, and for requirements regarding
78 responses without it.
79
80 now
81 The term "now" means "the current value of the clock at the host performing the calculation". A host ought to use
82 NTP ([RFC5905]) or some similar protocol to synchronize its clocks to Coordinated Universal Time.
83
84 request_time
85 The current value of the clock at the host at the time the request resulting in the stored response was made.
86
87 response_time
88 The current value of the clock at the host at the time the response was received.
89 A response's age can be calculated in two entirely independent ways:
90
91 the "apparent_age": response_time minus date_value,
92 if the local clock is reasonably well synchronized to the origin
93 server's clock. If the result is negative, the result is replaced by zero. the "corrected_age_value", if all of the
94 caches along the response path implement HTTP/1.1. A cache MUST interpret this value relative to the time the
95 request was initiated, not the time that the response was received. apparent_age = max(0, response_time -
96 date_value);
97
98 response_delay = response_time - request_time;
99 corrected_age_value = age_value + response_delay;
100 These are combined as
101
102 corrected_initial_age = max(apparent_age, corrected_age_value);
103 unless the cache is confident in the value of the Age header field (e.g., because there are no HTTP/1.0 hops in the
104 Via header field), in which case the corrected_age_value MAY be used as the corrected_initial_age.
105
106 The current_age of a stored response can then be calculated by adding the amount of time (in seconds) since the
107 stored response was last validated by the origin server to the corrected_initial_age.
108
109 resident_time = now - response_time;
110 current_age = corrected_initial_age + resident_time; */
111
112 int64_t age = std::max<int64_t>(0, cacheResponse_.GetAgeSeconds());
113 int64_t dateTime = std::max<int64_t>(0, cacheResponse_.GetDate());
114 int64_t nowTime = std::max<int64_t>(0, GetNowTimeSeconds());
115 int64_t requestTime = std::max<int64_t>(0, cacheResponse_.GetRequestTime());
116 int64_t responseTime = std::max<int64_t>(0, cacheResponse_.GetResponseTime());
117
118 NETSTACK_LOGI("CacheResponseAgeMillis: %{public}lld, %{public}lld, %{public}lld, %{public}lld, %{public}lld",
119 static_cast<long long>(age), static_cast<long long>(dateTime), static_cast<long long>(nowTime),
120 static_cast<long long>(requestTime), static_cast<long long>(responseTime));
121
122 int64_t apparentAge = std::max<int64_t>(0, responseTime - dateTime);
123 int64_t responseDelay = std::max<int64_t>(0, responseTime - requestTime);
124 int64_t correctedAgeValue = age + responseDelay;
125 int64_t correctedInitialAge = std::max<int64_t>(apparentAge, correctedAgeValue);
126
127 int64_t residentTime = std::max<int64_t>(0, nowTime - responseTime);
128
129 return (correctedInitialAge + residentTime) * CONVERT_TO_MILLISECONDS;
130 }
131
ComputeFreshnessLifetimeSecondsInternal()132 int64_t HttpCacheStrategy::ComputeFreshnessLifetimeSecondsInternal()
133 {
134 int64_t sMaxAge = cacheResponse_.GetSMaxAgeSeconds();
135 if (sMaxAge != INVALID_TIME) {
136 // If the cache is shared and the s-maxage response directive (Section 5.2.2.9) is present, use its value
137 return sMaxAge;
138 }
139
140 int64_t maxAge = cacheResponse_.GetMaxAgeSeconds();
141 if (maxAge != INVALID_TIME) {
142 // If the max-age response directive (Section 5.2.2.8) is present, use its value
143 return maxAge;
144 }
145
146 if (cacheResponse_.GetExpires() != INVALID_TIME) {
147 // If the Expires response header field (Section 5.3) is present, use its value minus the value of the Date
148 // response header field
149 int64_t responseTime = cacheResponse_.GetResponseTime();
150 if (cacheResponse_.GetDate() != INVALID_TIME) {
151 responseTime = cacheResponse_.GetDate();
152 }
153 int64_t delta = cacheResponse_.GetExpires() - responseTime;
154 return std::max<int64_t>(delta, 0);
155 }
156
157 if (cacheResponse_.GetLastModified() != INVALID_TIME) {
158 // 4.2.2. Calculating Heuristic Freshness
159 int64_t requestTime = cacheRequest_.GetRequestTime();
160 if (cacheResponse_.GetDate() != INVALID_TIME) {
161 requestTime = cacheResponse_.GetDate();
162 }
163 int64_t delta = requestTime - cacheResponse_.GetLastModified();
164 return std::max<int64_t>(delta / DECIMAL, 0);
165 }
166
167 return 0;
168 }
169
ComputeFreshnessLifetimeMillis()170 int64_t HttpCacheStrategy::ComputeFreshnessLifetimeMillis()
171 {
172 int64_t lifeTime = ComputeFreshnessLifetimeSecondsInternal();
173
174 int64_t reqMaxAge = cacheRequest_.GetMaxAgeSeconds();
175 if (reqMaxAge != INVALID_TIME) {
176 lifeTime = std::min(lifeTime, reqMaxAge);
177 }
178
179 NETSTACK_LOGI("lifeTime=%{public}lld", static_cast<long long>(lifeTime));
180 return lifeTime * CONVERT_TO_MILLISECONDS;
181 }
182
UpdateRequestHeader(const std::string & etag,const std::string & lastModified,const std::string & date)183 void HttpCacheStrategy::UpdateRequestHeader(const std::string &etag,
184 const std::string &lastModified,
185 const std::string &date)
186 {
187 if (!etag.empty()) {
188 requestOptions_.SetHeader(IF_NONE_MATCH, etag);
189 } else if (!lastModified.empty()) {
190 requestOptions_.SetHeader(IF_MODIFIED_SINCE, lastModified);
191 } else if (!date.empty()) {
192 requestOptions_.SetHeader(IF_MODIFIED_SINCE, date);
193 }
194 }
195
IsCacheable(const HttpCacheResponse & cacheResponse)196 bool HttpCacheStrategy::IsCacheable(const HttpCacheResponse &cacheResponse)
197 {
198 switch (cacheResponse.GetRespCode()) {
199 case ResponseCode::OK:
200 case ResponseCode::NOT_AUTHORITATIVE:
201 case ResponseCode::NO_CONTENT:
202 case ResponseCode::MULT_CHOICE:
203 case ResponseCode::MOVED_PERM:
204 case ResponseCode::NOT_FOUND:
205 case ResponseCode::BAD_METHOD:
206 case ResponseCode::GONE:
207 case ResponseCode::REQ_TOO_LONG:
208 case ResponseCode::NOT_IMPLEMENTED:
209 // These codes can be cached unless headers forbid it.
210 break;
211
212 case ResponseCode::MOVED_TEMP:
213 if (cacheResponse.GetExpires() != INVALID_TIME || cacheResponse.GetMaxAgeSeconds() != INVALID_TIME ||
214 cacheResponse.IsPublicCache() || cacheResponse.IsPrivateCache()) {
215 break;
216 }
217 return false;
218
219 default:
220 return false;
221 }
222
223 return !cacheResponse.IsNoStore() && !cacheRequest_.IsNoStore();
224 }
225
GetFreshness()226 std::tuple<int64_t, int64_t, int64_t, int64_t> HttpCacheStrategy::GetFreshness()
227 {
228 int64_t ageMillis = CacheResponseAgeMillis();
229
230 int64_t lifeTime = ComputeFreshnessLifetimeMillis();
231
232 int64_t minFreshMillis = std::max<int64_t>(0, cacheRequest_.GetMinFreshSeconds() * CONVERT_TO_MILLISECONDS);
233
234 int64_t maxStaleMillis = 0;
235 if (!cacheResponse_.IsMustRevalidate()) {
236 maxStaleMillis = std::max<int64_t>(0, cacheRequest_.GetMaxStaleSeconds() * CONVERT_TO_MILLISECONDS);
237 }
238
239 NETSTACK_LOGI("GetFreshness: %{public}lld, %{public}lld, %{public}lld, %{public}lld",
240 static_cast<long long>(ageMillis), static_cast<long long>(minFreshMillis),
241 static_cast<long long>(lifeTime), static_cast<long long>(maxStaleMillis));
242 return {ageMillis, minFreshMillis, lifeTime, maxStaleMillis};
243 }
244
RunStrategyInternal(HttpResponse & response)245 CacheStatus HttpCacheStrategy::RunStrategyInternal(HttpResponse &response)
246 {
247 if (cacheRequest_.IsNoCache()) {
248 NETSTACK_LOGI("return DENY");
249 return DENY;
250 }
251
252 if (cacheResponse_.IsNoCache()) {
253 NETSTACK_LOGI("return STALE");
254 return STALE;
255 }
256
257 if (cacheRequest_.IsOnlyIfCached()) {
258 NETSTACK_LOGI("return FRESH");
259 return FRESH;
260 }
261
262 // T.B.D https and TLS handshake
263 if (!IsCacheable(cacheResponse_)) {
264 NETSTACK_LOGI("return DENY");
265 return DENY;
266 }
267
268 if (cacheRequest_.GetIfModifiedSince() != INVALID_TIME || !cacheRequest_.GetIfNoneMatch().empty()) {
269 NETSTACK_LOGI("return DENY");
270 return DENY;
271 }
272
273 auto [ageMillis, minFreshMillis, lifeTime, maxStaleMillis] = GetFreshness();
274
275 if (ageMillis + minFreshMillis < lifeTime + maxStaleMillis) {
276 if (ageMillis + minFreshMillis >= lifeTime) {
277 response.SetWarning("110 \"Response is STALE\"");
278 }
279
280 if (ageMillis > ONE_DAY_MILLISECONDS && cacheRequest_.GetMaxAgeSeconds() == INVALID_TIME &&
281 cacheResponse_.GetExpires() == INVALID_TIME) {
282 response.SetWarning("113 \"Heuristic expiration\"");
283 }
284
285 NETSTACK_LOGI("return FRESH");
286 return FRESH;
287 }
288
289 // The cache has expired and the request needs to be re-initialized
290 UpdateRequestHeader(cacheResponse_.GetEtag(), cacheResponse_.GetLastModifiedStr(), cacheResponse_.GetDateStr());
291
292 NETSTACK_LOGI("return STALE");
293 return STALE;
294 }
295 } // namespace OHOS::NetStack::Http
296