blob: 54f26f15a15fa2a971c17e095ad011261b7fedcd [file] [log] [blame]
// Copyright 2011 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Author: bmcquade@google.com (Bryan McQuade)
// Author: sligocki@google.com (Shawn Ligocki)
#include "pagespeed/kernel/http/caching_headers.h"
#include <vector>
#include "base/logging.h"
#include "pagespeed/kernel/base/basictypes.h"
#include "pagespeed/kernel/base/stl_util.h"
#include "pagespeed/kernel/base/string_util.h"
#include "pagespeed/kernel/base/time_util.h"
#include "pagespeed/kernel/http/http_names.h"
namespace net_instaweb {
CachingHeaders::CachingHeaders(int status_code)
: status_code_(status_code),
parsed_cache_control_(false),
public_(false),
private_(false),
no_transform_(false),
must_revalidate_(false),
proxy_revalidate_(false),
no_cache_(false),
no_store_(false),
cache_control_parse_error_(false),
expires_invalid_(false) {
}
CachingHeaders::~CachingHeaders() {
}
// Lazy getters
bool CachingHeaders::IsCacheable() {
if (!is_cacheable_.has_value()) {
is_cacheable_.set_value(ComputeIsCacheable());
}
return is_cacheable_.value();
}
bool CachingHeaders::MustRevalidate() {
if (!is_cacheable_.has_value()) {
is_cacheable_.set_value(ComputeIsCacheable());
}
return must_revalidate_;
}
bool CachingHeaders::ProxyRevalidate() {
if (!is_cacheable_.has_value()) {
is_cacheable_.set_value(ComputeIsCacheable());
}
return proxy_revalidate_;
}
bool CachingHeaders::IsProxyCacheable() {
if (!is_proxy_cacheable_.has_value()) {
is_proxy_cacheable_.set_value(ComputeIsProxyCacheable());
}
return is_proxy_cacheable_.value();
}
bool CachingHeaders::IsHeuristicallyCacheable() {
if (!is_heuristically_cacheable_.has_value()) {
is_heuristically_cacheable_.set_value(ComputeIsHeuristicallyCacheable());
}
return is_heuristically_cacheable_.value();
}
bool CachingHeaders::GetFreshnessLifetimeMillis(
int64* out_freshness_lifetime_millis) {
ParseCacheControlIfNecessary();
DCHECK_EQ(is_explicitly_cacheable_.has_value(),
freshness_lifetime_millis_.has_value());
if (!is_explicitly_cacheable_.has_value() ||
!freshness_lifetime_millis_.has_value()) {
int64 freshness_lifetime_millis = 0;
is_explicitly_cacheable_.set_value(
ComputeFreshnessLifetimeMillis(&freshness_lifetime_millis));
freshness_lifetime_millis_.set_value(freshness_lifetime_millis);
}
*out_freshness_lifetime_millis = freshness_lifetime_millis_.value();
return is_explicitly_cacheable_.value();
}
bool CachingHeaders::HasExplicitNoCacheDirective() {
if (!has_explicit_no_cache_directive_.has_value()) {
has_explicit_no_cache_directive_.set_value(
ComputeHasExplicitNoCacheDirective());
}
return has_explicit_no_cache_directive_.value();
}
// Simple wrapper functions
bool CachingHeaders::IsExplicitlyCacheable() {
int64 freshness_lifetime = 0;
return (GetFreshnessLifetimeMillis(&freshness_lifetime) &&
(freshness_lifetime > 0));
}
bool CachingHeaders::HasExplicitFreshnessLifetime() {
int64 freshness_lifetime = 0;
return GetFreshnessLifetimeMillis(&freshness_lifetime);
}
bool CachingHeaders::IsRedirectStatusCode() const {
return status_code_ == 301 ||
status_code_ == 302 ||
status_code_ == 303 ||
status_code_ == 307;
}
// Actual compute logic
bool CachingHeaders::ComputeIsCacheable() {
int64 freshness_lifetime = 0;
if (GetFreshnessLifetimeMillis(&freshness_lifetime)) {
if (freshness_lifetime <= 0) {
// The resource is explicitly not fresh, so we don't consider it
// to be a static resource.
return false;
}
// If there's an explicit freshness lifetime and it's greater than
// zero, then the resource is cacheable.
return true;
}
// If we've made it this far, we've got a resource that doesn't have
// explicit caching headers. At this point we use heuristics
// specified in the HTTP RFC and implemented in many
// browsers/proxies to determine if this resource is typically
// cached.
return IsHeuristicallyCacheable();
}
bool CachingHeaders::ComputeIsProxyCacheable() {
return IsCacheable() && !private_;
}
bool CachingHeaders::ComputeIsHeuristicallyCacheable() {
if (HasExplicitFreshnessLifetime()) {
// If the response has an explicit freshness lifetime then it's
// not heuristically cacheable. This method only expects to be
// called if the resource does *not* have an explicit freshness
// lifetime.
LOG(DFATAL) << "IsHeuristicallyCacheable received a resource with "
<< "explicit freshness lifetime.";
return false;
}
if (must_revalidate_) {
// must-revalidate indicates that a non-fresh response should not
// be used in response to requests without validating at the
// origin. Such a resource is not heuristically cacheable.
return false;
}
if (url_.find_first_of('?') != StringPiece::npos) {
// The HTTP RFC says:
//
// ...since some applications have traditionally used GETs and
// HEADs with query URLs (those containing a "?" in the rel_path
// part) to perform operations with significant side effects,
// caches MUST NOT treat responses to such URIs as fresh unless
// the server provides an explicit expiration time.
//
// So if we find a '?' in the URL, the resource is not
// heuristically cacheable.
//
// In practice most browsers do not implement this policy. For
// instance, Chrome and IE8 do not look for the query string,
// while Firefox (as of version 3.6) does. For the time being we
// implement the RFC but it might make sense to revisit this
// decision in the future, given that major browser
// implementations do not match.
return false;
}
if (!IsCacheableResourceStatusCode()) {
return false;
}
if (!IsLikelyStaticResourceType()) {
return false;
}
return true;
}
bool CachingHeaders::ComputeFreshnessLifetimeMillis(
int64* out_freshness_lifetime_ms) {
ParseCacheControlIfNecessary();
// Initialize the output param to the default value. We do this in
// case clients use the out value without checking the return value
// of the function.
*out_freshness_lifetime_ms = 0;
if (HasExplicitNoCacheDirective()) {
// If there's an explicit no cache directive then the resource is
// never fresh.
return true;
}
// First, look for Cache-Control: max-age. The HTTP/1.1 RFC
// indicates that CC: max-age takes precedence to Expires.
if (max_age_seconds_.has_value()) {
*out_freshness_lifetime_ms = 1000LL * max_age_seconds_.value();
return true;
}
// Next look for Expires.
if (!expires_ms_.has_value()) {
// If there's no expires header and we previously determined there
// was no Cache-Control: max-age, then the resource doesn't have
// an explicit freshness lifetime.
return false;
}
// We've determined that there is an Expires header. Thus, the
// resource has a freshness lifetime. Even if the Expires header
// doesn't contain a valid date, it should be considered stale. From
// HTTP/1.1 RFC 14.21: "HTTP/1.1 clients and caches MUST treat other
// invalid date formats, especially including the value "0", as in
// the past (i.e., "already expired")."
int64 date_value_ms = 0;
StringPieceVector date;
if (!Lookup(HttpAttributes::kDate, &date) ||
(date.size() != 1) ||
!ConvertStringToTime(date[0], &date_value_ms)) {
// We have an Expires header, but no Date header to reference
// from. Thus we assume that the resource is heuristically
// cacheable, but not explicitly cacheable.
return false;
}
if (expires_invalid_) {
// If we can't parse the Expires header, then treat the resource as
// stale.
return true;
}
int64 freshness_lifetime_ms = expires_ms_.value() - date_value_ms;
if (freshness_lifetime_ms < 0) {
freshness_lifetime_ms = 0;
}
*out_freshness_lifetime_ms = freshness_lifetime_ms;
return true;
}
void CachingHeaders::ParseCacheControlIfNecessary() {
if (!parsed_cache_control_) {
parsed_cache_control_ = true;
StringPieceVector values;
if (Lookup(HttpAttributes::kCacheControl, &values)) {
for (int i = 0, n = values.size(); i < n; ++i) {
StringPiece value = values[i];
if (value == "public") {
public_ = true;
} else if (value.starts_with("private")) {
// See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html :
// "private" [ "=" <"> 1#field-name <"> ] ; Section 14.9.1
// So we must use 'starts_with' rather than test for equality.
private_ = true;
} else if (value.starts_with("no-cache")) {
// "no-cache" [ "=" <"> 1#field-name <"> ]; Section 14.9.1
no_cache_ = true;
} else if (value == "no-store") {
no_store_ = true;
} else if (value.starts_with("max-age=")) {
int max_age_value = 0;
StringPiece max_age_piece = value.substr(STATIC_STRLEN("max-age="));
if (StringToInt(max_age_piece, &max_age_value)) {
max_age_seconds_.set_value(max_age_value);
} else {
cache_control_parse_error_ = true;
}
} else if (value == "must-revalidate") {
must_revalidate_ = true;
} else if (value == "proxy-revalidate") {
proxy_revalidate_ = true;
}
}
}
StringPieceVector expires;
int64 expires_value_ms;
if (Lookup(HttpAttributes::kExpires, &expires)) {
if (!expires.empty() &&
ConvertStringToTime(expires[0], &expires_value_ms)) {
expires_ms_.set_value(expires_value_ms);
} else {
expires_invalid_ = true;
}
}
}
}
bool CachingHeaders::ComputeHasExplicitNoCacheDirective() {
ParseCacheControlIfNecessary();
if (no_cache_ || no_store_ ||
(max_age_seconds_.has_value() && max_age_seconds_.value() <= 0)) {
return true;
}
if (expires_invalid_) {
// An invalid Expires header (e.g. Expires: 0) means do not cache.
return true;
}
StringPieceVector pragma, vary;
if (Lookup(HttpAttributes::kPragma, &pragma) && STLFind(pragma, "no-cache")) {
return true;
}
if (Lookup(HttpAttributes::kVary, &vary) &&
STLFind(vary, "*")) {
return true;
}
return false;
}
GoogleString CachingHeaders::GenerateDisabledCacheControl() {
GoogleString new_cache_control(HttpAttributes::kNoCacheMaxAge0);
StringPieceVector pieces, name_value;
if (Lookup(HttpAttributes::kCacheControl, &pieces)) {
for (int i = 0, n = pieces.size(); i < n; ++i) {
name_value.clear();
SplitStringPieceToVector(pieces[i], "=", &name_value, true);
if (!name_value.empty()) {
StringPiece name = name_value[0];
TrimWhitespace(&name);
// See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.1
if (!StringCaseEqual(name, HttpAttributes::kNoCache) &&
!StringCaseEqual(name, HttpAttributes::kMaxAge) &&
!StringCaseEqual(name, HttpAttributes::kPrivate) &&
!StringCaseEqual(name, HttpAttributes::kPublic)) {
StrAppend(&new_cache_control, ", ", pieces[i]);
}
}
}
}
return new_cache_control;
}
} // namespace net_instaweb