| /* |
| * Copyright 2011 Google Inc. |
| * |
| * 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: jmarantz@google.com (Joshua Marantz) |
| // sligocki@google.com (Shawn Ligocki) |
| |
| #include "net/instaweb/http/public/async_fetch.h" |
| |
| #include "base/logging.h" |
| #include "net/instaweb/http/public/http_cache.h" |
| #include "net/instaweb/http/public/request_timing_info.h" |
| #include "pagespeed/kernel/base/ref_counted_ptr.h" |
| #include "pagespeed/kernel/base/statistics.h" |
| #include "pagespeed/kernel/http/http_names.h" |
| #include "pagespeed/kernel/http/request_headers.h" |
| #include "pagespeed/kernel/http/response_headers.h" |
| |
| namespace net_instaweb { |
| |
| AsyncFetch::AsyncFetch() |
| : request_headers_(NULL), |
| response_headers_(NULL), |
| extra_response_headers_(NULL), |
| request_ctx_(NULL), |
| owns_request_headers_(false), |
| owns_response_headers_(false), |
| owns_extra_response_headers_(false), |
| headers_complete_(false), |
| content_length_(kContentLengthUnknown) { |
| } |
| |
| AsyncFetch::AsyncFetch(const RequestContextPtr& request_ctx) |
| : request_headers_(NULL), |
| response_headers_(NULL), |
| extra_response_headers_(NULL), |
| request_ctx_(request_ctx), |
| owns_request_headers_(false), |
| owns_response_headers_(false), |
| owns_extra_response_headers_(false), |
| headers_complete_(false), |
| content_length_(kContentLengthUnknown) { |
| DCHECK(request_ctx_.get() != NULL); |
| } |
| |
| AsyncFetch::~AsyncFetch() { |
| if (owns_response_headers_) { |
| delete response_headers_; |
| } |
| if (owns_extra_response_headers_) { |
| delete extra_response_headers_; |
| } |
| if (owns_request_headers_) { |
| delete request_headers_; |
| } |
| } |
| |
| AbstractLogRecord* AsyncFetch::log_record() { |
| CHECK(request_context().get() != NULL); |
| return request_context()->log_record(); |
| } |
| |
| bool AsyncFetch::Write(const StringPiece& sp, MessageHandler* handler) { |
| bool ret = true; |
| if (!sp.empty()) { // empty-writes should be no-ops. |
| if (!headers_complete_) { |
| HeadersComplete(); |
| } |
| if (request_headers()->method() == RequestHeaders::kHead) { |
| // If the request is a head request, then don't write the contents of |
| // body. |
| return ret; |
| } |
| ret = HandleWrite(sp, handler); |
| } |
| return ret; |
| } |
| |
| bool AsyncFetch::Flush(MessageHandler* handler) { |
| if (!headers_complete_) { |
| HeadersComplete(); |
| } |
| return HandleFlush(handler); |
| } |
| |
| void AsyncFetch::HeadersComplete() { |
| DCHECK_NE(0, response_headers()->status_code()); |
| if (headers_complete_) { |
| LOG(DFATAL) << "AsyncFetch::HeadersComplete() called twice."; |
| } else { |
| headers_complete_ = true; |
| HandleHeadersComplete(); |
| } |
| } |
| |
| void AsyncFetch::Done(bool success) { |
| if (!headers_complete_) { |
| if (!success && |
| (response_headers()->status_code() == 0)) { |
| // Failing fetches might not set status codes but we expect |
| // successful ones to. |
| response_headers()->set_status_code(HttpStatus::kNotFound); |
| } |
| response_headers()->ComputeCaching(); |
| HeadersComplete(); |
| } |
| HandleDone(success); |
| } |
| |
| // Sets the request-headers to the specified pointer. The caller must |
| // guarantee that the pointed-to headers remain valid as long as the |
| // AsyncFetch is running. |
| void AsyncFetch::set_request_headers(RequestHeaders* headers) { |
| DCHECK(!owns_request_headers_); |
| if (owns_request_headers_) { |
| delete request_headers_; |
| } |
| request_headers_ = headers; |
| owns_request_headers_ = false; |
| } |
| |
| void AsyncFetch::SetRequestHeadersTakingOwnership(RequestHeaders* headers) { |
| set_request_headers(headers); |
| owns_request_headers_ = true; |
| } |
| |
| RequestHeaders* AsyncFetch::request_headers() { |
| // TODO(jmarantz): Consider DCHECKing that only const reads occur |
| // after headers_complete_. |
| if (request_headers_ == NULL) { |
| request_headers_ = new RequestHeaders; |
| owns_request_headers_ = true; |
| } |
| return request_headers_; |
| } |
| |
| const RequestHeaders* AsyncFetch::request_headers() const { |
| CHECK(request_headers_ != NULL); |
| return request_headers_; |
| } |
| |
| ResponseHeaders* AsyncFetch::response_headers() { |
| if (response_headers_ == NULL) { |
| response_headers_ = new ResponseHeaders(request_ctx_->options()); |
| owns_response_headers_ = true; |
| } |
| return response_headers_; |
| } |
| |
| void AsyncFetch::set_response_headers(ResponseHeaders* headers) { |
| DCHECK(!owns_response_headers_); |
| if (owns_response_headers_) { |
| delete response_headers_; |
| } |
| response_headers_ = headers; |
| owns_response_headers_ = false; |
| } |
| |
| ResponseHeaders* AsyncFetch::extra_response_headers() { |
| if (extra_response_headers_ == NULL) { |
| extra_response_headers_ = new ResponseHeaders(request_ctx_->options()); |
| owns_extra_response_headers_ = true; |
| } |
| return extra_response_headers_; |
| } |
| |
| void AsyncFetch::set_extra_response_headers(ResponseHeaders* headers) { |
| DCHECK(!owns_extra_response_headers_); |
| if (owns_extra_response_headers_) { |
| delete extra_response_headers_; |
| } |
| extra_response_headers_ = headers; |
| owns_extra_response_headers_ = false; |
| } |
| |
| GoogleString AsyncFetch::LoggingString() { |
| GoogleString logging_info_str; |
| |
| if (NULL == request_ctx_.get()) { |
| return logging_info_str; |
| } |
| |
| int64 latency; |
| const RequestTimingInfo& timing_info = request_ctx_->timing_info(); |
| if (timing_info.GetHTTPCacheLatencyMs(&latency)) { |
| StrAppend(&logging_info_str, "c1:", Integer64ToString(latency), ";"); |
| } |
| if (timing_info.GetL2HTTPCacheLatencyMs(&latency)) { |
| StrAppend(&logging_info_str, "c2:", Integer64ToString(latency), ";"); |
| } |
| if (timing_info.GetFetchHeaderLatencyMs(&latency)) { |
| StrAppend(&logging_info_str, "hf:", Integer64ToString(latency), ";"); |
| } |
| if (timing_info.GetFetchLatencyMs(&latency)) { |
| StrAppend(&logging_info_str, "f:", Integer64ToString(latency), ";"); |
| } |
| return logging_info_str; |
| } |
| |
| StringAsyncFetch::~StringAsyncFetch() { |
| } |
| |
| bool AsyncFetchUsingWriter::HandleWrite(const StringPiece& sp, |
| MessageHandler* handler) { |
| return writer_->Write(sp, handler); |
| } |
| |
| bool AsyncFetchUsingWriter::HandleFlush(MessageHandler* handler) { |
| return writer_->Flush(handler); |
| } |
| |
| AsyncFetchUsingWriter::~AsyncFetchUsingWriter() { |
| } |
| |
| SharedAsyncFetch::SharedAsyncFetch(AsyncFetch* base_fetch) |
| : AsyncFetch(base_fetch->request_context()), |
| base_fetch_(base_fetch) { |
| set_response_headers(base_fetch->response_headers()); |
| set_extra_response_headers(base_fetch->extra_response_headers()); |
| set_request_headers(base_fetch->request_headers()); |
| } |
| |
| SharedAsyncFetch::~SharedAsyncFetch() { |
| } |
| |
| void SharedAsyncFetch::PropagateContentLength() { |
| if (content_length_known()) { |
| base_fetch_->set_content_length(content_length()); |
| } |
| } |
| |
| void SharedAsyncFetch::HandleHeadersComplete() { |
| PropagateContentLength(); |
| base_fetch_->HeadersComplete(); |
| } |
| |
| void AsyncFetch::FixCacheControlForGoogleCache() { |
| ConstStringStarVector values; |
| if (request_headers()->Lookup(HttpAttributes::kVia, &values)) { |
| for (int i = 0, n = values.size(); i < n; ++i) { |
| // See https://github.com/pagespeed/ngx_pagespeed/issues/1149 |
| // In general, it is not necessary to include a specific 'public' |
| // in a cache-control entry to allow a proxy-cache to cache it; |
| // it's sufficient to specify a max-age, as long as 'private' |
| // is not present. |
| // |
| // However, the Google Cloud CDN cache requires it, with some |
| // controversy. See this discussion with Mark Nottingham: |
| // https://groups.google.com/forum/#!searchin/pagespeed-insights-discuss/nottingham/pagespeed-insights-discuss/NWwrz1By36c/9RN9sHdj9EIJ |
| // |
| // Mark's comment about Firefox treating public differently is |
| // no longer current. |
| // |
| // However the Google Cloud CDN help page on caching unambiguously |
| // requires that 'public' be included in Cache-Control to enable |
| // the Google Cache. |
| // https://cloud.google.com/cdn/docs/caching#cacheability |
| // |
| // We only need to put in the 'public" header if there's |
| // a "Via: 1.1 google" header. As there's only a small cost |
| // in bytes, we'll just scan the via value for 'google', so |
| // that a future HTTP rev won't break caching. |
| if (IsGoogleCacheVia(*values[i])) { |
| response_headers()->SetCacheControlPublic(); |
| break; |
| } |
| } |
| } |
| } |
| |
| // static |
| bool AsyncFetch::IsGoogleCacheVia(StringPiece via_value) { |
| // The value of the Google Via header as of this writing |
| // is "1.1 google". In an attempt to be future proof, we'll |
| // match "X.Y google", and be case-insensitive matching "google". |
| StringPieceVector tokens; |
| SplitStringPieceToVector(via_value, " ", &tokens, true); |
| double version; |
| return |
| (tokens.size() == 2) && |
| StringCaseEqual(tokens[1], "google") && |
| StringToDouble(tokens[0], &version); |
| } |
| |
| const char FallbackSharedAsyncFetch::kStaleWarningHeaderValue[] = |
| "110 Response is stale"; |
| |
| FallbackSharedAsyncFetch::FallbackSharedAsyncFetch(AsyncFetch* base_fetch, |
| HTTPValue* fallback, |
| MessageHandler* handler) |
| : SharedAsyncFetch(base_fetch), |
| handler_(handler), |
| serving_fallback_(false), |
| fallback_responses_served_(NULL) { |
| if (fallback != NULL && !fallback->Empty()) { |
| fallback_.Link(fallback); |
| } |
| } |
| |
| FallbackSharedAsyncFetch::~FallbackSharedAsyncFetch() {} |
| |
| void FallbackSharedAsyncFetch::HandleHeadersComplete() { |
| if (response_headers()->IsServerErrorStatus() && !fallback_.Empty()) { |
| // If the fetch resulted in a server side error from the origin, stop |
| // passing any events through to the base fetch until HandleDone(). |
| serving_fallback_ = true; |
| response_headers()->Clear(); |
| fallback_.ExtractHeaders(response_headers(), handler_); |
| // Add a warning header indicating that the response is stale. |
| response_headers()->Add(HttpAttributes::kWarning, kStaleWarningHeaderValue); |
| response_headers()->ComputeCaching(); |
| StringPiece contents; |
| fallback_.ExtractContents(&contents); |
| set_content_length(contents.size()); |
| SharedAsyncFetch::HandleHeadersComplete(); |
| SharedAsyncFetch::HandleWrite(contents, handler_); |
| SharedAsyncFetch::HandleFlush(handler_); |
| if (fallback_responses_served_ != NULL) { |
| fallback_responses_served_->Add(1); |
| } |
| // Do not call Done() on the base fetch yet since it could delete shared |
| // pointers. |
| } else { |
| SharedAsyncFetch::HandleHeadersComplete(); |
| } |
| } |
| |
| bool FallbackSharedAsyncFetch::HandleWrite(const StringPiece& content, |
| MessageHandler* handler) { |
| if (serving_fallback_) { |
| return true; |
| } |
| return SharedAsyncFetch::HandleWrite(content, handler); |
| } |
| |
| bool FallbackSharedAsyncFetch::HandleFlush(MessageHandler* handler) { |
| if (serving_fallback_) { |
| return true; |
| } |
| return SharedAsyncFetch::HandleFlush(handler); |
| } |
| |
| void FallbackSharedAsyncFetch::HandleDone(bool success) { |
| SharedAsyncFetch::HandleDone(serving_fallback_ || success); |
| delete this; |
| } |
| |
| ConditionalSharedAsyncFetch::ConditionalSharedAsyncFetch( |
| AsyncFetch* base_fetch, |
| HTTPValue* cached_value, |
| MessageHandler* handler) |
| : SharedAsyncFetch(base_fetch), |
| handler_(handler), |
| serving_cached_value_(false), |
| added_conditional_headers_to_request_(false), |
| num_conditional_refreshes_(NULL) { |
| if (cached_value != NULL && !cached_value->Empty()) { |
| // Only do our own conditional fetch if the original request wasn't |
| // conditional. |
| if (!request_headers()->Has(HttpAttributes::kIfModifiedSince) && |
| !request_headers()->Has(HttpAttributes::kIfNoneMatch)) { |
| ResponseHeaders cached_response_headers(request_context()->options()); |
| cached_value->ExtractHeaders(&cached_response_headers, handler_); |
| // Check that the cached response is a 200. |
| if (cached_response_headers.status_code() == HttpStatus::kOK) { |
| // Copy the Etag and Last-Modified if any into the If-None-Match and |
| // If-Modified-Since request headers. Also, ensure that the Etag wasn't |
| // added by us. |
| const char* etag = cached_response_headers.Lookup1( |
| HttpAttributes::kEtag); |
| if (etag != NULL && !StringCaseStartsWith(etag, |
| HTTPCache::kEtagPrefix)) { |
| request_headers()->Add(HttpAttributes::kIfNoneMatch, etag); |
| added_conditional_headers_to_request_ = true; |
| } |
| const char* last_modified = cached_response_headers.Lookup1( |
| HttpAttributes::kLastModified); |
| if (last_modified != NULL) { |
| request_headers()->Add(HttpAttributes::kIfModifiedSince, |
| last_modified); |
| added_conditional_headers_to_request_ = true; |
| } |
| } |
| if (added_conditional_headers_to_request_) { |
| cached_value_.Link(cached_value); |
| } |
| } |
| } |
| } |
| |
| ConditionalSharedAsyncFetch::~ConditionalSharedAsyncFetch() {} |
| |
| void ConditionalSharedAsyncFetch::HandleHeadersComplete() { |
| if (added_conditional_headers_to_request_ && |
| response_headers()->status_code() == HttpStatus::kNotModified) { |
| // If the fetch resulted in a 304 from the server, serve the cached response |
| // and stop passing any events through to the base fetch. |
| serving_cached_value_ = true; |
| int64 implicit_cache_ttl_ms = response_headers()->implicit_cache_ttl_ms(); |
| int64 min_cache_ttl_ms = response_headers()->min_cache_ttl_ms(); |
| response_headers()->Clear(); |
| cached_value_.ExtractHeaders(response_headers(), handler_); |
| if (response_headers()->is_implicitly_cacheable()) { |
| response_headers()->SetCacheControlMaxAge(implicit_cache_ttl_ms); |
| response_headers()->ComputeCaching(); |
| } else if (response_headers()->cache_ttl_ms() < min_cache_ttl_ms) { |
| response_headers()->SetCacheControlMaxAge(min_cache_ttl_ms); |
| response_headers()->ComputeCaching(); |
| } |
| SharedAsyncFetch::HandleHeadersComplete(); |
| StringPiece contents; |
| cached_value_.ExtractContents(&contents); |
| SharedAsyncFetch::HandleWrite(contents, handler_); |
| SharedAsyncFetch::HandleFlush(handler_); |
| // Do not call Done() on the base fetch yet since it could delete shared |
| // pointers. |
| if (num_conditional_refreshes_ != NULL) { |
| num_conditional_refreshes_->Add(1); |
| } |
| } else { |
| SharedAsyncFetch::HandleHeadersComplete(); |
| } |
| } |
| |
| bool ConditionalSharedAsyncFetch::HandleWrite(const StringPiece& content, |
| MessageHandler* handler) { |
| if (serving_cached_value_) { |
| return true; |
| } |
| return SharedAsyncFetch::HandleWrite(content, handler); |
| } |
| |
| bool ConditionalSharedAsyncFetch::HandleFlush(MessageHandler* handler) { |
| if (serving_cached_value_) { |
| return true; |
| } |
| return SharedAsyncFetch::HandleFlush(handler); |
| } |
| |
| void ConditionalSharedAsyncFetch::HandleDone(bool success) { |
| SharedAsyncFetch::HandleDone(serving_cached_value_ || success); |
| delete this; |
| } |
| |
| } // namespace net_instaweb |