blob: 7ea12cba6ec8d2303310a881b4bd8a9aa383a56f [file] [log] [blame]
/*
* 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: morlovich@google.com (Maksim Orlovich)
// Unit-tests for ProxyInterface.
#include "pagespeed/automatic/proxy_interface.h"
#include "net/instaweb/http/public/async_fetch.h"
#include "net/instaweb/http/public/http_cache.h"
#include "net/instaweb/http/public/logging_proto_impl.h"
#include "net/instaweb/http/public/mock_callback.h"
#include "net/instaweb/http/public/mock_url_fetcher.h"
#include "net/instaweb/http/public/reflecting_test_fetcher.h"
#include "net/instaweb/http/public/request_context.h"
#include "net/instaweb/http/public/request_timing_info.h"
#include "net/instaweb/rewriter/public/blink_util.h"
#include "net/instaweb/rewriter/public/domain_lawyer.h"
#include "net/instaweb/rewriter/public/experiment_util.h"
#include "net/instaweb/rewriter/public/rewrite_driver.h"
#include "net/instaweb/rewriter/public/rewrite_options.h"
#include "net/instaweb/rewriter/public/rewrite_test_base.h"
#include "net/instaweb/rewriter/public/server_context.h"
#include "net/instaweb/rewriter/public/test_rewrite_driver_factory.h"
#include "net/instaweb/util/public/fallback_property_page.h"
#include "net/instaweb/util/public/mock_property_page.h"
#include "net/instaweb/util/public/property_cache.h"
#include "pagespeed/automatic/proxy_fetch.h"
#include "pagespeed/automatic/proxy_interface_test_base.h"
#include "pagespeed/kernel/base/abstract_mutex.h"
#include "pagespeed/kernel/base/basictypes.h"
#include "pagespeed/kernel/base/function.h"
#include "pagespeed/kernel/base/gtest.h"
#include "pagespeed/kernel/base/mock_message_handler.h"
#include "pagespeed/kernel/base/mock_timer.h"
#include "pagespeed/kernel/base/null_message_handler.h"
#include "pagespeed/kernel/base/scoped_ptr.h"
#include "pagespeed/kernel/base/statistics.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/base/string_util.h"
#include "pagespeed/kernel/base/thread_system.h"
#include "pagespeed/kernel/base/time_util.h"
#include "pagespeed/kernel/base/timer.h"
#include "pagespeed/kernel/cache/lru_cache.h"
#include "pagespeed/kernel/html/html_parse_test_base.h"
#include "pagespeed/kernel/http/content_type.h"
#include "pagespeed/kernel/http/google_url.h"
#include "pagespeed/kernel/http/http_names.h"
#include "pagespeed/kernel/http/request_headers.h"
#include "pagespeed/kernel/http/response_headers.h"
#include "pagespeed/kernel/http/semantic_type.h"
#include "pagespeed/kernel/http/user_agent_matcher.h"
#include "pagespeed/kernel/thread/mock_scheduler.h"
#include "pagespeed/kernel/thread/queued_worker_pool.h"
#include "pagespeed/kernel/thread/thread_synchronizer.h"
#include "pagespeed/kernel/thread/worker_test_base.h"
namespace net_instaweb {
namespace {
// This jpeg file lacks a .jpg or .jpeg extension. So we initiate
// a property-cache read prior to getting the response-headers back,
// but will never go into the ProxyFetch flow that blocks waiting
// for the cache lookup to come back.
const char kImageFilenameLackingExt[] = "jpg_file_lacks_ext";
const char kHttpsPageUrl[] = "https://www.test.com/page.html";
const char kHttpsCssUrl[] = "https://www.test.com/style.css";
const char kCssContent[] = "* { display: none; }";
const char kMinimizedCssContent[] = "*{display:none}";
} // namespace
class ProxyInterfaceTest : public ProxyInterfaceTestBase {
protected:
static const int kHtmlCacheTimeSec = 5000;
ProxyInterfaceTest()
: start_time_ms_(0),
max_age_300_("max-age=300") {
ConvertTimeToString(MockTimer::kApr_5_2010_ms, &start_time_string_);
ConvertTimeToString(MockTimer::kApr_5_2010_ms + 5 * Timer::kMinuteMs,
&start_time_plus_300s_string_);
ConvertTimeToString(MockTimer::kApr_5_2010_ms - 2 * Timer::kDayMs,
&old_time_string_);
}
virtual ~ProxyInterfaceTest() {}
virtual void SetUp() {
ThreadSynchronizer* sync = server_context()->thread_synchronizer();
sync->EnableForPrefix(ProxyFetch::kCollectorDoneFinish);
sync->EnableForPrefix(ProxyFetch::kCollectorDetachFinish);
sync->EnableForPrefix(ProxyFetch::kCollectorConnectProxyFetchFinish);
server_context_->set_enable_property_cache(true);
const PropertyCache::Cohort* dom_cohort =
SetupCohort(server_context_->page_property_cache(),
RewriteDriver::kDomCohort);
const PropertyCache::Cohort* blink_cohort =
SetupCohort(server_context_->page_property_cache(),
BlinkUtil::kBlinkCohort);
server_context()->set_dom_cohort(dom_cohort);
server_context()->set_blink_cohort(blink_cohort);
RewriteOptions* options = server_context()->global_options();
options->ClearSignatureForTesting();
options->EnableFilter(RewriteOptions::kRewriteCss);
options->set_max_html_cache_time_ms(kHtmlCacheTimeSec * Timer::kSecondMs);
options->set_in_place_rewriting_enabled(true);
options->Disallow("*blacklist*");
server_context()->ComputeSignature(options);
ProxyInterfaceTestBase::SetUp();
// The original url_async_fetcher() is still owned by RewriteDriverFactory.
background_fetch_fetcher_.reset(new BackgroundFetchCheckingUrlAsyncFetcher(
factory()->ComputeUrlAsyncFetcher()));
server_context()->set_default_system_fetcher(
background_fetch_fetcher_.get());
start_time_ms_ = timer()->NowMs();
SetResponseWithDefaultHeaders(kImageFilenameLackingExt, kContentTypeJpeg,
"image data", 300);
SetResponseWithDefaultHeaders(kPageUrl, kContentTypeHtml,
"<div><p></p></div>", 0);
}
void CheckHeaders(const ResponseHeaders& headers,
const ContentType& expect_type) {
ASSERT_TRUE(headers.has_status_code());
EXPECT_EQ(HttpStatus::kOK, headers.status_code());
EXPECT_STREQ(expect_type.mime_type(),
headers.Lookup1(HttpAttributes::kContentType));
}
void CheckBackgroundFetch(const ResponseHeaders& headers,
bool is_background_fetch) {
EXPECT_STREQ(is_background_fetch ? "1" : "0",
headers.Lookup1(kBackgroundFetchHeader));
}
void CheckNumBackgroundFetches(int num) {
EXPECT_EQ(num, background_fetch_fetcher_->num_background_fetches());
}
virtual void ClearStats() {
RewriteTestBase::ClearStats();
background_fetch_fetcher_->clear_num_background_fetches();
}
// Serve a trivial HTML page with initial Cache-Control header set to
// input_cache_control and return the Cache-Control header after running
// through ProxyInterface.
//
// A unique id must be set to assure different websites are requested.
// id is put in a URL, so it probably shouldn't have spaces and other
// special chars.
GoogleString RewriteHtmlCacheHeader(const StringPiece& id,
const StringPiece& input_cache_control) {
GoogleString url = StrCat("http://www.example.com/", id, ".html");
ResponseHeaders input_headers;
DefaultResponseHeaders(kContentTypeHtml, 100, &input_headers);
input_headers.Replace(HttpAttributes::kCacheControl, input_cache_control);
SetFetchResponse(url, input_headers, "<body>Foo</body>");
GoogleString body;
ResponseHeaders output_headers;
FetchFromProxy(url, true, &body, &output_headers);
return output_headers.LookupJoined(HttpAttributes::kCacheControl);
}
int GetStatusCodeInPropertyCache(const GoogleString& url) {
PropertyCache* pcache = page_property_cache();
scoped_ptr<MockPropertyPage> page(NewMockPage(
url,
server_context()->GetRewriteOptionsSignatureHash(
server_context()->global_options()),
UserAgentMatcher::kDesktop));
const PropertyCache::Cohort* cohort = pcache->GetCohort(
RewriteDriver::kDomCohort);
PropertyValue* value;
pcache->Read(page.get());
value = page->GetProperty(
cohort, RewriteDriver::kStatusCodePropertyName);
int status_code;
EXPECT_TRUE(StringToInt(value->value(), &status_code));
return status_code;
}
void DisableAjax() {
RewriteOptions* options = server_context()->global_options();
options->ClearSignatureForTesting();
options->set_in_place_rewriting_enabled(false);
server_context()->ComputeSignature(options);
}
void RejectBlacklisted() {
RewriteOptions* options = server_context()->global_options();
options->ClearSignatureForTesting();
options->set_reject_blacklisted(true);
options->set_reject_blacklisted_status_code(HttpStatus::kImATeapot);
server_context()->ComputeSignature(options);
}
// This function is primarily meant to enable writes to the dom cohort of the
// property cache. Writes to this cohort are predicated on a filter that uses
// that cohort being enabled, which includes the insert dns prefetch filter.
void EnableDomCohortWritesWithDnsPrefetch() {
RewriteOptions* options = server_context()->global_options();
options->ClearSignatureForTesting();
options->EnableFilter(RewriteOptions::kInsertDnsPrefetch);
server_context()->ComputeSignature(options);
}
void TestFallbackPageProperties(
const GoogleString& url, const GoogleString& fallback_url) {
GoogleUrl gurl(url);
GoogleString kPropertyName("prop");
GoogleString kValue("value");
options()->set_use_fallback_property_cache_values(true);
// No fallback value is present.
const PropertyCache::Cohort* cohort =
page_property_cache()->GetCohort(RewriteDriver::kDomCohort);
StringAsyncFetch callback(
RequestContext::NewTestRequestContext(
server_context()->thread_system()));
RequestHeaders request_headers;
callback.set_request_headers(&request_headers);
scoped_ptr<ProxyFetchPropertyCallbackCollector> callback_collector(
proxy_interface_->InitiatePropertyCacheLookup(
false, gurl, options(), &callback, false));
FallbackPropertyPage* fallback_page =
callback_collector->fallback_property_page();
fallback_page->UpdateValue(cohort, kPropertyName, kValue);
fallback_page->WriteCohort(cohort);
// Read from fallback value.
GoogleUrl new_gurl(fallback_url);
callback_collector.reset(proxy_interface_->InitiatePropertyCacheLookup(
false, new_gurl, options(), &callback, false));
fallback_page = callback_collector->fallback_property_page();
EXPECT_FALSE(fallback_page->actual_property_page()->GetProperty(
cohort, kPropertyName)->has_value());
EXPECT_EQ(kValue,
fallback_page->GetProperty(cohort, kPropertyName)->value());
// If use_fallback_property_cache_values option is set to false, fallback
// values will not be used.
options()->ClearSignatureForTesting();
options()->set_use_fallback_property_cache_values(false);
callback_collector.reset(proxy_interface_->InitiatePropertyCacheLookup(
false, new_gurl, options(), &callback, false));
EXPECT_FALSE(callback_collector->fallback_property_page()->GetProperty(
cohort, kPropertyName)->has_value());
}
void TestQueryParameters(StringPiece original_domain,
StringPiece redirect_domain,
bool add_params_to_redirect,
bool add_params_to_location) {
// Test to check if we re-add our query params when we get a redirect.
const char kParams[] = "PageSpeedCssInlineMaxBytes=99";
GoogleString original_url = StrCat(original_domain, "a/?x=y");
GoogleString redirect_url = StrCat(redirect_domain, "b/",
add_params_to_redirect ? "?x=y" : "");
GoogleString set_text, get_text;
RequestHeaders request_headers;
ResponseHeaders set_headers, get_headers;
NullMessageHandler handler;
GoogleString expected_location(redirect_url);
if (add_params_to_location) {
StrAppend(&expected_location,
add_params_to_redirect ? "&" : "?", kParams);
}
set_headers.Add(HttpAttributes::kContentType, kContentTypeHtml.mime_type());
set_headers.Add(HttpAttributes::kLocation, redirect_url);
set_headers.SetStatusAndReason(HttpStatus::kFound);
set_text = "<html></html>";
mock_url_fetcher_.SetResponse(original_url, set_headers, set_text);
FetchFromProxy(StrCat(original_url, "&", kParams), request_headers, true,
&get_text, &get_headers);
EXPECT_STREQ(StrCat("HTTP/1.0 302 Found\r\n"
"Content-Type: text/html\r\n"
"Location: ", expected_location, "\r\n"
"X-Background-Fetch: 0\r\n"
"Date: Tue, 02 Feb 2010 18:51:26 GMT\r\n"
"Expires: Tue, 02 Feb 2010 18:51:26 GMT\r\n"
"Cache-Control: max-age=0, private\r\n"
"X-Page-Speed: \r\n"
"HeadersComplete: 1\r\n\r\n"),
get_headers.ToString());
}
scoped_ptr<BackgroundFetchCheckingUrlAsyncFetcher> background_fetch_fetcher_;
int64 start_time_ms_;
GoogleString start_time_string_;
GoogleString start_time_plus_300s_string_;
GoogleString old_time_string_;
const GoogleString max_age_300_;
private:
DISALLOW_COPY_AND_ASSIGN(ProxyInterfaceTest);
};
TEST_F(ProxyInterfaceTest, LoggingInfo) {
GoogleString url = "http://www.example.com/";
GoogleString text;
RequestHeaders request_headers;
ResponseHeaders headers;
headers.Add(HttpAttributes::kContentType, kContentTypeHtml.mime_type());
headers.SetStatusAndReason(HttpStatus::kOK);
// Fetch HTML content.
mock_url_fetcher_.SetResponse(url, headers, "<html></html>");
FetchFromProxy(url, request_headers, true, &text, &headers);
CheckBackgroundFetch(headers, false);
CheckNumBackgroundFetches(0);
const RequestTimingInfo& rti = timing_info();
int64 latency_ms;
ASSERT_TRUE(rti.GetHTTPCacheLatencyMs(&latency_ms));
EXPECT_EQ(0, latency_ms);
EXPECT_FALSE(rti.GetL2HTTPCacheLatencyMs(&latency_ms));
EXPECT_FALSE(rti.GetFetchHeaderLatencyMs(&latency_ms));
EXPECT_FALSE(rti.GetFetchLatencyMs(&latency_ms));
EXPECT_TRUE(logging_info()->is_html_response());
EXPECT_FALSE(logging_info()->is_url_disallowed());
EXPECT_FALSE(logging_info()->is_request_disabled());
EXPECT_FALSE(logging_info()->is_pagespeed_resource());
// Fetch non-HTML content.
logging_info()->Clear();
mock_url_fetcher_.SetResponse(url, headers, "js");
FetchFromProxy(url, request_headers, true, &text, &headers);
EXPECT_FALSE(logging_info()->is_html_response());
EXPECT_FALSE(logging_info()->is_url_disallowed());
EXPECT_FALSE(logging_info()->is_request_disabled());
// Fetch blacklisted url.
url = "http://www.blacklist.com/";
logging_info()->Clear();
mock_url_fetcher_.SetResponse(url, headers, "<html></html>");
FetchFromProxy(url,
request_headers,
true, /* expect_success */
&text,
&headers,
false /* proxy_fetch_property_callback_collector_created */);
EXPECT_TRUE(logging_info()->is_html_response());
EXPECT_TRUE(logging_info()->is_url_disallowed());
EXPECT_FALSE(logging_info()->is_request_disabled());
// Fetch disabled url.
url = "http://www.example.com/?PageSpeed=off";
logging_info()->Clear();
mock_url_fetcher_.SetResponse("http://www.example.com/", headers,
"<html></html>");
FetchFromProxy(url,
request_headers,
true, /* expect_success */
&text,
&headers,
false /* proxy_fetch_property_callback_collector_created */);
EXPECT_TRUE(logging_info()->is_html_response());
EXPECT_FALSE(logging_info()->is_url_disallowed());
EXPECT_TRUE(logging_info()->is_request_disabled());
}
TEST_F(ProxyInterfaceTest, SkipPropertyCacheLookupIfOptionsNotEnabled) {
GoogleString url = "http://www.example.com/";
GoogleString text;
RequestHeaders request_headers;
ResponseHeaders headers;
headers.Add(HttpAttributes::kContentType, kContentTypeHtml.mime_type());
headers.SetStatusAndReason(HttpStatus::kOK);
// Fetch disabled url.
url = "http://www.example.com/?PageSpeed=off";
logging_info()->Clear();
mock_url_fetcher_.SetResponse("http://www.example.com/", headers,
"<html></html>");
FetchFromProxy(url,
request_headers,
true, /* expect_success */
&text,
&headers,
false /* proxy_fetch_property_callback_collector_created */);
EXPECT_TRUE(logging_info()->is_html_response());
EXPECT_FALSE(logging_info()->is_url_disallowed());
EXPECT_TRUE(logging_info()->is_request_disabled());
// Only the HTTP response lookup is issued and it is not in the cache.
EXPECT_EQ(0, lru_cache()->num_hits());
EXPECT_EQ(1, lru_cache()->num_misses());
EXPECT_EQ(1, http_cache()->cache_misses()->Get());
}
TEST_F(ProxyInterfaceTest, SkipPropertyCacheLookupIfUrlBlacklisted) {
GoogleString url = "http://www.blacklist.com/";
RequestHeaders request_headers;
GoogleString text;
ResponseHeaders headers;
headers.Add(HttpAttributes::kContentType, kContentTypeHtml.mime_type());
headers.SetStatusAndReason(HttpStatus::kOK);
scoped_ptr<RewriteOptions> custom_options(
server_context()->global_options()->Clone());
custom_options->AddRejectedUrlWildcard(AbsolutifyUrl("blacklist*"));
SetRewriteOptions(custom_options.get());
logging_info()->Clear();
mock_url_fetcher_.SetResponse(url, headers, "<html></html>");
FetchFromProxy(url, request_headers, true, &text, &headers, false);
EXPECT_TRUE(logging_info()->is_html_response());
EXPECT_TRUE(logging_info()->is_url_disallowed());
EXPECT_FALSE(logging_info()->is_request_disabled());
// Only the HTTP response lookup is issued and it is not in the cache.
EXPECT_EQ(0, lru_cache()->num_hits());
EXPECT_EQ(1, lru_cache()->num_misses());
EXPECT_EQ(1, http_cache()->cache_misses()->Get());
}
TEST_F(ProxyInterfaceTest, HeadRequest) {
// Test to check if we are handling Head requests correctly.
GoogleString url = "http://www.example.com/";
GoogleString set_text, get_text;
RequestHeaders request_headers;
ResponseHeaders set_headers, get_headers;
set_headers.Add(HttpAttributes::kContentType, kContentTypeHtml.mime_type());
set_headers.SetStatusAndReason(HttpStatus::kOK);
set_text = "<html><head/></html>";
mock_url_fetcher_.SetResponse(url, set_headers, set_text);
FetchFromProxy(url, request_headers, true, &get_text, &get_headers);
// Headers and body are correct for a Get request.
EXPECT_EQ("HTTP/1.0 200 OK\r\n"
"Content-Type: text/html\r\n"
"X-Background-Fetch: 0\r\n"
"Date: Tue, 02 Feb 2010 18:51:26 GMT\r\n"
"Expires: Tue, 02 Feb 2010 18:51:26 GMT\r\n"
"Cache-Control: max-age=0, private\r\n"
"X-Page-Speed: \r\n"
"HeadersComplete: 1\r\n\r\n", get_headers.ToString());
EXPECT_EQ(set_text, get_text);
// Remove from the cache so we can actually test a HEAD fetch.
http_cache()->Delete(url, rewrite_driver_->CacheFragment());
ClearStats();
// Headers and body are correct for a Head request.
request_headers.set_method(RequestHeaders::kHead);
FetchFromProxy(url,
request_headers,
true, /* expect_success */
&get_text,
&get_headers,
false /* proxy_fetch_property_callback_collector_created */);
EXPECT_EQ(0, http_cache()->cache_hits()->Get());
EXPECT_EQ("HTTP/1.0 200 OK\r\n"
"Content-Type: text/html\r\n"
"X-Background-Fetch: 0\r\n"
"X-Page-Speed: \r\n"
"HeadersComplete: 1\r\n\r\n", get_headers.ToString());
EXPECT_TRUE(get_text.empty());
}
TEST_F(ProxyInterfaceTest, RedirectRequestWhenDomainRewriterEnabled) {
// A MapRewriteDomain should not change the Location of a redirect.
// Test to check if we are handling Head requests correctly.
GoogleString url = "http://www.example.com/";
GoogleString set_text, get_text;
RequestHeaders request_headers;
ResponseHeaders set_headers, get_headers;
NullMessageHandler handler;
set_headers.Add(HttpAttributes::kContentType, kContentTypeHtml.mime_type());
set_headers.Add(HttpAttributes::kLocation, "http://m.example.com");
set_headers.SetStatusAndReason(HttpStatus::kFound);
scoped_ptr<RewriteOptions> custom_options(
server_context()->global_options()->Clone());
custom_options->EnableFilter(RewriteOptions::kRewriteDomains);
custom_options->WriteableDomainLawyer()->AddTwoProtocolRewriteDomainMapping(
"www.example.com", "m.example.com", &handler);
SetRewriteOptions(custom_options.get());
set_text = "<html></html>";
mock_url_fetcher_.SetResponse(url, set_headers, set_text);
FetchFromProxy(url, request_headers, true, &get_text, &get_headers);
// Headers and body are correct for a Get request.
EXPECT_STREQ("HTTP/1.0 302 Found\r\n"
"Content-Type: text/html\r\n"
"Location: http://m.example.com\r\n"
"X-Background-Fetch: 0\r\n"
"Date: Tue, 02 Feb 2010 18:51:26 GMT\r\n"
"Expires: Tue, 02 Feb 2010 18:51:26 GMT\r\n"
"Cache-Control: max-age=0, private\r\n"
"X-Page-Speed: \r\n"
"HeadersComplete: 1\r\n\r\n",
get_headers.ToString());
}
TEST_F(ProxyInterfaceTest, RewriteDomainForRedirectsAndCookiesWithEmptyBody) {
GoogleString url = "http://www.example.com/";
GoogleString get_text;
RequestHeaders request_headers;
ResponseHeaders set_headers, get_headers;
NullMessageHandler handler;
set_headers.Add(HttpAttributes::kContentType, kContentTypeHtml.mime_type());
set_headers.Add(HttpAttributes::kLocation, "http://m.example.com");
set_headers.Add(HttpAttributes::kSetCookie,
"a=b; domain=m.example.com; HttpOnly");
set_headers.SetStatusAndReason(HttpStatus::kFound);
scoped_ptr<RewriteOptions> custom_options(
server_context()->global_options()->Clone());
custom_options->EnableFilter(RewriteOptions::kRewriteDomains);
custom_options->WriteableDomainLawyer()->AddTwoProtocolRewriteDomainMapping(
"www.example.com", "m.example.com", &handler);
custom_options->set_domain_rewrite_hyperlinks(true);
custom_options->set_domain_rewrite_cookies(true);
SetRewriteOptions(custom_options.get());
mock_url_fetcher_.SetResponse(url, set_headers, "");
FetchFromProxy(url, request_headers, true, &get_text, &get_headers);
// The ProxyFetch should change the Location of a redirect when
// domain_rewrite_hyperlinks is enabled.
const char* location = get_headers.Lookup1(HttpAttributes::kLocation);
EXPECT_STREQ("http://www.example.com/", location);
// The ProxyFetch should change the domain of a Cookie when
// domain_rewrite_cookies is enabled.
const char* cookie = get_headers.Lookup1(HttpAttributes::kSetCookie);
EXPECT_STREQ("a=b; domain=www.example.com; HttpOnly", cookie);
}
TEST_F(ProxyInterfaceTest, RedirectRequestRetainsQueryParams) {
// "Location: http://www.example.com/b/?x=y&PageSpeedCssInlineMaxBytes=99" is
// what we expect to see - note the PageSpeed query parameter IS copied over.
TestQueryParameters(/* original_domain= */ "http://www.example.com/",
/* redirect_domain= */ "http://www.example.com/",
/* add_params_to_redirect= */ true,
/* add_params_to_location= */ true);
}
TEST_F(ProxyInterfaceTest, RedirectRequestRetainsOnlyPagespeedQueryParams) {
// "Location: http://www.example.com/b/?PageSpeedCssInlineMaxBytes=99" is
// what we expect to see - note the PageSpeed query parameter IS copied over.
TestQueryParameters(/* original_domain= */ "http://www.example.com/",
/* redirect_domain= */ "http://www.example.com/",
/* add_params_to_redirect= */ false,
/* add_params_to_location= */ true);
}
TEST_F(ProxyInterfaceTest, RedirectToDiffDomainDiscardsQueryParams) {
// "Location: http://test.com/?x=y" is what we expect to see - note the
// PageSpeed query parameter is NOT copied over.
TestQueryParameters(/* original_domain= */ "http://www.example.com/",
/* redirect_domain= */ kTestDomain,
/* add_params_to_redirect= */ true,
/* add_params_to_location= */ false);
}
TEST_F(ProxyInterfaceTest, RedirectToDiffDomainDiscardsPagespeedQueryParams) {
// "Location: http://test.com/" is what we expect to see - note the
// PageSpeed query parameter is NOT copied over.
TestQueryParameters(/* original_domain= */ "http://www.example.com/",
/* redirect_domain= */ kTestDomain,
/* add_params_to_redirect= */ false,
/* add_params_to_location= */ false);
}
TEST_F(ProxyInterfaceTest, HeadResourceRequest) {
// Test to check if we are handling Head requests correctly in pagespeed
// resource flow.
const char kCssWithEmbeddedImage[] = "*{background-image:url(%s)}";
const char kBackgroundImage[] = "1.png";
GoogleString text;
RequestHeaders request_headers;
ResponseHeaders response_headers;
GoogleString expected_response_headers_string = "HTTP/1.1 200 OK\r\n"
"Content-Type: text/css\r\n"
"X-Background-Fetch: 0\r\n"
"X-Content-Type-Options: nosniff\r\n"
"Date: Tue, 02 Feb 2010 18:51:26 GMT\r\n"
"Expires: Tue, 02 Feb 2010 18:56:26 GMT\r\n"
"Cache-Control: max-age=300,private\r\n"
"X-Page-Speed: \r\n"
"HeadersComplete: 1\r\n\r\n";
// We're not going to image-compress so we don't need our mock image
// to really be an image.
SetResponseWithDefaultHeaders(kBackgroundImage, kContentTypePng, "image",
kHtmlCacheTimeSec * 2);
GoogleString orig_css = StringPrintf(kCssWithEmbeddedImage, kBackgroundImage);
SetResponseWithDefaultHeaders("embedded.css", kContentTypeCss,
orig_css, kHtmlCacheTimeSec * 2);
// By default, cache extension is off in the default options.
server_context()->global_options()->SetDefaultRewriteLevel(
RewriteOptions::kPassThrough);
// Because cache-extension was turned off, the image in the CSS file
// will not be changed.
FetchFromProxy("I.embedded.css.pagespeed.cf.0.css",
request_headers,
true, /* expect_success */
&text,
&response_headers,
false /* proxy_fetch_property_callback_collector_created */);
EXPECT_TRUE(logging_info()->is_pagespeed_resource());
EXPECT_EQ(expected_response_headers_string, response_headers.ToString());
EXPECT_EQ(orig_css, text);
// Headers and body are correct for a Head request.
request_headers.set_method(RequestHeaders::kHead);
FetchFromProxy("I.embedded.css.pagespeed.cf.0.css",
request_headers,
true, /* expect_success */
&text,
&response_headers,
false /* proxy_fetch_property_callback_collector_created */);
// This leads to a conditional refresh of the original resource.
expected_response_headers_string = "HTTP/1.1 200 OK\r\n"
"Content-Type: text/css\r\n"
"X-Background-Fetch: 0\r\n"
"X-Content-Type-Options: nosniff\r\n"
"Date: Tue, 02 Feb 2010 18:51:26 GMT\r\n"
"Expires: Tue, 02 Feb 2010 18:56:26 GMT\r\n"
"Cache-Control: max-age=300,private\r\n"
"X-Page-Speed: \r\n"
"HeadersComplete: 1\r\n\r\n";
EXPECT_EQ(expected_response_headers_string, response_headers.ToString());
EXPECT_TRUE(text.empty());
}
TEST_F(ProxyInterfaceTest, FetchFailure) {
GoogleString text;
ResponseHeaders headers;
// We don't want fetcher to fail the test, merely the fetch.
SetFetchFailOnUnexpected(false);
FetchFromProxy("invalid", false, &text, &headers);
CheckBackgroundFetch(headers, false);
CheckNumBackgroundFetches(0);
}
TEST_F(ProxyInterfaceTest, ReturnUnavailableForBlockedUrls) {
GoogleString text;
ResponseHeaders response_headers;
response_headers.SetStatusAndReason(HttpStatus::kOK);
mock_url_fetcher_.SetResponse(AbsolutifyUrl("blocked"), response_headers,
"<html></html>");
FetchFromProxy("blocked", true,
&text, &response_headers);
EXPECT_EQ(HttpStatus::kOK, response_headers.status_code());
text.clear();
response_headers.Clear();
scoped_ptr<RewriteOptions> custom_options(
server_context()->global_options()->Clone());
custom_options->AddRejectedUrlWildcard(AbsolutifyUrl("block*"));
SetRewriteOptions(custom_options.get());
RequestHeaders request_headers;
FetchFromProxy(
"blocked",
request_headers,
false, /* expect_success */
&text,
&response_headers,
false /* proxy_fetch_property_callback_collector_created */);
EXPECT_EQ(HttpStatus::kProxyDeclinedRequest, response_headers.status_code());
}
TEST_F(ProxyInterfaceTest, ReturnUnavailableForBlockedHeaders) {
GoogleString text;
RequestHeaders request_headers;
ResponseHeaders response_headers;
response_headers.SetStatusAndReason(HttpStatus::kOK);
mock_url_fetcher_.SetResponse(kTestDomain, response_headers, "<html></html>");
scoped_ptr<RewriteOptions> custom_options(
server_context()->global_options()->Clone());
custom_options->AddRejectedHeaderWildcard(HttpAttributes::kUserAgent,
"*Chrome*");
custom_options->AddRejectedHeaderWildcard(HttpAttributes::kXForwardedFor,
"10.3.4.*");
SetRewriteOptions(custom_options.get());
request_headers.Add(HttpAttributes::kUserAgent, "Firefox");
request_headers.Add(HttpAttributes::kXForwardedFor, "10.0.0.11");
FetchFromProxy(kTestDomain, request_headers, true,
&text, &response_headers);
EXPECT_EQ(HttpStatus::kOK, response_headers.status_code());
request_headers.Clear();
response_headers.Clear();
request_headers.Add(HttpAttributes::kUserAgent, "abc");
request_headers.Add(HttpAttributes::kUserAgent, "xyz Chrome abc");
FetchFromProxy(kTestDomain,
request_headers,
false, /* expect_success */
&text,
&response_headers,
false /* proxy_fetch_property_callback_collector_created */);
EXPECT_EQ(HttpStatus::kProxyDeclinedRequest, response_headers.status_code());
request_headers.Clear();
response_headers.Clear();
request_headers.Add(HttpAttributes::kXForwardedFor, "10.3.4.32");
FetchFromProxy(kTestDomain,
request_headers,
false, /* expect_success */
&text,
&response_headers,
false /* proxy_fetch_property_callback_collector_created */);
EXPECT_EQ(HttpStatus::kProxyDeclinedRequest, response_headers.status_code());
}
TEST_F(ProxyInterfaceTest, InvalidUrl) {
ExpectStringAsyncFetch fetch(false, rewrite_driver()->request_context());
proxy_interface_->Fetch("localhost:3141", message_handler(), &fetch);
EXPECT_TRUE(fetch.done());
}
TEST_F(ProxyInterfaceTest, PassThrough404) {
GoogleString text;
ResponseHeaders headers;
SetFetchResponse404("404");
FetchFromProxy("404", true, &text, &headers);
ASSERT_TRUE(headers.has_status_code());
EXPECT_EQ(HttpStatus::kNotFound, headers.status_code());
}
TEST_F(ProxyInterfaceTest, PassThroughResource) {
GoogleString text;
ResponseHeaders headers;
const char kContent[] = "A very compelling article";
SetResponseWithDefaultHeaders("text.txt", kContentTypeText, kContent,
kHtmlCacheTimeSec * 2);
FetchFromProxy("text.txt", true, &text, &headers);
CheckHeaders(headers, kContentTypeText);
CheckBackgroundFetch(headers, false);
CheckNumBackgroundFetches(0);
EXPECT_EQ(kContent, text);
}
TEST_F(ProxyInterfaceTest, PassThroughEmptyResource) {
ResponseHeaders headers;
const char kContent[] = "";
SetDefaultLongCacheHeaders(&kContentTypeText, &headers);
SetFetchResponse(AbsolutifyUrl("text.txt"), headers, kContent);
GoogleString text;
ResponseHeaders response_headers;
FetchFromProxy("text.txt", true, &text, &response_headers);
EXPECT_EQ(kContent, text);
// HTTP resource not found the first time.
EXPECT_EQ(1, http_cache()->cache_misses()->Get());
EXPECT_EQ(0, http_cache()->cache_hits()->Get());
// One lookup for ajax metadata and one for the HTTP response. Neither are
// found.
EXPECT_EQ(2, lru_cache()->num_misses());
EXPECT_EQ(0, lru_cache()->num_hits());
ClearStats();
GoogleString text2;
ResponseHeaders response_headers2;
FetchFromProxy("text.txt", true, &text2, &response_headers2);
EXPECT_EQ(kContent, text2);
// Empty resources are not remembered in this flow, so stats are exactly the
// same on second load.
EXPECT_EQ(1, http_cache()->cache_misses()->Get());
EXPECT_EQ(0, http_cache()->cache_hits()->Get());
EXPECT_EQ(2, lru_cache()->num_misses());
EXPECT_EQ(0, lru_cache()->num_hits());
}
TEST_F(ProxyInterfaceTest, SetCookieNotCached) {
ResponseHeaders headers;
const char kContent[] = "A very compelling article";
SetDefaultLongCacheHeaders(&kContentTypeText, &headers);
headers.Add(HttpAttributes::kSetCookie, "cookie");
headers.ComputeCaching();
SetFetchResponse(AbsolutifyUrl("text.txt"), headers, kContent);
// The first response served by the fetcher has Set-Cookie headers.
GoogleString text;
ResponseHeaders response_headers;
FetchFromProxy("text.txt", true, &text, &response_headers);
EXPECT_STREQ("cookie", response_headers.Lookup1(HttpAttributes::kSetCookie));
EXPECT_EQ(kContent, text);
// One lookup for ajax metadata and one for the HTTP response. Neither are
// found.
EXPECT_EQ(2, lru_cache()->num_misses());
EXPECT_EQ(1, http_cache()->cache_misses()->Get());
EXPECT_EQ(0, lru_cache()->num_hits());
ClearStats();
// The next response that is served from cache does not have any Set-Cookie
// headers.
GoogleString text2;
ResponseHeaders response_headers2;
FetchFromProxy("text.txt", true, &text2, &response_headers2);
EXPECT_EQ(NULL, response_headers2.Lookup1(HttpAttributes::kSetCookie));
EXPECT_EQ(kContent, text2);
// The HTTP response is found but the ajax metadata is not found.
EXPECT_EQ(1, lru_cache()->num_hits());
EXPECT_EQ(1, http_cache()->cache_hits()->Get());
EXPECT_EQ(1, lru_cache()->num_misses());
EXPECT_EQ(0, http_cache()->cache_misses()->Get());
}
TEST_F(ProxyInterfaceTest, SetCookie2NotCached) {
ResponseHeaders headers;
const char kContent[] = "A very compelling article";
SetDefaultLongCacheHeaders(&kContentTypeText, &headers);
headers.Add(HttpAttributes::kSetCookie2, "cookie");
headers.ComputeCaching();
SetFetchResponse(AbsolutifyUrl("text.txt"), headers, kContent);
// The first response served by the fetcher has Set-Cookie headers.
GoogleString text;
ResponseHeaders response_headers;
FetchFromProxy("text.txt", true, &text, &response_headers);
EXPECT_STREQ("cookie", response_headers.Lookup1(HttpAttributes::kSetCookie2));
EXPECT_EQ(kContent, text);
// One lookup for ajax metadata and one for the HTTP response. Neither are
// found.
EXPECT_EQ(2, lru_cache()->num_misses());
EXPECT_EQ(1, http_cache()->cache_misses()->Get());
EXPECT_EQ(0, lru_cache()->num_hits());
ClearStats();
// The next response that is served from cache does not have any Set-Cookie
// headers.
GoogleString text2;
ResponseHeaders response_headers2;
FetchFromProxy("text.txt", true, &text2, &response_headers2);
EXPECT_EQ(NULL, response_headers2.Lookup1(HttpAttributes::kSetCookie2));
EXPECT_EQ(kContent, text2);
// The HTTP response is found but the ajax metadata is not found.
EXPECT_EQ(1, lru_cache()->num_misses());
EXPECT_EQ(0, http_cache()->cache_misses()->Get());
EXPECT_EQ(1, lru_cache()->num_hits());
EXPECT_EQ(1, http_cache()->cache_hits()->Get());
}
TEST_F(ProxyInterfaceTest, NotCachedIfAuthorizedAndNotPublic) {
// We should not cache things which are default cache-control if we
// are sending Authorization:. See RFC 2616, 14.8.
ReflectingTestFetcher reflect;
server_context()->set_default_system_fetcher(&reflect);
RequestHeaders request_headers;
request_headers.Add("Was", "Here");
request_headers.Add(HttpAttributes::kAuthorization, "Secret");
// This will get reflected as well, and hence will determine whether
// cacheable or not.
request_headers.Replace(HttpAttributes::kCacheControl, "max-age=600000");
ResponseHeaders out_headers;
GoogleString out_text;
// Using .txt here so we don't try any AJAX rewriting.
FetchFromProxy("http://test.com/file.txt",
request_headers, true, &out_text, &out_headers);
// We should see the request headers we sent back as the response headers
// as we're using a ReflectingTestFetcher.
EXPECT_STREQ("Here", out_headers.Lookup1("Was"));
// Not cross-domain, so should propagate out header.
EXPECT_TRUE(out_headers.Has(HttpAttributes::kAuthorization));
// Should not have written anything to cache, due to the authorization
// header.
EXPECT_EQ(0, http_cache()->cache_inserts()->Get());
ClearStats();
// Now try again. This time no authorization header, different 'Was'.
request_headers.Replace("Was", "There");
request_headers.RemoveAll(HttpAttributes::kAuthorization);
FetchFromProxy("http://test.com/file.txt",
request_headers, true, &out_text, &out_headers);
// Should get different headers since we should not be cached.
EXPECT_STREQ("There", out_headers.Lookup1("Was"));
EXPECT_FALSE(out_headers.Has(HttpAttributes::kAuthorization));
// And should be a miss per stats.
EXPECT_EQ(0, http_cache()->cache_hits()->Get());
EXPECT_EQ(1, http_cache()->cache_misses()->Get());
mock_scheduler()->AwaitQuiescence();
}
TEST_F(ProxyInterfaceTest, CachedIfAuthorizedAndPublic) {
// This with Cache-Control: public should be cached even if
// we are sending Authorization:. See RFC 2616.
ReflectingTestFetcher reflect;
server_context()->set_default_system_fetcher(&reflect);
RequestHeaders request_headers;
request_headers.Add("Was", "Here");
request_headers.Add(HttpAttributes::kAuthorization, "Secret");
// This will get reflected as well, and hence will determine whether
// cacheable or not.
request_headers.Replace(HttpAttributes::kCacheControl, "max-age=600000");
request_headers.Add(HttpAttributes::kCacheControl, "public"); // unlike above
ResponseHeaders out_headers;
GoogleString out_text;
// Using .txt here so we don't try any AJAX rewriting.
FetchFromProxy("http://test.com/file.txt",
request_headers, true, &out_text, &out_headers);
EXPECT_STREQ("Here", out_headers.Lookup1("Was"));
// Not cross-domain, so should propagate out header.
EXPECT_TRUE(out_headers.Has(HttpAttributes::kAuthorization));
// Should have written the result to the cache, despite the request having
// Authorization: thanks to cache-control: public,
EXPECT_EQ(1, http_cache()->cache_inserts()->Get());
ClearStats();
// Now try again. This time no authorization header, different 'Was'.
request_headers.Replace("Was", "There");
request_headers.RemoveAll(HttpAttributes::kAuthorization);
FetchFromProxy("http://test.com/file.txt",
request_headers, true, &out_text, &out_headers);
// Should get old headers, since original was cacheable.
EXPECT_STREQ("Here", out_headers.Lookup1("Was"));
// ... of course hopefully a real server won't serve secrets on a
// cache-control: public page.
EXPECT_STREQ("Secret", out_headers.Lookup1(HttpAttributes::kAuthorization));
// And should be a hit per stats.
EXPECT_EQ(1, http_cache()->cache_hits()->Get());
EXPECT_EQ(0, http_cache()->cache_misses()->Get());
mock_scheduler()->AwaitQuiescence();
}
TEST_F(ProxyInterfaceTest, ImplicitCachingHeadersForCss) {
ResponseHeaders headers;
const char kContent[] = "A very compelling article";
SetTimeMs(MockTimer::kApr_5_2010_ms);
headers.Add(HttpAttributes::kContentType, kContentTypeCss.mime_type());
headers.SetDate(MockTimer::kApr_5_2010_ms);
headers.SetStatusAndReason(HttpStatus::kOK);
headers.ComputeCaching();
SetFetchResponse(AbsolutifyUrl("text.css"), headers, kContent);
// The first response served by the fetcher has caching headers.
GoogleString text;
ResponseHeaders response_headers;
FetchFromProxy("text.css", true, &text, &response_headers);
EXPECT_STREQ(max_age_300_,
response_headers.Lookup1(HttpAttributes::kCacheControl));
EXPECT_STREQ(start_time_plus_300s_string_,
response_headers.Lookup1(HttpAttributes::kExpires));
EXPECT_STREQ(start_time_string_,
response_headers.Lookup1(HttpAttributes::kDate));
EXPECT_EQ(kContent, text);
// One lookup for ajax metadata, one for the HTTP response and one by the css
// filter which looks up metadata while rewriting. None are found.
EXPECT_EQ(3, lru_cache()->num_misses());
EXPECT_EQ(1, http_cache()->cache_misses()->Get());
EXPECT_EQ(0, lru_cache()->num_hits());
ClearStats();
// Fetch again from cache. It has the same caching headers.
text.clear();
response_headers.Clear();
FetchFromProxy("text.css", true, &text, &response_headers);
EXPECT_STREQ(max_age_300_,
response_headers.Lookup1(HttpAttributes::kCacheControl));
EXPECT_STREQ(start_time_plus_300s_string_,
response_headers.Lookup1(HttpAttributes::kExpires));
EXPECT_STREQ(start_time_string_,
response_headers.Lookup1(HttpAttributes::kDate));
EXPECT_EQ(kContent, text);
// One hit for ajax metadata and one for the HTTP response.
EXPECT_EQ(2, lru_cache()->num_hits());
EXPECT_EQ(1, http_cache()->cache_hits()->Get());
EXPECT_EQ(0, lru_cache()->num_misses());
}
TEST_F(ProxyInterfaceTest, MinCacheTtl) {
RewriteOptions* options = server_context()->global_options();
options->ClearSignatureForTesting();
options->set_min_cache_ttl_ms(600 * Timer::kSecondMs);
server_context()->ComputeSignature(options);
ResponseHeaders headers;
const char kContent[] = "A very compelling article";
SetTimeMs(MockTimer::kApr_5_2010_ms);
headers.Add(HttpAttributes::kContentType, kContentTypeCss.mime_type());
headers.SetStatusAndReason(HttpStatus::kOK);
headers.SetDateAndCaching(MockTimer::kApr_5_2010_ms, 300 * Timer::kSecondMs);
SetFetchResponse(AbsolutifyUrl("text.css"), headers, kContent);
GoogleString text;
ResponseHeaders response_headers;
FetchFromProxy("text.css", true, &text, &response_headers);
GoogleString expiry;
ConvertTimeToString(MockTimer::kApr_5_2010_ms + 600 * Timer::kSecondMs,
&expiry);
EXPECT_STREQ("max-age=600",
response_headers.Lookup1(HttpAttributes::kCacheControl));
EXPECT_STREQ(expiry, response_headers.Lookup1(HttpAttributes::kExpires));
EXPECT_STREQ(start_time_string_,
response_headers.Lookup1(HttpAttributes::kDate));
EXPECT_EQ(kContent, text);
// One lookup for ajax metadata, one for the HTTP response and one by the css
// filter which looks up metadata while rewriting. None are found.
EXPECT_EQ(3, lru_cache()->num_misses());
EXPECT_EQ(1, http_cache()->cache_misses()->Get());
EXPECT_EQ(0, lru_cache()->num_hits());
ClearStats();
// Change the origin content and advance time. Fetch again from cache.
SetFetchResponse(AbsolutifyUrl("text.css"), headers, "new");
text.clear();
response_headers.Clear();
AdvanceTimeMs(400 * 1000);
// Even though the max age set on the resource of 300 seconds, since the
// min caching is set to 600 seconds, we should get a cache hit. And since
// the content is fetched from cache, the old content is returned.
FetchFromProxy("text.css", true, &text, &response_headers);
ConvertTimeToString(timer()->NowMs() + 600 * Timer::kSecondMs, &expiry);
EXPECT_STREQ("max-age=600",
response_headers.Lookup1(HttpAttributes::kCacheControl));
EXPECT_STREQ(expiry,
response_headers.Lookup1(HttpAttributes::kExpires));
EXPECT_EQ(kContent, text);
// One hit for ajax metadata and one for the HTTP response.
EXPECT_EQ(2, lru_cache()->num_hits());
EXPECT_EQ(1, http_cache()->cache_hits()->Get());
EXPECT_EQ(0, lru_cache()->num_misses());
ClearStats();
// Advance time past min cache ttl and fetch again. This time the new content
// should be fetched.
text.clear();
response_headers.Clear();
AdvanceTimeMs(400 * 1000);
FetchFromProxy("text.css", true, &text, &response_headers);
ConvertTimeToString(timer()->NowMs() + 600 * Timer::kSecondMs, &expiry);
EXPECT_STREQ("max-age=600",
response_headers.Lookup1(HttpAttributes::kCacheControl));
EXPECT_STREQ(expiry,
response_headers.Lookup1(HttpAttributes::kExpires));
EXPECT_EQ("new", text);
// 1. ajax metadata 2. text.css 3. cf filter metadata lookup.
EXPECT_EQ(3, lru_cache()->num_hits());
// http cache miss for text.css as it has expired.
EXPECT_EQ(1, http_cache()->cache_misses()->Get());
EXPECT_EQ(0, lru_cache()->num_misses());
}
TEST_F(ProxyInterfaceTest, CacheableSize) {
// Test to check that we are not caching responses which have content length >
// max_cacheable_response_content_length.
ResponseHeaders headers;
const char kContent[] = "A very compelling article";
SetTimeMs(MockTimer::kApr_5_2010_ms);
headers.Add(HttpAttributes::kContentType, kContentTypeHtml.mime_type());
headers.SetStatusAndReason(HttpStatus::kOK);
headers.SetDateAndCaching(MockTimer::kApr_5_2010_ms, 300 * Timer::kSecondMs);
headers.ComputeCaching();
SetFetchResponse(AbsolutifyUrl("text.html"), headers, kContent);
// Set the set_max_cacheable_response_content_length to 10 bytes.
http_cache()->set_max_cacheable_response_content_length(10);
// Fetch once.
GoogleString text;
ResponseHeaders response_headers;
FetchFromProxy("text.html", true, &text, &response_headers);
// One lookup for ajax metadata, one for the HTTP response and one for the
// property cache entry. None are found.
EXPECT_EQ(3, lru_cache()->num_misses());
EXPECT_EQ(1, http_cache()->cache_misses()->Get());
EXPECT_EQ(0, lru_cache()->num_hits());
EXPECT_EQ(0, lru_cache()->num_inserts());
// Fetch again. It has the same caching headers.
ClearStats();
text.clear();
response_headers.Clear();
FetchFromProxy("text.html", true, &text, &response_headers);
// None are found as the size is bigger than
// max_cacheable_response_content_length.
EXPECT_EQ(3, lru_cache()->num_misses());
EXPECT_EQ(1, http_cache()->cache_misses()->Get());
EXPECT_EQ(0, lru_cache()->num_hits());
// Set the set_max_cacheable_response_content_length to 1024 bytes.
http_cache()->set_max_cacheable_response_content_length(1024);
ClearStats();
text.clear();
response_headers.Clear();
FetchFromProxy("text.html", true, &text, &response_headers);
// None are found.
EXPECT_EQ(3, lru_cache()->num_misses());
EXPECT_EQ(1, http_cache()->cache_misses()->Get());
EXPECT_EQ(0, lru_cache()->num_hits());
EXPECT_EQ(1, lru_cache()->num_inserts());
// Fetch again.
ClearStats();
text.clear();
response_headers.Clear();
FetchFromProxy("text.html", true, &text, &response_headers);
// One hit for the HTTP response as content is smaller than
// max_cacheable_response_content_length.
EXPECT_EQ(1, lru_cache()->num_hits());
EXPECT_EQ(1, http_cache()->cache_hits()->Get());
EXPECT_EQ(2, lru_cache()->num_misses());
}
TEST_F(ProxyInterfaceTest, CacheableSizeAjax) {
// Test to check that we are not caching responses which have content length >
// max_cacheable_response_content_length in Ajax flow.
ResponseHeaders headers;
SetTimeMs(MockTimer::kApr_5_2010_ms);
headers.Add(HttpAttributes::kContentType, kContentTypeCss.mime_type());
headers.SetDate(MockTimer::kApr_5_2010_ms);
headers.SetStatusAndReason(HttpStatus::kOK);
headers.ComputeCaching();
SetFetchResponse(AbsolutifyUrl("text.css"), headers, kCssContent);
http_cache()->set_max_cacheable_response_content_length(0);
// The first response served by the fetcher and is not rewritten. An ajax
// rewrite should not be triggered as the content length is greater than
// max_cacheable_response_content_length.
GoogleString text;
ResponseHeaders response_headers;
FetchFromProxy("text.css", true, &text, &response_headers);
EXPECT_EQ(kCssContent, text);
// One lookup for ajax metadata, one for the HTTP response. None are found.
EXPECT_EQ(2, lru_cache()->num_misses());
EXPECT_EQ(1, http_cache()->cache_misses()->Get());
EXPECT_EQ(0, lru_cache()->num_hits());
EXPECT_EQ(0, lru_cache()->num_inserts());
ClearStats();
// Fetch again. Optimized version is not served.
text.clear();
response_headers.Clear();
FetchFromProxy("text.css", true, &text, &response_headers);
EXPECT_EQ(kCssContent, text);
// None are found.
EXPECT_EQ(2, lru_cache()->num_misses());
EXPECT_EQ(1, http_cache()->cache_misses()->Get());
EXPECT_EQ(0, lru_cache()->num_hits());
}
TEST_F(ProxyInterfaceTest, CacheableSizeResource) {
// Test to check that we are not caching responses which have content length >
// max_cacheable_response_content_length in resource flow.
GoogleString text;
RequestHeaders request_headers;
ResponseHeaders headers;
// Fetching of a rewritten resource we did not just create
// after an HTML rewrite.
SetResponseWithDefaultHeaders("a.css", kContentTypeCss, kCssContent,
kHtmlCacheTimeSec * 2);
// Set the set_max_cacheable_response_content_length to 0 bytes.
http_cache()->set_max_cacheable_response_content_length(0);
// Fetch fails as original is not accessible.
FetchFromProxy(
Encode("", "cf", "0", "a.css", "css"),
request_headers,
false, /* expect_success */
&text,
&headers,
false /* proxy_fetch_property_callback_collector_created */);
}
TEST_F(ProxyInterfaceTest, InvalidationForCacheableHtml) {
ResponseHeaders headers;
const char kContent[] = "A very compelling article";
SetTimeMs(MockTimer::kApr_5_2010_ms);
headers.Add(HttpAttributes::kContentType, kContentTypeHtml.mime_type());
headers.SetDate(MockTimer::kApr_5_2010_ms);
headers.SetStatusAndReason(HttpStatus::kOK);
headers.SetDateAndCaching(MockTimer::kApr_5_2010_ms, 300 * Timer::kSecondMs);
headers.ComputeCaching();
SetFetchResponse(AbsolutifyUrl("text.html"), headers, kContent);
GoogleString text;
ResponseHeaders response_headers;
FetchFromProxy("text.html", true, &text, &response_headers);
EXPECT_STREQ(max_age_300_,
response_headers.Lookup1(HttpAttributes::kCacheControl));
EXPECT_STREQ(start_time_plus_300s_string_,
response_headers.Lookup1(HttpAttributes::kExpires));
EXPECT_STREQ(start_time_string_,
response_headers.Lookup1(HttpAttributes::kDate));
EXPECT_EQ(kContent, text);
// One lookup for ajax metadata, one for the HTTP response and one for the
// property cache entry. None are found.
EXPECT_EQ(3, lru_cache()->num_misses());
EXPECT_EQ(1, http_cache()->cache_misses()->Get());
EXPECT_EQ(0, lru_cache()->num_hits());
ClearStats();
// Fetch again from cache. It has the same caching headers.
text.clear();
response_headers.Clear();
FetchFromProxy("text.html", true, &text, &response_headers);
EXPECT_STREQ(max_age_300_,
response_headers.Lookup1(HttpAttributes::kCacheControl));
EXPECT_STREQ(start_time_plus_300s_string_,
response_headers.Lookup1(HttpAttributes::kExpires));
EXPECT_STREQ(start_time_string_,
response_headers.Lookup1(HttpAttributes::kDate));
EXPECT_EQ(kContent, text);
// One hit for the HTTP response. Misses for the property cache entry and the
// ajax metadata.
EXPECT_EQ(1, lru_cache()->num_hits());
EXPECT_EQ(1, http_cache()->cache_hits()->Get());
EXPECT_EQ(2, lru_cache()->num_misses());
// Change the response.
SetFetchResponse(AbsolutifyUrl("text.html"), headers, "new");
ClearStats();
// Fetch again from cache. It has the same caching headers.
text.clear();
response_headers.Clear();
FetchFromProxy("text.html", true, &text, &response_headers);
EXPECT_STREQ(max_age_300_,
response_headers.Lookup1(HttpAttributes::kCacheControl));
EXPECT_STREQ(start_time_plus_300s_string_,
response_headers.Lookup1(HttpAttributes::kExpires));
EXPECT_STREQ(start_time_string_,
response_headers.Lookup1(HttpAttributes::kDate));
// We continue to serve the previous response since we've cached it.
EXPECT_EQ(kContent, text);
// One hit for the HTTP response. Misses for the property cache entry and the
// ajax metadata.
EXPECT_EQ(1, lru_cache()->num_hits());
EXPECT_EQ(1, http_cache()->cache_hits()->Get());
EXPECT_EQ(2, lru_cache()->num_misses());
// Invalidate the cache.
scoped_ptr<RewriteOptions> custom_options(
server_context()->global_options()->Clone());
custom_options->UpdateCacheInvalidationTimestampMs(timer()->NowMs());
SetRewriteOptions(custom_options.get());
ClearStats();
text.clear();
response_headers.Clear();
FetchFromProxy("text.html", true, &text, &response_headers);
EXPECT_STREQ(max_age_300_,
response_headers.Lookup1(HttpAttributes::kCacheControl));
EXPECT_STREQ(start_time_plus_300s_string_,
response_headers.Lookup1(HttpAttributes::kExpires));
EXPECT_STREQ(start_time_string_,
response_headers.Lookup1(HttpAttributes::kDate));
// We get the new response since we've invalidated the cache.
EXPECT_EQ("new", text);
// The HTTP response is found in the LRU cache but counts as a miss in the
// HTTPCache since it has been invalidated. Also, cache misses for the ajax
// metadata and property cache entry.
EXPECT_EQ(1, lru_cache()->num_hits());
EXPECT_EQ(0, http_cache()->cache_hits()->Get());
EXPECT_EQ(1, http_cache()->cache_misses()->Get());
EXPECT_EQ(2, lru_cache()->num_misses());
}
TEST_F(ProxyInterfaceTest, UrlInvalidationForCacheableHtml) {
ResponseHeaders headers;
const char kContent[] = "A very compelling article";
SetTimeMs(MockTimer::kApr_5_2010_ms);
headers.Add(HttpAttributes::kContentType, kContentTypeHtml.mime_type());
headers.SetStatusAndReason(HttpStatus::kOK);
headers.SetDateAndCaching(MockTimer::kApr_5_2010_ms, 300 * Timer::kSecondMs);
headers.ComputeCaching();
SetFetchResponse(AbsolutifyUrl("text.html"), headers, kContent);
GoogleString text;
ResponseHeaders response_headers;
FetchFromProxy("text.html", true, &text, &response_headers);
EXPECT_STREQ(max_age_300_,
response_headers.Lookup1(HttpAttributes::kCacheControl));
EXPECT_STREQ(start_time_plus_300s_string_,
response_headers.Lookup1(HttpAttributes::kExpires));
EXPECT_STREQ(start_time_string_,
response_headers.Lookup1(HttpAttributes::kDate));
EXPECT_EQ(kContent, text);
// One lookup for ajax metadata, one for the HTTP response and one for the
// property cache entry. None are found.
EXPECT_EQ(3, lru_cache()->num_misses());
EXPECT_EQ(1, http_cache()->cache_misses()->Get());
EXPECT_EQ(0, lru_cache()->num_hits());
ClearStats();
// Fetch again from cache. It has the same caching headers.
text.clear();
response_headers.Clear();
FetchFromProxy("text.html", true, &text, &response_headers);
EXPECT_STREQ(max_age_300_,
response_headers.Lookup1(HttpAttributes::kCacheControl));
EXPECT_STREQ(start_time_plus_300s_string_,
response_headers.Lookup1(HttpAttributes::kExpires));
EXPECT_STREQ(start_time_string_,
response_headers.Lookup1(HttpAttributes::kDate));
EXPECT_EQ(kContent, text);
// One hit for the HTTP response. Misses for the property cache entry and the
// ajax metadata.
EXPECT_EQ(1, lru_cache()->num_hits());
EXPECT_EQ(1, http_cache()->cache_hits()->Get());
EXPECT_EQ(2, lru_cache()->num_misses());
// Change the response.
SetFetchResponse(AbsolutifyUrl("text.html"), headers, "new");
ClearStats();
// Fetch again from cache. It has the same caching headers.
text.clear();
response_headers.Clear();
FetchFromProxy("text.html", true, &text, &response_headers);
EXPECT_STREQ(max_age_300_,
response_headers.Lookup1(HttpAttributes::kCacheControl));
EXPECT_STREQ(start_time_plus_300s_string_,
response_headers.Lookup1(HttpAttributes::kExpires));
EXPECT_STREQ(start_time_string_,
response_headers.Lookup1(HttpAttributes::kDate));
// We continue to serve the previous response since we've cached it.
EXPECT_EQ(kContent, text);
// One hit for the HTTP response. Misses for the property cache entry and the
// ajax metadata.
EXPECT_EQ(1, lru_cache()->num_hits());
EXPECT_EQ(1, http_cache()->cache_hits()->Get());
EXPECT_EQ(2, lru_cache()->num_misses());
// Invalidate the cache for some URL other than 'text.html'.
scoped_ptr<RewriteOptions> custom_options_1(
server_context()->global_options()->Clone());
custom_options_1->AddUrlCacheInvalidationEntry(
AbsolutifyUrl("foo.bar"), timer()->NowMs(), true);
SetRewriteOptions(custom_options_1.get());
ClearStats();
text.clear();
response_headers.Clear();
FetchFromProxy("text.html", true, &text, &response_headers);
EXPECT_STREQ(max_age_300_,
response_headers.Lookup1(HttpAttributes::kCacheControl));
EXPECT_STREQ(start_time_plus_300s_string_,
response_headers.Lookup1(HttpAttributes::kExpires));
EXPECT_STREQ(start_time_string_,
response_headers.Lookup1(HttpAttributes::kDate));
// We continue to serve the previous response since we've cached it.
EXPECT_EQ(kContent, text);
// One hit for the HTTP response. Misses for the property cache entry and the
// ajax metadata.
EXPECT_EQ(1, lru_cache()->num_hits());
EXPECT_EQ(1, http_cache()->cache_hits()->Get());
EXPECT_EQ(2, lru_cache()->num_misses());
// Invalidate the cache.
scoped_ptr<RewriteOptions> custom_options_2(
server_context()->global_options()->Clone());
// Strictness of URL cache invalidation entry (last argument below) does not
// matter in this test since there is nothing cached in metadata or property
// caches.
custom_options_2->AddUrlCacheInvalidationEntry(
AbsolutifyUrl("text.html"), timer()->NowMs(), true);
SetRewriteOptions(custom_options_2.get());
ClearStats();
text.clear();
response_headers.Clear();
FetchFromProxy("text.html", true, &text, &response_headers);
EXPECT_STREQ(max_age_300_,
response_headers.Lookup1(HttpAttributes::kCacheControl));
EXPECT_STREQ(start_time_plus_300s_string_,
response_headers.Lookup1(HttpAttributes::kExpires));
EXPECT_STREQ(start_time_string_,
response_headers.Lookup1(HttpAttributes::kDate));
// We get the new response since we've invalidated the cache.
EXPECT_EQ("new", text);
// The HTTP response is found in the LRU cache but counts as a miss in the
// HTTPCache since it has been invalidated. Also, cache misses for the ajax
// metadata and property cache entry.
EXPECT_EQ(1, lru_cache()->num_hits());
EXPECT_EQ(0, http_cache()->cache_hits()->Get());
EXPECT_EQ(1, http_cache()->cache_misses()->Get());
EXPECT_EQ(2, lru_cache()->num_misses());
}
TEST_F(ProxyInterfaceTest, NoImplicitCachingHeadersForHtml) {
ResponseHeaders headers;
const char kContent[] = "A very compelling article";
headers.Add(HttpAttributes::kContentType, kContentTypeHtml.mime_type());
SetTimeMs(MockTimer::kApr_5_2010_ms);
headers.SetDate(MockTimer::kApr_5_2010_ms);
headers.SetStatusAndReason(HttpStatus::kOK);
headers.ComputeCaching();
SetFetchResponse(AbsolutifyUrl("text.html"), headers, kContent);
// The first response served by the fetcher does not have implicit caching
// headers.
GoogleString text;
ResponseHeaders response_headers;
FetchFromProxy("text.html", true, &text, &response_headers);
EXPECT_STREQ(NULL, response_headers.Lookup1(HttpAttributes::kCacheControl));
EXPECT_STREQ(start_time_string_,
response_headers.Lookup1(HttpAttributes::kDate));
EXPECT_EQ(kContent, text);
// Lookups for: (1) ajax metadata (2) HTTP response (3) Property cache.
// None are found.
EXPECT_EQ(3, lru_cache()->num_misses());
EXPECT_EQ(1, http_cache()->cache_misses()->Get());
EXPECT_EQ(0, lru_cache()->num_hits());
ClearStats();
// Fetch again. Not found in cache.
text.clear();
response_headers.Clear();
FetchFromProxy("text.html", true, &text, &response_headers);
EXPECT_EQ(NULL, response_headers.Lookup1(HttpAttributes::kCacheControl));
EXPECT_STREQ(start_time_string_,
response_headers.Lookup1(HttpAttributes::kDate));
EXPECT_EQ(kContent, text);
// Lookups for: (1) ajax metadata (2) HTTP response (3) Property cache.
// None are found.
EXPECT_EQ(3, lru_cache()->num_misses());
EXPECT_EQ(1, http_cache()->cache_misses()->Get());
EXPECT_EQ(0, lru_cache()->num_hits());
}
TEST_F(ProxyInterfaceTest, ModifiedImplicitCachingHeadersForCss) {
RewriteOptions* options = server_context()->global_options();
options->ClearSignatureForTesting();
options->set_implicit_cache_ttl_ms(500 * Timer::kSecondMs);
server_context()->ComputeSignature(options);
ResponseHeaders headers;
const char kContent[] = "A very compelling article";
SetTimeMs(MockTimer::kApr_5_2010_ms);
headers.Add(HttpAttributes::kContentType, kContentTypeCss.mime_type());
headers.SetStatusAndReason(HttpStatus::kOK);
// Do not call ComputeCaching before calling SetFetchResponse because it will
// add an explicit max-age=300 cache control header. We do not want that
// header in this test.
SetFetchResponse(AbsolutifyUrl("text.css"), headers, kContent);
// The first response served by the fetcher has caching headers.
GoogleString text;
ResponseHeaders response_headers;
FetchFromProxy("text.css", true, &text, &response_headers);
GoogleString max_age_500 = "max-age=500";
GoogleString start_time_plus_500s_string;
ConvertTimeToString(MockTimer::kApr_5_2010_ms + 500 * Timer::kSecondMs,
&start_time_plus_500s_string);
EXPECT_STREQ(max_age_500,
response_headers.Lookup1(HttpAttributes::kCacheControl));
EXPECT_STREQ(start_time_plus_500s_string,
response_headers.Lookup1(HttpAttributes::kExpires));
EXPECT_STREQ(start_time_string_,
response_headers.Lookup1(HttpAttributes::kDate));
EXPECT_EQ(kContent, text);
// One lookup for ajax metadata, one for the HTTP response and one by the css
// filter which looks up metadata while rewriting. None are found.
EXPECT_EQ(3, lru_cache()->num_misses());
EXPECT_EQ(1, http_cache()->cache_misses()->Get());
EXPECT_EQ(0, lru_cache()->num_hits());
ClearStats();
// Fetch again from cache. It has the same caching headers.
text.clear();
response_headers.Clear();
FetchFromProxy("text.css", true, &text, &response_headers);
EXPECT_STREQ(max_age_500,
response_headers.Lookup1(HttpAttributes::kCacheControl));
EXPECT_STREQ(start_time_plus_500s_string,
response_headers.Lookup1(HttpAttributes::kExpires));
EXPECT_STREQ(start_time_string_,
response_headers.Lookup1(HttpAttributes::kDate));
EXPECT_EQ(kContent, text);
// One hit for ajax metadata and one for the HTTP response.
EXPECT_EQ(2, lru_cache()->num_hits());
EXPECT_EQ(1, http_cache()->cache_hits()->Get());
EXPECT_EQ(0, lru_cache()->num_misses());
}
TEST_F(ProxyInterfaceTest, EtagsAddedWhenAbsent) {
ResponseHeaders headers;
const char kContent[] = "A very compelling article";
SetDefaultLongCacheHeaders(&kContentTypeText, &headers);
headers.RemoveAll(HttpAttributes::kEtag);
headers.ComputeCaching();
SetFetchResponse(AbsolutifyUrl("text.txt"), headers, kContent);
// The first response served by the fetcher has no Etag in the response.
GoogleString text;
ResponseHeaders response_headers;
FetchFromProxy("text.txt", true, &text, &response_headers);
EXPECT_EQ(HttpStatus::kOK, response_headers.status_code());
EXPECT_EQ(NULL, response_headers.Lookup1(HttpAttributes::kEtag));
EXPECT_EQ(kContent, text);
// One lookup for ajax metadata and one for the HTTP response. Neither are
// found.
EXPECT_EQ(2, lru_cache()->num_misses());
EXPECT_EQ(1, http_cache()->cache_misses()->Get());
EXPECT_EQ(0, lru_cache()->num_hits());
ClearStats();
// An Etag is added before writing to cache. The next response is served from
// cache and has an Etag.
GoogleString text2;
ResponseHeaders response_headers2;
FetchFromProxy("text.txt", true, &text2, &response_headers2);
EXPECT_EQ(HttpStatus::kOK, response_headers2.status_code());
EXPECT_STREQ(kEtag0, response_headers2.Lookup1(HttpAttributes::kEtag));
EXPECT_EQ(kContent, text2);
// One lookup for ajax metadata and one for the HTTP response. The metadata is
// not found but the HTTP response is found.
EXPECT_EQ(1, lru_cache()->num_misses());
EXPECT_EQ(0, http_cache()->cache_misses()->Get());
EXPECT_EQ(1, lru_cache()->num_hits());
EXPECT_EQ(1, http_cache()->cache_hits()->Get());
ClearStats();
// The Etag matches and a 304 is served out.
GoogleString text3;
ResponseHeaders response_headers3;
RequestHeaders request_headers;
request_headers.Add(HttpAttributes::kIfNoneMatch, kEtag0);
FetchFromProxy("text.txt", request_headers, true, &text3, &response_headers3);
EXPECT_EQ(HttpStatus::kNotModified, response_headers3.status_code());
EXPECT_STREQ(NULL, response_headers3.Lookup1(HttpAttributes::kEtag));
EXPECT_EQ("", text3);
// One lookup for ajax metadata and one for the HTTP response. The metadata is
// not found but the HTTP response is found.
EXPECT_EQ(1, lru_cache()->num_misses());
EXPECT_EQ(0, http_cache()->cache_misses()->Get());
EXPECT_EQ(1, lru_cache()->num_hits());
EXPECT_EQ(1, http_cache()->cache_hits()->Get());
}
TEST_F(ProxyInterfaceTest, EtagMatching) {
ResponseHeaders headers;
const char kContent[] = "A very compelling article";
SetDefaultLongCacheHeaders(&kContentTypeText, &headers);
headers.Replace(HttpAttributes::kEtag, "etag");
headers.ComputeCaching();
SetFetchResponse(AbsolutifyUrl("text.txt"), headers, kContent);
// The first response served by the fetcher has an Etag in the response.
GoogleString text;
ResponseHeaders response_headers;
FetchFromProxy("text.txt", true, &text, &response_headers);
EXPECT_EQ(HttpStatus::kOK, response_headers.status_code());
EXPECT_STREQ("etag", response_headers.Lookup1(HttpAttributes::kEtag));
EXPECT_EQ(kContent, text);
// One lookup for ajax metadata and one for the HTTP response. Neither are
// found.
EXPECT_EQ(2, lru_cache()->num_misses());
EXPECT_EQ(1, http_cache()->cache_misses()->Get());
EXPECT_EQ(0, lru_cache()->num_hits());
EXPECT_EQ(0, http_cache()->cache_hits()->Get());
ClearStats();
// The next response is served from cache.
GoogleString text2;
ResponseHeaders response_headers2;
FetchFromProxy("text.txt", true, &text2, &response_headers2);
EXPECT_EQ(HttpStatus::kOK, response_headers2.status_code());
EXPECT_STREQ("etag", response_headers2.Lookup1(HttpAttributes::kEtag));
EXPECT_EQ(kContent, text2);
// One lookup for ajax metadata and one for the HTTP response. The metadata is
// not found but the HTTP response is found.
EXPECT_EQ(1, lru_cache()->num_misses());
EXPECT_EQ(0, http_cache()->cache_misses()->Get());
EXPECT_EQ(1, lru_cache()->num_hits());
EXPECT_EQ(1, http_cache()->cache_hits()->Get());
ClearStats();
// The Etag matches and a 304 is served out.
GoogleString text3;
ResponseHeaders response_headers3;
RequestHeaders request_headers;
request_headers.Add(HttpAttributes::kIfNoneMatch, "etag");
FetchFromProxy("text.txt", request_headers, true, &text3, &response_headers3);
EXPECT_EQ(HttpStatus::kNotModified, response_headers3.status_code());
EXPECT_STREQ(NULL, response_headers3.Lookup1(HttpAttributes::kEtag));
EXPECT_EQ("", text3);
// One lookup for ajax metadata and one for the HTTP response. The metadata is
// not found but the HTTP response is found.
EXPECT_EQ(1, lru_cache()->num_misses());
EXPECT_EQ(0, http_cache()->cache_misses()->Get());
EXPECT_EQ(1, lru_cache()->num_hits());
EXPECT_EQ(1, http_cache()->cache_hits()->Get());
ClearStats();
// The Etag doesn't match and the full response is returned.
GoogleString text4;
ResponseHeaders response_headers4;
request_headers.Replace(HttpAttributes::kIfNoneMatch, "mismatch");
FetchFromProxy("text.txt", request_headers, true, &text4, &response_headers4);
EXPECT_EQ(HttpStatus::kOK, response_headers4.status_code());
EXPECT_STREQ("etag", response_headers4.Lookup1(HttpAttributes::kEtag));
EXPECT_EQ(kContent, text4);
// One lookup for ajax metadata and one for the HTTP response. The metadata is
// not found but the HTTP response is found.
EXPECT_EQ(1, lru_cache()->num_misses());
EXPECT_EQ(0, http_cache()->cache_misses()->Get());
EXPECT_EQ(1, lru_cache()->num_hits());
EXPECT_EQ(1, http_cache()->cache_hits()->Get());
}
TEST_F(ProxyInterfaceTest, LastModifiedMatch) {
ResponseHeaders headers;
const char kContent[] = "A very compelling article";
SetDefaultLongCacheHeaders(&kContentTypeText, &headers);
headers.SetLastModified(MockTimer::kApr_5_2010_ms);
headers.ComputeCaching();
SetFetchResponse(AbsolutifyUrl("text.txt"), headers, kContent);
// The first response served by the fetcher has an Etag in the response.
GoogleString text;
ResponseHeaders response_headers;
FetchFromProxy("text.txt", true, &text, &response_headers);
EXPECT_EQ(HttpStatus::kOK, response_headers.status_code());
EXPECT_STREQ(start_time_string_,
response_headers.Lookup1(HttpAttributes::kLastModified));
EXPECT_EQ(kContent, text);
// One lookup for ajax metadata and one for the HTTP response. Neither are
// found.
EXPECT_EQ(2, lru_cache()->num_misses());
EXPECT_EQ(1, http_cache()->cache_misses()->Get());
EXPECT_EQ(0, lru_cache()->num_hits());
EXPECT_EQ(0, http_cache()->cache_hits()->Get());
ClearStats();
// The next response is served from cache.
GoogleString text2;
ResponseHeaders response_headers2;
FetchFromProxy("text.txt", true, &text2, &response_headers2);
EXPECT_EQ(HttpStatus::kOK, response_headers2.status_code());
EXPECT_STREQ(start_time_string_,
response_headers2.Lookup1(HttpAttributes::kLastModified));
EXPECT_EQ(kContent, text2);
// One lookup for ajax metadata and one for the HTTP response. The metadata is
// not found but the HTTP response is found.
EXPECT_EQ(1, lru_cache()->num_misses());
EXPECT_EQ(0, http_cache()->cache_misses()->Get());
EXPECT_EQ(1, lru_cache()->num_hits());
EXPECT_EQ(1, http_cache()->cache_hits()->Get());
ClearStats();
// The last modified timestamp matches and a 304 is served out.
GoogleString text3;
ResponseHeaders response_headers3;
RequestHeaders request_headers;
request_headers.Add(HttpAttributes::kIfModifiedSince, start_time_string_);
FetchFromProxy("text.txt", request_headers, true, &text3, &response_headers3);
EXPECT_EQ(HttpStatus::kNotModified, response_headers3.status_code());
EXPECT_STREQ(NULL, response_headers3.Lookup1(HttpAttributes::kLastModified));
EXPECT_EQ("", text3);
// One lookup for ajax metadata and one for the HTTP response. The metadata is
// not found but the HTTP response is found.
EXPECT_EQ(1, lru_cache()->num_misses());
EXPECT_EQ(0, http_cache()->cache_misses()->Get());
EXPECT_EQ(1, lru_cache()->num_hits());
EXPECT_EQ(1, http_cache()->cache_hits()->Get());
ClearStats();
// The last modified timestamp doesn't match and the full response is
// returned.
GoogleString text4;
ResponseHeaders response_headers4;
request_headers.Replace(HttpAttributes::kIfModifiedSince,
"Fri, 02 Apr 2010 18:51:26 GMT");
FetchFromProxy("text.txt", request_headers, true, &text4, &response_headers4);
EXPECT_EQ(HttpStatus::kOK, response_headers4.status_code());
EXPECT_STREQ(start_time_string_,
response_headers4.Lookup1(HttpAttributes::kLastModified));
EXPECT_EQ(kContent, text4);
// One lookup for ajax metadata and one for the HTTP response. The metadata is
// not found but the HTTP response is found.`
EXPECT_EQ(1, lru_cache()->num_misses());
EXPECT_EQ(0, http_cache()->cache_misses()->Get());
EXPECT_EQ(1, lru_cache()->num_hits());
EXPECT_EQ(1, http_cache()->cache_hits()->Get());
}
TEST_F(ProxyInterfaceTest, AjaxRewritingForCss) {
ResponseHeaders headers;
SetTimeMs(MockTimer::kApr_5_2010_ms);
headers.Add(HttpAttributes::kContentType, kContentTypeCss.mime_type());
headers.SetDate(MockTimer::kApr_5_2010_ms);
headers.SetStatusAndReason(HttpStatus::kOK);
headers.ComputeCaching();
SetFetchResponse(AbsolutifyUrl("text.css"), headers, kCssContent);
// The first response served by the fetcher and is not rewritten. An ajax
// rewrite is triggered.
GoogleString text;
ResponseHeaders response_headers;
FetchFromProxy("text.css", true, &text, &response_headers);
EXPECT_STREQ(max_age_300_,
response_headers.Lookup1(HttpAttributes::kCacheControl));
EXPECT_STREQ(start_time_plus_300s_string_,
response_headers.Lookup1(HttpAttributes::kExpires));
EXPECT_STREQ(start_time_string_,
response_headers.Lookup1(HttpAttributes::kDate));
EXPECT_EQ(kCssContent, text);
CheckBackgroundFetch(response_headers, false);
CheckNumBackgroundFetches(0);
// One lookup for ajax metadata, one for the HTTP response and one by the css
// filter which looks up metadata while rewriting. None are found.
EXPECT_EQ(3, lru_cache()->num_misses());
EXPECT_EQ(1, http_cache()->cache_misses()->Get());
EXPECT_EQ(0, lru_cache()->num_hits());
ClearStats();
// The rewrite is complete and the optimized version is served.
text.clear();
response_headers.Clear();
FetchFromProxy("text.css", true, &text, &response_headers);
EXPECT_STREQ(max_age_300_,
response_headers.Lookup1(HttpAttributes::kCacheControl));
EXPECT_STREQ(start_time_plus_300s_string_,
response_headers.Lookup1(HttpAttributes::kExpires));
EXPECT_STREQ(start_time_string_,
response_headers.Lookup1(HttpAttributes::kDate));
EXPECT_EQ(kMinimizedCssContent, text);
// One hit for ajax metadata and one for the rewritten HTTP response.
EXPECT_EQ(2, lru_cache()->num_hits());
EXPECT_EQ(1, http_cache()->cache_hits()->Get());
EXPECT_EQ(0, lru_cache()->num_misses());
CheckNumBackgroundFetches(0);
ClearStats();
// Advance close to expiry.
AdvanceTimeUs(270 * Timer::kSecondUs);
// The rewrite is complete and the optimized version is served. A freshen is
// triggered to refresh the original CSS file.
text.clear();
response_headers.Clear();
FetchFromProxy("text.css", true, &text, &response_headers);
EXPECT_STREQ("max-age=30",
response_headers.Lookup1(HttpAttributes::kCacheControl));
EXPECT_STREQ(start_time_plus_300s_string_,
response_headers.Lookup1(HttpAttributes::kExpires));
EXPECT_STREQ("Mon, 05 Apr 2010 18:55:56 GMT",
response_headers.Lookup1(HttpAttributes::kDate));
EXPECT_EQ(kMinimizedCssContent, text);
// One hit for ajax metadata, one for the rewritten HTTP response and one for
// the original HTTP response while freshening.
EXPECT_EQ(3, lru_cache()->num_hits());
EXPECT_EQ(1, http_cache()->cache_hits()->Get());
EXPECT_EQ(0, lru_cache()->num_misses());
// One background fetch is triggered while freshening.
CheckNumBackgroundFetches(1);
// Disable ajax rewriting. We now received the response fetched while
// freshening. This response has kBackgroundFetchHeader set to 1.
RewriteOptions* options = server_context()->global_options();
options->ClearSignatureForTesting();
options->set_in_place_rewriting_enabled(false);
server_context()->ComputeSignature(options);
ClearStats();
text.clear();
response_headers.Clear();
FetchFromProxy("text.css", true, &text, &response_headers);
EXPECT_STREQ(max_age_300_,
response_headers.Lookup1(HttpAttributes::kCacheControl));
EXPECT_STREQ("Mon, 05 Apr 2010 19:00:56 GMT",
response_headers.Lookup1(HttpAttributes::kExpires));
EXPECT_STREQ("Mon, 05 Apr 2010 18:55:56 GMT",
response_headers.Lookup1(HttpAttributes::kDate));
EXPECT_EQ(kCssContent, text);
CheckNumBackgroundFetches(0);
CheckBackgroundFetch(response_headers, true);
// Done HTTP cache hit for the original response.
EXPECT_EQ(1, lru_cache()->num_hits());
EXPECT_EQ(1, http_cache()->cache_hits()->Get());
EXPECT_EQ(0, lru_cache()->num_misses());
}
TEST_F(ProxyInterfaceTest, FallbackNoAcceptGzip) {
RewriteOptions* options = server_context()->global_options();
options->ClearSignatureForTesting();
options->set_http_cache_compression_level(9);
options->set_serve_stale_while_revalidate_threshold_sec(
1000 * Timer::kDayMs);
server_context()->ComputeSignature(options);
ResponseHeaders headers;
SetTimeMs(MockTimer::kApr_5_2010_ms);
headers.Add(HttpAttributes::kContentType, kContentTypeCss.mime_type());
headers.SetDate(MockTimer::kApr_5_2010_ms);
headers.SetStatusAndReason(HttpStatus::kOK);
headers.ComputeCaching();
SetFetchResponse(AbsolutifyUrl("text.css"), headers, kCssContent);
// The first response served by the fetcher and is not rewritten. An ajax
// rewrite is triggered.
GoogleString text;
ResponseHeaders response_headers;
FetchFromProxy("text.css", true, &text, &response_headers);
EXPECT_STREQ(max_age_300_,
response_headers.Lookup1(HttpAttributes::kCacheControl));
EXPECT_STREQ(start_time_plus_300s_string_,
response_headers.Lookup1(HttpAttributes::kExpires));
EXPECT_STREQ(start_time_string_,
response_headers.Lookup1(HttpAttributes::kDate));
EXPECT_EQ(kCssContent, text);
CheckBackgroundFetch(response_headers, false);
CheckNumBackgroundFetches(0);
// One lookup for ajax metadata, one for the HTTP response and one by the css
// filter which looks up metadata while rewriting. None are found.
EXPECT_EQ(3, lru_cache()->num_misses());
EXPECT_EQ(1, http_cache()->cache_misses()->Get());
EXPECT_EQ(0, lru_cache()->num_hits());
ClearStats();
// The rewrite is complete and the optimized version is served.
text.clear();
response_headers.Clear();
AdvanceTimeUs(400 * Timer::kSecondUs); // expired
FetchFromProxy("text.css", true, &text, &response_headers);
EXPECT_FALSE(response_headers.IsGzipped());
}
TEST_F(ProxyInterfaceTest, NoAjaxRewritingWhenAuthorizationSent) {
// We should not do ajax rewriting when sending over an authorization
// header if the original isn't cache-control: public.
ResponseHeaders headers;
SetTimeMs(MockTimer::kApr_5_2010_ms);
headers.Add(HttpAttributes::kContentType, kContentTypeCss.mime_type());
headers.SetDate(MockTimer::kApr_5_2010_ms);
headers.SetStatusAndReason(HttpStatus::kOK);
headers.ComputeCaching();
SetFetchResponse(AbsolutifyUrl("text.css"), headers, kCssContent);
// The first response served by the fetcher and is not rewritten. An ajax
// rewrite is triggered.
GoogleString text;
ResponseHeaders response_headers;
RequestHeaders request_headers;
request_headers.Add(HttpAttributes::kAuthorization, "Paperwork");
FetchFromProxy("text.css", request_headers, true, &text, &response_headers);
EXPECT_EQ(kCssContent, text);
// The second version should still be unoptimized, since original wasn't
// cacheable.
text.clear();
response_headers.Clear();
FetchFromProxy("text.css", request_headers, true, &text, &response_headers);
EXPECT_EQ(kCssContent, text);
}
TEST_F(ProxyInterfaceTest, AjaxRewritingWhenAuthorizationButPublic) {
// We should do ajax rewriting when sending over an authorization
// header if the original is cache-control: public.
ResponseHeaders headers;
SetTimeMs(MockTimer::kApr_5_2010_ms);
headers.Add(HttpAttributes::kContentType, kContentTypeCss.mime_type());
headers.SetDate(MockTimer::kApr_5_2010_ms);
headers.SetStatusAndReason(HttpStatus::kOK);
headers.Add(HttpAttributes::kCacheControl, "public, max-age=400");
headers.ComputeCaching();
SetFetchResponse(AbsolutifyUrl("text.css"), headers, kCssContent);
// The first response served by the fetcher and is not rewritten. An ajax
// rewrite is triggered.
GoogleString text;
ResponseHeaders response_headers;
RequestHeaders request_headers;
request_headers.Add(HttpAttributes::kAuthorization, "Paperwork");
FetchFromProxy("text.css", request_headers, true, &text, &response_headers);
EXPECT_EQ(kCssContent, text);
// The second version should be optimized in this case.
text.clear();
response_headers.Clear();
FetchFromProxy("text.css", request_headers, true, &text, &response_headers);
EXPECT_EQ(kMinimizedCssContent, text);
}
TEST_F(ProxyInterfaceTest, AjaxRewritingDisabledByGlobalDisable) {
RewriteOptions* options = server_context()->global_options();
options->ClearSignatureForTesting();
options->set_enabled(RewriteOptions::kEnabledOff);
server_context()->ComputeSignature(options);
SetResponseWithDefaultHeaders("a.css", kContentTypeCss, kCssContent,
kHtmlCacheTimeSec * 2);
GoogleString text;
ResponseHeaders response_headers;
FetchFromProxy("a.css", true, &text, &response_headers);
// First fetch will not get rewritten no matter what.
EXPECT_STREQ(kCssContent, text);
// Second fetch would get minified if ajax rewriting were on; but
// it got disabled by the global toggle.
text.clear();
FetchFromProxy("a.css", true, &text, &response_headers);
EXPECT_STREQ(kCssContent, text);
}
TEST_F(ProxyInterfaceTest, AjaxRewritingSkippedIfBlacklisted) {
ResponseHeaders headers;
SetTimeMs(MockTimer::kApr_5_2010_ms);
headers.Add(HttpAttributes::kContentType, kContentTypeCss.mime_type());
headers.SetDate(MockTimer::kApr_5_2010_ms);
headers.SetStatusAndReason(HttpStatus::kOK);
headers.ComputeCaching();
SetFetchResponse(AbsolutifyUrl("blacklist.css"), headers, kCssContent);
// The first response is served by the fetcher. Since the url is blacklisted,
// no ajax rewriting happens.
GoogleString text;
ResponseHeaders response_headers;
FetchFromProxy("blacklist.css", true, &text, &response_headers);
EXPECT_STREQ(max_age_300_,
response_headers.Lookup1(HttpAttributes::kCacheControl));
EXPECT_STREQ(start_time_plus_300s_string_,
response_headers.Lookup1(HttpAttributes::kExpires));
EXPECT_STREQ(start_time_string_,
response_headers.Lookup1(HttpAttributes::kDate));
EXPECT_EQ(kCssContent, text);
// Since no ajax rewriting happens, there is only a single cache lookup for
// the resource.
EXPECT_EQ(1, lru_cache()->num_misses());
EXPECT_EQ(1, http_cache()->cache_misses()->Get());
EXPECT_EQ(0, lru_cache()->num_hits());
ClearStats();
// The same thing happens on the second request.
text.clear();
response_headers.Clear();
FetchFromProxy("blacklist.css", true, &text, &response_headers);
EXPECT_STREQ(max_age_300_,
response_headers.Lookup1(HttpAttributes::kCacheControl));
EXPECT_STREQ(start_time_plus_300s_string_,
response_headers.Lookup1(HttpAttributes::kExpires));
EXPECT_STREQ(start_time_string_,
response_headers.Lookup1(HttpAttributes::kDate));
EXPECT_EQ(kCssContent, text);
// The resource is found in cache this time.
EXPECT_EQ(0, lru_cache()->num_misses());
EXPECT_EQ(0, http_cache()->cache_misses()->Get());
EXPECT_EQ(1, http_cache()->cache_hits()->Get());
EXPECT_EQ(1, lru_cache()->num_hits());
}
TEST_F(ProxyInterfaceTest, AjaxRewritingBlacklistReject) {
// Makes sure that we honor reject_blacklisted() when ajax rewriting may
// have normally happened.
RejectBlacklisted();
ResponseHeaders headers;
SetTimeMs(MockTimer::kApr_5_2010_ms);
headers.Add(HttpAttributes::kContentType, kContentTypeCss.mime_type());
headers.SetDate(MockTimer::kApr_5_2010_ms);
headers.SetStatusAndReason(HttpStatus::kOK);
headers.ComputeCaching();
SetFetchResponse(AbsolutifyUrl("blacklistCoffee.css"), headers, kCssContent);
SetFetchResponse(AbsolutifyUrl("tea.css"), headers, kCssContent);
GoogleString text;
ResponseHeaders response_headers;
FetchFromProxy("blacklistCoffee.css", true, &text, &response_headers);
EXPECT_EQ(HttpStatus::kImATeapot, response_headers.status_code());
EXPECT_TRUE(text.empty());
// Non-blacklisted stuff works OK.
FetchFromProxy("tea.css", true, &text, &response_headers);
EXPECT_EQ(HttpStatus::kOK, response_headers.status_code());
EXPECT_EQ(kCssContent, text);
}
TEST_F(ProxyInterfaceTest, EatCookiesOnReconstructFailure) {
// Make sure we don't pass through a Set-Cookie[2] when reconstructing
// a resource on demand fails.
GoogleString abs_path = AbsolutifyUrl("a.css");
ResponseHeaders response_headers;
SetDefaultLongCacheHeaders(&kContentTypeCss, &response_headers);
response_headers.Add(HttpAttributes::kSetCookie, "a cookie");
response_headers.Add(HttpAttributes::kSetCookie2, "a weird old-time cookie");
response_headers.ComputeCaching();
SetFetchResponse(abs_path, response_headers, "broken_css{");
ResponseHeaders out_response_headers;
RequestHeaders request_headers;
GoogleString text;
FetchFromProxy(Encode("", "cf", "0", "a.css", "css"),
request_headers,
true, /* expect_success */
&text,
&out_response_headers,
false /* proxy_fetch_property_callback_collector_created */);
EXPECT_EQ(NULL, out_response_headers.Lookup1(HttpAttributes::kSetCookie));
EXPECT_EQ(NULL, out_response_headers.Lookup1(HttpAttributes::kSetCookie2));
}
TEST_F(ProxyInterfaceTest, RewriteHtml) {
GoogleString text;
ResponseHeaders headers;
RewriteOptions* options = server_context()->global_options();
options->ClearSignatureForTesting();
options->SetRewriteLevel(RewriteOptions::kPassThrough);
options->EnableFilter(RewriteOptions::kRewriteCss);
server_context()->ComputeSignature(options);
headers.Add(HttpAttributes::kEtag, "something");
headers.SetDateAndCaching(MockTimer::kApr_5_2010_ms,
kHtmlCacheTimeSec * 2 * Timer::kSecondMs);
headers.SetLastModified(MockTimer::kApr_5_2010_ms);
headers.Add(HttpAttributes::kContentType, kContentTypeHtml.mime_type());
headers.SetStatusAndReason(HttpStatus::kOK);
headers.ComputeCaching();
SetFetchResponse(AbsolutifyUrl(kPageUrl), headers, CssLinkHref("a.css"));
SetResponseWithDefaultHeaders("a.css", kContentTypeCss, kCssContent,
kHtmlCacheTimeSec * 2);
text.clear();
headers.Clear();
FetchFromProxy(kPageUrl, true, &text, &headers);
CheckBackgroundFetch(headers, false);
CheckNumBackgroundFetches(1);
CheckHeaders(headers, kContentTypeHtml);
EXPECT_EQ(CssLinkHref(Encode("", "cf", "0", "a.css", "css")), text);
headers.ComputeCaching();
EXPECT_LE(start_time_ms_ + kHtmlCacheTimeSec * Timer::kSecondMs,
headers.CacheExpirationTimeMs());
EXPECT_EQ(NULL, headers.Lookup1(HttpAttributes::kEtag));
EXPECT_EQ(NULL, headers.Lookup1(HttpAttributes::kLastModified));
EXPECT_STREQ("cf", AppliedRewriterStringFromLog());
// Fetch the rewritten resource as well.
text.clear();
headers.Clear();
ClearStats();
RequestHeaders request_headers;
FetchFromProxy(Encode("", "cf", "0", "a.css", "css"),
request_headers,
true, /* expect_success */
&text,
&headers,
false /* proxy_fetch_property_callback_collector_created */);
CheckHeaders(headers, kContentTypeCss);
// Note that the fetch for the original resource was triggered as a result of
// the initial HTML request. Hence, its headers indicate that it is a
// background request
// This response has kBackgroundFetchHeader set to 1 since a fetch was
// triggered for it in the background while rewriting the original html.
CheckBackgroundFetch(headers, true);
CheckNumBackgroundFetches(0);
headers.ComputeCaching();
EXPECT_LE(start_time_ms_ + Timer::kYearMs, headers.CacheExpirationTimeMs());
EXPECT_EQ(kMinimizedCssContent, text);
}
TEST_F(ProxyInterfaceTest, LogChainedResourceRewrites) {
GoogleString text;
ResponseHeaders headers;
SetResponseWithDefaultHeaders("1.js", kContentTypeJavascript, "var wxyz=1;",
kHtmlCacheTimeSec * 2);
SetResponseWithDefaultHeaders("2.js", kContentTypeJavascript, "var abcd=2;",
kHtmlCacheTimeSec * 2);
GoogleString combined_js_url = Encode(
kTestDomain, "jc", "0",
"1.js.pagespeed.jm.0.jsX2.js.pagespeed.jm.0.js", "js");
combined_js_url[combined_js_url.find('X')] = '+';
RequestHeaders request_headers;
FetchFromProxy(
combined_js_url,
request_headers,
true, /* expect_success */
&text,
&headers,
false /* proxy_fetch_property_callback_collector_created */);
EXPECT_STREQ("jc,jm", AppliedRewriterStringFromLog());
}
TEST_F(ProxyInterfaceTest, FlushHugeHtml) {
// Test the forced flushing of HTML controlled by flush_buffer_limit_bytes().
RewriteOptions* options = server_context()->global_options();
options->ClearSignatureForTesting();
options->set_flush_buffer_limit_bytes(8); // 2 self-closing tags ("<p/>")
options->set_flush_html(true);
options->DisableFilter(RewriteOptions::kAddHead);
rewrite_driver()->AddFilters();
server_context()->ComputeSignature(options);
SetResponseWithDefaultHeaders("page.html", kContentTypeHtml,
"<a/><b/><c/><d/><e/><f/><g/><h/>",
kHtmlCacheTimeSec * 2);
GoogleString out;
FetchFromProxyLoggingFlushes("page.html", true /*success*/, &out);
EXPECT_EQ(
"<a/><b/>|Flush|<c/><d/>|Flush|<e/><f/>|Flush|<g/><h/>|Flush||Flush|",
out);
// Now tell to flush after 3 self-closing tags.
options->ClearSignatureForTesting();
options->set_flush_buffer_limit_bytes(12); // 3 self-closing tags
server_context()->ComputeSignature(options);
FetchFromProxyLoggingFlushes("page.html", true /*success*/, &out);
EXPECT_EQ(
"<a/><b/><c/>|Flush|<d/><e/><f/>|Flush|<g/><h/>|Flush|", out);
// And now with 2.5. This means we will flush 2 (as that many are complete),
// then 5, and 7.
options->ClearSignatureForTesting();
options->set_flush_buffer_limit_bytes(10);
server_context()->ComputeSignature(options);
FetchFromProxyLoggingFlushes("page.html", true /*success*/, &out);
EXPECT_EQ(
"<a/><b/>|Flush|<c/><d/><e/>|Flush|<f/><g/>|Flush|<h/>|Flush|", out);
// Now 9 bytes, e.g. 2 1/4 of a self-closing tag. Looks almost the same as
// every 2 self-closing tags (8 bytes), but we don't get an extra flush
// at the end.
options->ClearSignatureForTesting();
options->set_flush_buffer_limit_bytes(9);
server_context()->ComputeSignature(options);
FetchFromProxyLoggingFlushes("page.html", true /*success*/, &out);
EXPECT_EQ(
"<a/><b/>|Flush|<c/><d/>|Flush|<e/><f/>|Flush|<g/><h/>|Flush|",
out);
}
TEST_F(ProxyInterfaceTest, DontRewriteDisallowedHtml) {
// Blacklisted URL should not be rewritten.
SetResponseWithDefaultHeaders("blacklist.html", kContentTypeHtml,
CssLinkHref("a.css"), kHtmlCacheTimeSec * 2),
SetResponseWithDefaultHeaders("a.css", kContentTypeCss, kCssContent,
kHtmlCacheTimeSec * 2);
GoogleString text;
ResponseHeaders headers;
FetchFromProxy("blacklist.html", true, &text, &headers);
CheckHeaders(headers, kContentTypeHtml);
EXPECT_EQ(CssLinkHref("a.css"), text);
}
TEST_F(ProxyInterfaceTest, DontRewriteDisallowedHtmlRejectMode) {
// If we're in reject_blacklisted mode, we should just respond with the
// configured status.
RejectBlacklisted();
SetResponseWithDefaultHeaders("blacklistCoffee.html", kContentTypeHtml,
CssLinkHref("a.css"), kHtmlCacheTimeSec * 2),
SetResponseWithDefaultHeaders("tea.html", kContentTypeHtml,
"tasty", kHtmlCacheTimeSec * 2);
SetResponseWithDefaultHeaders("a.css", kContentTypeCss, kCssContent,
kHtmlCacheTimeSec * 2);
GoogleString text;
ResponseHeaders headers;
FetchFromProxy("blacklistCoffee.html", true, &text, &headers);
EXPECT_EQ(HttpStatus::kImATeapot, headers.status_code());
EXPECT_TRUE(text.empty());
// Fetching non-blacklisted one works fine.
FetchFromProxy("tea.html", true, &text, &headers);
EXPECT_EQ(HttpStatus::kOK, headers.status_code());
EXPECT_STREQ("tasty", text);
}
TEST_F(ProxyInterfaceTest, DontRewriteMislabeledAsHtml) {
// Make sure we don't rewrite things that claim to be HTML, but aren't.
GoogleString text;
ResponseHeaders headers;
SetResponseWithDefaultHeaders("page.js", kContentTypeHtml,
StrCat("//", CssLinkHref("a.css")),
kHtmlCacheTimeSec * 2);
SetResponseWithDefaultHeaders("a.css", kContentTypeCss, kCssContent,
kHtmlCacheTimeSec * 2);
FetchFromProxy("page.js", true, &text, &headers);
CheckHeaders(headers, kContentTypeHtml);
EXPECT_EQ(StrCat("//", CssLinkHref("a.css")), text);
}
TEST_F(ProxyInterfaceTest, ReconstructResource) {
GoogleString text;
ResponseHeaders headers;
// Fetching of a rewritten resource we did not just create
// after an HTML rewrite.
SetResponseWithDefaultHeaders("a.css", kContentTypeCss, kCssContent,
kHtmlCacheTimeSec * 2);
RequestHeaders request_headers;
FetchFromProxy(Encode("", "cf", "0", "a.css", "css"),
request_headers,
true, /* expect_success */
&text,
&headers,
false /* proxy_fetch_property_callback_collector_created */);
CheckHeaders(headers, kContentTypeCss);
headers.ComputeCaching();
CheckBackgroundFetch(headers, false);
EXPECT_LE(start_time_ms_ + Timer::kYearMs, headers.CacheExpirationTimeMs());
EXPECT_EQ(kMinimizedCssContent, text);
EXPECT_STREQ("cf", AppliedRewriterStringFromLog());
}
TEST_F(ProxyInterfaceTest, ReconstructResourceCustomOptions) {
const char kCssWithEmbeddedImage[] = "*{background-image:url(%s)}";
const char kBackgroundImage[] = "1.png";
GoogleString text;
ResponseHeaders headers;
// We're not going to image-compress so we don't need our mock image
// to really be an image.
SetResponseWithDefaultHeaders(kBackgroundImage, kContentTypePng, "image",
kHtmlCacheTimeSec * 2);
GoogleString orig_css = StringPrintf(kCssWithEmbeddedImage, kBackgroundImage);
SetResponseWithDefaultHeaders("embedded.css", kContentTypeCss,
orig_css, kHtmlCacheTimeSec * 2);
// By default, cache extension is off in the default options.
server_context()->global_options()->SetDefaultRewriteLevel(
RewriteOptions::kPassThrough);
ASSERT_FALSE(options()->Enabled(RewriteOptions::kExtendCacheCss));
ASSERT_FALSE(options()->Enabled(RewriteOptions::kExtendCacheImages));
ASSERT_FALSE(options()->Enabled(RewriteOptions::kExtendCacheScripts));
ASSERT_FALSE(options()->Enabled(RewriteOptions::kExtendCachePdfs));
ASSERT_EQ(RewriteOptions::kPassThrough, options()->level());
// Because cache-extension was turned off, the image in the CSS file
// will not be changed.
RequestHeaders request_headers;
FetchFromProxy("I.embedded.css.pagespeed.cf.0.css",
request_headers,
true, /* expect_success */
&text,
&headers,
false /* proxy_fetch_property_callback_collector_created */);
EXPECT_EQ(orig_css, text);
// Now turn on cache-extension for custom options. Invalidate cache entries
// up to and including the current timestamp and advance by 1ms, otherwise
// the previously stored embedded.css.pagespeed.cf.0.css will get re-used.
scoped_ptr<RewriteOptions> custom_options(factory()->NewRewriteOptions());
custom_options->EnableFilter(RewriteOptions::kExtendCacheCss);
custom_options->EnableFilter(RewriteOptions::kExtendCacheImages);
custom_options->EnableFilter(RewriteOptions::kExtendCacheScripts);
custom_options->EnableFilter(RewriteOptions::kExtendCachePdfs);
custom_options->UpdateCacheInvalidationTimestampMs(timer()->NowMs());
AdvanceTimeUs(Timer::kMsUs);
// Inject the custom options into the flow via a RewriteOptionsManager.
SetRewriteOptions(custom_options.get());
// Use EncodeNormal because it matches the logic used by ProxyUrlNamer.
const GoogleString kExtendedBackgroundImage =
EncodeNormal("", "ce", "0", kBackgroundImage, "png");
// Now when we fetch the options, we'll find the image in the CSS
// cache-extended.
text.clear();
FetchFromProxy("I.embedded.css.pagespeed.cf.0.css",
request_headers,
true, /* expect_success */
&text,
&headers,
false /* proxy_fetch_property_callback_collector_created */);
EXPECT_EQ(StringPrintf(kCssWithEmbeddedImage,
kExtendedBackgroundImage.c_str()),
text);
}
TEST_F(ProxyInterfaceTest, MinResourceTimeZero) {
RewriteOptions* options = server_context()->global_options();
options->ClearSignatureForTesting();
options->SetRewriteLevel(RewriteOptions::kPassThrough);
options->EnableFilter(RewriteOptions::kRewriteCss);
options->set_min_resource_cache_time_to_rewrite_ms(
kHtmlCacheTimeSec * Timer::kSecondMs);
SetRewriteOptions(options);
SetResponseWithDefaultHeaders(kPageUrl, kContentTypeHtml,
CssLinkHref("a.css"), kHtmlCacheTimeSec * 2);
SetResponseWithDefaultHeaders("a.css", kContentTypeCss, kCssContent,
kHtmlCacheTimeSec * 2);
GoogleString text;
ResponseHeaders headers;
FetchFromProxy(kPageUrl, true, &text, &headers);
EXPECT_EQ(CssLinkHref(Encode("", "cf", "0", "a.css", "css")), text);
}
TEST_F(ProxyInterfaceTest, MinResourceTimeLarge) {
RewriteOptions* options = server_context()->global_options();
options->ClearSignatureForTesting();
options->SetRewriteLevel(RewriteOptions::kPassThrough);
options->EnableFilter(RewriteOptions::kRewriteCss);
options->set_min_resource_cache_time_to_rewrite_ms(
4 * kHtmlCacheTimeSec * Timer::kSecondMs);
server_context()->ComputeSignature(options);
SetResponseWithDefaultHeaders(kPageUrl, kContentTypeHtml,
CssLinkHref("a.css"), kHtmlCacheTimeSec * 2);
SetResponseWithDefaultHeaders("a.css", kContentTypeCss, kCssContent,
kHtmlCacheTimeSec * 2);
GoogleString text;
ResponseHeaders headers;
FetchFromProxy(kPageUrl, true, &text, &headers);
EXPECT_EQ(CssLinkHref("a.css"), text);
}
TEST_F(ProxyInterfaceTest, CacheRequests) {
ResponseHeaders html_headers;
DefaultResponseHeaders(kContentTypeHtml, kHtmlCacheTimeSec, &html_headers);
SetFetchResponse(AbsolutifyUrl(kPageUrl), html_headers, "1");
ResponseHeaders resource_headers;
DefaultResponseHeaders(kContentTypeCss, kHtmlCacheTimeSec, &resource_headers);
SetFetchResponse(AbsolutifyUrl("style.css"), resource_headers, "a");
GoogleString text;
ResponseHeaders actual_headers;
FetchFromProxy(kPageUrl, true, &text, &actual_headers);
EXPECT_EQ("1", text);
text.clear();
FetchFromProxy("style.css", true, &text, &actual_headers);
EXPECT_EQ("a", text);
SetFetchResponse(AbsolutifyUrl(kPageUrl), html_headers, "2");
SetFetchResponse(AbsolutifyUrl("style.css"), resource_headers, "b");
// Original response is still cached in both cases, so we do not
// fetch the new values.
text.clear();
FetchFromProxy(kPageUrl, true, &text, &actual_headers);
EXPECT_EQ("1", text);
text.clear();
FetchFromProxy("style.css", true, &text, &actual_headers);
EXPECT_EQ("a", text);
}
// Verifies that we proxy uncacheable resources, but do not insert them in the
// cache.
TEST_F(ProxyInterfaceTest, UncacheableResourcesNotCachedOnProxy) {
ResponseHeaders resource_headers;
DefaultResponseHeaders(kContentTypeCss, kHtmlCacheTimeSec, &resource_headers);
resource_headers.SetDateAndCaching(http_cache()->timer()->NowMs(),
300 * Timer::kSecondMs, ", private");
resource_headers.ComputeCaching();
SetFetchResponse(AbsolutifyUrl("style.css"), resource_headers, "a");
ProxyUrlNamer url_namer;
server_context()->set_url_namer(&url_namer);
ResponseHeaders out_headers;
GoogleString out_text;
// We should not cache while fetching via kProxyHost.
FetchFromProxy(
StrCat("http://", ProxyUrlNamer::kProxyHost,
"/test.com/test.com/style.css"),
true, &out_text, &out_headers);
EXPECT_EQ("a", out_text);
EXPECT_EQ(0, lru_cache()->num_hits());
EXPECT_EQ(0, http_cache()->cache_hits()->Get());
EXPECT_EQ(2, lru_cache()->num_misses()); // mapping, input resource
EXPECT_EQ(1, http_cache()->cache_misses()->Get()); // input resource
EXPECT_EQ(0, lru_cache()->num_inserts());
EXPECT_EQ(0, http_cache()->cache_inserts()->Get());
// We should likewise not cache while fetching on the origin domain.
out_text.clear();
ClearStats();
FetchFromProxy("style.css", true, &out_text, &out_headers);
EXPECT_EQ("a", out_text);
EXPECT_EQ(0, lru_cache()->num_hits());
EXPECT_EQ(0, http_cache()->cache_hits()->Get());
EXPECT_EQ(2, lru_cache()->num_misses()); // mapping, input resource
EXPECT_EQ(1, http_cache()->cache_misses()->Get()); // input resource
EXPECT_EQ(0, lru_cache()->num_inserts());
EXPECT_EQ(0, http_cache()->cache_inserts()->Get());
// Since the original response is not cached, we should pick up changes in the
// input resource immediately.
SetFetchResponse(AbsolutifyUrl("style.css"), resource_headers, "b");
out_text.clear();
ClearStats();
FetchFromProxy("style.css", true, &out_text, &out_headers);
EXPECT_EQ("b", out_text);
EXPECT_EQ(0, lru_cache()->num_hits());
EXPECT_EQ(0, http_cache()->cache_hits()->Get());
EXPECT_EQ(2, lru_cache()->num_misses()); // mapping, input resource
EXPECT_EQ(1, http_cache()->cache_misses()->Get()); // input resource
EXPECT_EQ(0, lru_cache()->num_inserts());
EXPECT_EQ(0, http_cache()->cache_inserts()->Get());
}
// Verifies that we retrieve and serve uncacheable resources, but do not insert
// them in the cache.
TEST_F(ProxyInterfaceTest, UncacheableResourcesNotCachedOnResourceFetch) {
ResponseHeaders resource_headers;
RequestHeaders request_headers;
DefaultResponseHeaders(kContentTypeCss, kHtmlCacheTimeSec, &resource_headers);
resource_headers.SetDateAndCaching(http_cache()->timer()->NowMs(),
300 * Timer::kSecondMs, ", private");
resource_headers.ComputeCaching();
SetFetchResponse(AbsolutifyUrl("style.css"), resource_headers, "a");
RewriteOptions* options = server_context()->global_options();
options->ClearSignatureForTesting();
options->SetRewriteLevel(RewriteOptions::kPassThrough);
options->EnableFilter(RewriteOptions::kRewriteCss);
server_context()->ComputeSignature(options);
ResponseHeaders out_headers;
GoogleString out_text;
// cf is not on-the-fly, and we can reconstruct it while keeping it private.
FetchFromProxy(Encode("", "cf", "0", "style.css", "css"),
request_headers,
true, /* expect_success */
&out_text,
&out_headers,
false /* proxy_fetch_property_callback_collector_created */);
EXPECT_TRUE(out_headers.HasValue(HttpAttributes::kCacheControl, "private"));
EXPECT_EQ("a", out_text);
EXPECT_EQ(0, lru_cache()->num_hits());
EXPECT_EQ(0, http_cache()->cache_hits()->Get());
EXPECT_EQ(3, lru_cache()->num_misses()); // 2x output, metadata, input
EXPECT_EQ(2, http_cache()->cache_misses()->Get()); // 2x output, input
EXPECT_EQ(2, lru_cache()->num_inserts()); // mapping, uncacheable memo
EXPECT_EQ(1, http_cache()->cache_inserts()->Get()); // uncacheable memo
out_text.clear();
ClearStats();
// ce is on-the-fly, and we can recover even though style.css is private.
FetchFromProxy(Encode("", "ce", "0", "style.css", "css"),
request_headers,
true, /* expect_success */
&out_text,
&out_headers,
false /* proxy_fetch_property_callback_collector_created */);
EXPECT_TRUE(out_headers.HasValue(HttpAttributes::kCacheControl, "private"));
EXPECT_EQ("a", out_text);
EXPECT_EQ(1, lru_cache()->num_hits()); // input uncacheable memo
EXPECT_EQ(0, http_cache()->cache_hits()->Get());
EXPECT_EQ(1, lru_cache()->num_misses());
EXPECT_EQ(1, http_cache()->cache_misses()->Get()); // input uncacheable memo
EXPECT_EQ(1, lru_cache()->num_inserts()); // mapping
EXPECT_EQ(1, lru_cache()->num_identical_reinserts()); // uncacheable memo
EXPECT_EQ(1, http_cache()->cache_inserts()->Get()); // uncacheable memo
out_text.clear();
ClearStats();
FetchFromProxy(Encode("", "ce", "0", "style.css", "css"),
request_headers,
true, /* expect_success */
&out_text,
&out_headers,
false /* proxy_fetch_property_callback_collector_created */);
EXPECT_TRUE(out_headers.HasValue(HttpAttributes::kCacheControl, "private"));
EXPECT_EQ("a", out_text);
EXPECT_EQ(2, lru_cache()->num_hits()); // uncacheable memo & modified check
EXPECT_EQ(0, http_cache()->cache_hits()->Get());
EXPECT_EQ(0, lru_cache()->num_misses());
EXPECT_EQ(1, http_cache()->cache_misses()->Get()); // uncacheable memo
EXPECT_EQ(0, lru_cache()->num_inserts());
EXPECT_EQ(1, lru_cache()->num_identical_reinserts()) << "uncacheable memo";
EXPECT_EQ(1, http_cache()->cache_inserts()->Get()); // uncacheable memo
// Since the original response is not cached, we should pick up changes in the
// input resource immediately.
SetFetchResponse(AbsolutifyUrl("style.css"), resource_headers, "b");
out_text.clear();
ClearStats();
FetchFromProxy(Encode("", "ce", "0", "style.css", "css"),
request_headers,
true, /* expect_success */
&out_text,
&out_headers,
false /* proxy_fetch_property_callback_collector_created */);
EXPECT_TRUE(out_headers.HasValue(HttpAttributes::kCacheControl, "private"));
EXPECT_EQ("b", out_text);
EXPECT_EQ(2, lru_cache()->num_hits()); // uncacheable memo & modified-check
EXPECT_EQ(0, http_cache()->cache_hits()->Get());
EXPECT_EQ(0, lru_cache()->num_misses());
EXPECT_EQ(1, http_cache()->cache_misses()->Get()) << "uncacheable memo";
EXPECT_EQ(0, lru_cache()->num_inserts());
EXPECT_EQ(1, lru_cache()->num_identical_reinserts()) << "uncacheable memo";
EXPECT_EQ(1, http_cache()->cache_inserts()->Get()) << "uncacheable memo";
}
// No matter what options->respect_vary() is set to we will respect HTML Vary
// headers.
TEST_F(ProxyInterfaceTest, NoCacheVaryHtml) {
RewriteOptions* options = server_context()->global_options();
options->ClearSignatureForTesting();
options->set_respect_vary(false);
server_context()->ComputeSignature(options);
ResponseHeaders html_headers;
DefaultResponseHeaders(kContentTypeHtml, kHtmlCacheTimeSec, &html_headers);
html_headers.Add(HttpAttributes::kVary, HttpAttributes::kUserAgent);
html_headers.ComputeCaching();
SetFetchResponse(AbsolutifyUrl(kPageUrl), html_headers, "1");
ResponseHeaders resource_headers;
DefaultResponseHeaders(kContentTypeCss, kHtmlCacheTimeSec, &resource_headers);
resource_headers.Add(HttpAttributes::kVary, HttpAttributes::kUserAgent);
resource_headers.ComputeCaching();
SetFetchResponse(AbsolutifyUrl("style.css"), resource_headers, "a");
GoogleString text;
ResponseHeaders actual_headers;
FetchFromProxy(kPageUrl, true, &text, &actual_headers);
EXPECT_EQ("1", text);
text.clear();
FetchFromProxy("style.css", true, &text, &actual_headers);
EXPECT_EQ("a", text);
SetFetchResponse(AbsolutifyUrl(kPageUrl), html_headers, "2");
SetFetchResponse(AbsolutifyUrl("style.css"), resource_headers, "b");
// HTML was not cached because of Vary: User-Agent header.
// So we do fetch the new value.
text.clear();
FetchFromProxy(kPageUrl, true, &text, &actual_headers);
EXPECT_EQ("2", text);
// Resource was cached because we have respect_vary == false.
// So we serve the old value.
text.clear();
FetchFromProxy("style.css", true, &text, &actual_headers);
EXPECT_EQ("a", text);
}
// Test https HTML responses are never cached, while https resources are cached.
TEST_F(ProxyInterfaceTest, NoCacheHttpsHtml) {
RewriteOptions* options = server_context()->global_options();
options->ClearSignatureForTesting();
options->set_respect_vary(false);
server_context()->ComputeSignature(options);
http_cache()->set_disable_html_caching_on_https(true);
ResponseHeaders html_headers(options->ComputeHttpOptions());
DefaultResponseHeaders(kContentTypeHtml, kHtmlCacheTimeSec, &html_headers);
html_headers.ComputeCaching();
SetFetchResponse(kHttpsPageUrl, html_headers, "1");
ResponseHeaders resource_headers(options->ComputeHttpOptions());
DefaultResponseHeaders(kContentTypeCss, kHtmlCacheTimeSec, &resource_headers);
resource_headers.ComputeCaching();
SetFetchResponse(kHttpsCssUrl, resource_headers, "a");
GoogleString text;
ResponseHeaders actual_headers(options->ComputeHttpOptions());
FetchFromProxy(kHttpsPageUrl, true, &text, &actual_headers);
EXPECT_EQ("1", text);
text.clear();
FetchFromProxy(kHttpsCssUrl, true, &text, &actual_headers);
EXPECT_EQ("a", text);
SetFetchResponse(kHttpsPageUrl, html_headers, "2");
SetFetchResponse(kHttpsCssUrl, resource_headers, "b");
ClearStats();
// HTML was not cached because it was via https. So we do fetch the new value.
text.clear();
FetchFromProxy(kHttpsPageUrl, true, &text, &actual_headers);
EXPECT_EQ("2", text);
EXPECT_EQ(0, lru_cache()->num_hits());
// Resource was cached, so we serve the old value.
text.clear();
FetchFromProxy(kHttpsCssUrl, true, &text, &actual_headers);
EXPECT_EQ("a", text);
EXPECT_EQ(1, http_cache()->cache_hits()->Get());
}
// Respect Vary for resources if options tell us to.
TEST_F(ProxyInterfaceTest, NoCacheVaryAll) {
RewriteOptions* options = server_context()->global_options();
options->ClearSignatureForTesting();
options->set_respect_vary(true);
server_context()->ComputeSignature(options);
ResponseHeaders html_headers(options->ComputeHttpOptions());
DefaultResponseHeaders(kContentTypeHtml, kHtmlCacheTimeSec, &html_headers);
html_headers.Add(HttpAttributes::kVary, HttpAttributes::kUserAgent);
html_headers.ComputeCaching();
SetFetchResponse(AbsolutifyUrl(kPageUrl), html_headers, "1");
ResponseHeaders resource_headers(options->ComputeHttpOptions());
DefaultResponseHeaders(kContentTypeCss, kHtmlCacheTimeSec, &resource_headers);
resource_headers.Add(HttpAttributes::kVary, HttpAttributes::kUserAgent);
resource_headers.ComputeCaching();
SetFetchResponse(AbsolutifyUrl("style.css"), resource_headers, "a");
GoogleString text;
ResponseHeaders actual_headers(options->ComputeHttpOptions());
FetchFromProxy(kPageUrl, true, &text, &actual_headers);
EXPECT_EQ("1", text);
text.clear();
FetchFromProxy("style.css", true, &text, &actual_headers);
EXPECT_EQ("a", text);
SetFetchResponse(AbsolutifyUrl(kPageUrl), html_headers, "2");
SetFetchResponse(AbsolutifyUrl("style.css"), resource_headers, "b");
// Original response was not cached in either case, so we do fetch the
// new value.
text.clear();
FetchFromProxy(kPageUrl, true, &text, &actual_headers);
EXPECT_EQ("2", text);
text.clear();
FetchFromProxy("style.css", true, &text, &actual_headers);
EXPECT_EQ("b", text);
}
TEST_F(ProxyInterfaceTest, Blacklist) {
const char content[] =
"<html>\n"
" <head/>\n"
" <body>\n"
" <script src='tiny_mce.js'></script>\n"
" </body>\n"
"</html>\n";
SetResponseWithDefaultHeaders("tiny_mce.js", kContentTypeJavascript, "", 100);
ValidateNoChanges("blacklist", content);
SetResponseWithDefaultHeaders(kPageUrl, kContentTypeHtml, content, 0);
GoogleString text_out;
ResponseHeaders headers_out;
FetchFromProxy(kPageUrl, true, &text_out, &headers_out);
EXPECT_STREQ(content, text_out);
}
TEST_F(ProxyInterfaceTest, RepairMismappedResource) {
// Teach the mock fetcher to serve origin content for
// "http://test.com/foo.js".
const char kContent[] = "function f() {alert('foo');}";
SetResponseWithDefaultHeaders("foo.js", kContentTypeHtml, kContent,
kHtmlCacheTimeSec * 2);
// Set up a Mock Namer that will mutate output resources to
// be served on proxy_host.com, encoding the origin URL.
ProxyUrlNamer url_namer;
ResponseHeaders headers;
GoogleString text;
server_context()->set_url_namer(&url_namer);
// Now fetch the origin content. This will simply hit the
// mock fetcher and always worked.
FetchFromProxy("foo.js", true, &text, &headers);
EXPECT_EQ(kContent, text);
// Now make a weird URL encoding of the origin resource using the
// proxy host. This may happen via javascript that detects its
// own path and initiates a 'load()' of another js file from the
// same path. In this variant, the resource is served from the
// "source domain", so it is automatically whitelisted.
text.clear();
FetchFromProxy(
StrCat("http://", ProxyUrlNamer::kProxyHost, "/test.com/test.com/foo.js"),
true, &text, &headers);
EXPECT_EQ(kContent, text);
// In the next case, the resource is served from a different domain. This
// is an open-proxy vulnerability and thus should fail.
text.clear();
url_namer.set_authorized(false);
FetchFromProxy(
StrCat("http://", ProxyUrlNamer::kProxyHost, "/test.com/evil.com/foo.js"),
false, &text, &headers);
}
TEST_F(ProxyInterfaceTest, CrossDomainHeaders) {
// If we're serving content from test.com via kProxyHost URL, we need to make
// sure that cookies are not propagated, as evil.com could also be potentially
// proxied via kProxyHost.
const char kText[] = "* { pretty; }";
ResponseHeaders orig_headers;
DefaultResponseHeaders(kContentTypeCss, 100, &orig_headers);
orig_headers.Add(HttpAttributes::kSetCookie, "tasty");
SetFetchResponse("http://test.com/file.css", orig_headers, kText);
ProxyUrlNamer url_namer;
server_context()->set_url_namer(&url_namer);
ResponseHeaders out_headers;
GoogleString out_text;
FetchFromProxy(
StrCat("http://", ProxyUrlNamer::kProxyHost,
"/test.com/test.com/file.css"),
true, &out_text, &out_headers);
EXPECT_STREQ(kText, out_text);
EXPECT_STREQ(NULL, out_headers.Lookup1(HttpAttributes::kSetCookie));
}
TEST_F(ProxyInterfaceTest, CrossDomainRedirectIfBlacklisted) {
ProxyUrlNamer url_namer;
server_context()->set_url_namer(&url_namer);
ResponseHeaders out_headers;
GoogleString out_text;
FetchFromProxy(
StrCat("http://", ProxyUrlNamer::kProxyHost,
"/test.com/test1.com/blacklist.css"),
false, &out_text, &out_headers);
EXPECT_STREQ("", out_text);
EXPECT_EQ(HttpStatus::kFound, out_headers.status_code());
EXPECT_STREQ("http://test1.com/blacklist.css",
out_headers.Lookup1(HttpAttributes::kLocation));
}
TEST_F(ProxyInterfaceTest, CrossDomainAuthorization) {
// If we're serving content from evil.com via kProxyHostUrl, we need to make
// sure we don't propagate through any (non-proxy) authorization headers, as
// they may have been cached from good.com (as both would look like
// kProxyHost to the browser).
ReflectingTestFetcher reflect;
server_context()->set_default_system_fetcher(&reflect);
ProxyUrlNamer url_namer;
server_context()->set_url_namer(&url_namer);
RequestHeaders request_headers;
request_headers.Add("Was", "Here");
request_headers.Add(HttpAttributes::kAuthorization, "Secret");
request_headers.Add(HttpAttributes::kProxyAuthorization, "OurSecret");
ResponseHeaders out_headers;
GoogleString out_text;
// Using .txt here so we don't try any AJAX rewriting.
FetchFromProxy(StrCat("http://", ProxyUrlNamer::kProxyHost,
"/test.com/test.com/file.txt"),
request_headers, true, &out_text, &out_headers);
EXPECT_STREQ("Here", out_headers.Lookup1("Was"));
EXPECT_FALSE(out_headers.Has(HttpAttributes::kAuthorization));
EXPECT_FALSE(out_headers.Has(HttpAttributes::kProxyAuthorization));
mock_scheduler()->AwaitQuiescence();
}
TEST_F(ProxyInterfaceTest, CrossDomainHeadersWithUncacheableResourceOnProxy) {
// Check that we do not propagate cookies from test.com via kProxyHost URL,
// as in CrossDomainHeaders above. Also check that we do propagate cache
// control.
const char kText[] = "* { pretty; }";
ResponseHeaders orig_headers;
DefaultResponseHeaders(kContentTypeCss, 100, &orig_headers);
orig_headers.Add(HttpAttributes::kSetCookie, "tasty");
orig_headers.SetDateAndCaching(http_cache()->timer()->NowMs(),
400 * Timer::kSecondMs, ", private");
orig_headers.ComputeCaching();
SetFetchResponse("http://test.com/file.css", orig_headers, kText);
ProxyUrlNamer url_namer;
server_context()->set_url_namer(&url_namer);
ResponseHeaders out_headers;
GoogleString out_text;
FetchFromProxy(
StrCat("http://", ProxyUrlNamer::kProxyHost,
"/test.com/test.com/file.css"),
true, &out_text, &out_headers);
// Check that we ate the cookies.
EXPECT_STREQ(kText, out_text);
ConstStringStarVector values;
out_headers.Lookup(HttpAttributes::kSetCookie, &values);
EXPECT_EQ(0, values.size());
// Check that the resource Cache-Control has been preserved.
values.clear();
out_headers.Lookup(HttpAttributes::kCacheControl, &values);
ASSERT_EQ(2, values.size());
EXPECT_STREQ("max-age=400", *values[0]);
EXPECT_STREQ("private", *values[1]);
}
TEST_F(ProxyInterfaceTest, CrossDomainHeadersWithUncacheableResourceOnFetch) {
// Check that we do not propagate cookies from test.com via a resource fetch,
// as in CrossDomainHeaders above. Also check that we do propagate cache
// control, and that we run the filter specified in the resource fetch URL.
// Note that the running of filters at present can only happen if
// the filter is on the-fly.
const char kText[] = "* { pretty; }";
ResponseHeaders orig_headers;
DefaultResponseHeaders(kContentTypeCss, 100, &orig_headers);
orig_headers.Add(HttpAttributes::kSetCookie, "tasty");
orig_headers.SetDateAndCaching(http_cache()->timer()->NowMs(),
400 * Timer::kSecondMs, ", private");
orig_headers.ComputeCaching();
SetFetchResponse("http://test.com/file.css", orig_headers, kText);
ProxyUrlNamer url_namer;
server_context()->set_url_namer(&url_namer);
ResponseHeaders out_headers;
GoogleString out_text;
RequestHeaders request_headers;
FetchFromProxy(Encode("", "ce", "0", "file.css", "css"),
request_headers,
true,
&out_text,
&out_headers,
false);
// Check that we passed through the CSS.
EXPECT_STREQ(kText, out_text);
// Check that we ate the cookies.
ConstStringStarVector values;
out_headers.Lookup(HttpAttributes::kSetCookie, &values);
EXPECT_EQ(0, values.size());
// Check that the resource Cache-Control has been preserved.
// max-age actually gets smaller, though, since this also triggers
// a rewrite failure.
values.clear();
EXPECT_STREQ("max-age=300, private", out_headers.LookupJoined(
HttpAttributes::kCacheControl));
}
TEST_F(ProxyInterfaceTest, CrossDomainHeadersWithUncacheableResourceOnFetch2) {
// Variant of the above with a non-on-the-fly filter.
const char kText[] = "* { pretty; }";
ResponseHeaders orig_headers;
DefaultResponseHeaders(kContentTypeCss, 100, &orig_headers);
orig_headers.Add(HttpAttributes::kSetCookie, "tasty");
orig_headers.SetDateAndCaching(http_cache()->timer()->NowMs(),
400 * Timer::kSecondMs, ", private");
orig_headers.ComputeCaching();
SetFetchResponse("http://test.com/file.css", orig_headers, kText);
ProxyUrlNamer url_namer;
server_context()->set_url_namer(&url_namer);
ResponseHeaders out_headers;
GoogleString out_text;
RequestHeaders request_headers;
FetchFromProxy(Encode("", "cf", "0", "file.css", "css"),
request_headers,
true, /* expect_success */
&out_text,
&out_headers,
false /* proxy_fetch_property_callback_collector_created */);
// Proper output
EXPECT_STREQ("*{pretty}", out_text);
// Private.
EXPECT_STREQ("max-age=400, private", out_headers.LookupJoined(
HttpAttributes::kCacheControl));
// Check that we ate the cookies.
EXPECT_FALSE(out_headers.Has(HttpAttributes::kSetCookie));
}
TEST_F(ProxyInterfaceTest, ProxyResourceQueryOnly) {
// At one point we had a bug where if we optimized a pagespeed resource
// whose original name was a bare query, we would loop infinitely when
// trying to fetch it from a separate-domain proxy.
const char kUrl[] = "?somestuff";
SetResponseWithDefaultHeaders(kUrl, kContentTypeJavascript,
"var a = 2;// stuff", kHtmlCacheTimeSec * 2);
ProxyUrlNamer url_namer;
server_context()->set_url_namer(&url_namer);
ResponseHeaders out_headers;
GoogleString out_text;
RequestHeaders request_headers;
FetchFromProxy(
StrCat("http://", ProxyUrlNamer::kProxyHost,
"/test.com/test.com/",
EncodeNormal("", "jm", "0", kUrl, "css")),
request_headers,
true, /* expect_success */
&out_text,
&out_headers,
false /* proxy_fetch_property_callback_collector_created */);
EXPECT_STREQ("var a=2;", out_text);
CheckBackgroundFetch(out_headers, false);
}
TEST_F(ProxyInterfaceTest, NoRehostIncompatMPS) {
// Make sure we don't try to interpret a URL from an incompatible
// mod_pagespeed version at our proxy host level.
// This url will be rejected by CssUrlEncoder
const char kOldName[] = "style.css.pagespeed.cf.0.css";
const char kContent[] = "* {}";
SetResponseWithDefaultHeaders(kOldName, kContentTypeCss, kContent, 100);
ProxyUrlNamer url_namer;
server_context()->set_url_namer(&url_namer);
ResponseHeaders out_headers;
GoogleString out_text;
RequestHeaders request_headers;
FetchFromProxy(
StrCat("http://", ProxyUrlNamer::kProxyHost,
"/test.com/test.com/",
EncodeNormal("", "ce", "0", kOldName, "css")),
request_headers,
true, /* expect_success */
&out_text,
&out_headers,
false /* proxy_fetch_property_callback_collector_created */);
EXPECT_EQ(HttpStatus::kOK, out_headers.status_code());
EXPECT_STREQ(kContent, out_text);
}
// Test that we serve "Cache-Control: no-store" only when original page did.
TEST_F(ProxyInterfaceTest, NoStore) {
RewriteOptions* options = server_context()->global_options();
options->ClearSignatureForTesting();
options->set_max_html_cache_time_ms(0);
server_context()->ComputeSignature(options);
// Most headers get converted to "no-cache, max-age=0".
EXPECT_STREQ("max-age=0, no-cache",
RewriteHtmlCacheHeader("empty", ""));
EXPECT_STREQ("max-age=0, no-cache",
RewriteHtmlCacheHeader("private", "private, max-age=100"));
EXPECT_STREQ("max-age=0, no-cache",
RewriteHtmlCacheHeader("no-cache", "no-cache"));
// Headers with "no-store", preserve that header as well.
EXPECT_STREQ("max-age=0, no-cache, no-store",
RewriteHtmlCacheHeader("no-store", "no-cache, no-store"));
EXPECT_STREQ("max-age=0, no-cache, no-store",
RewriteHtmlCacheHeader("no-store2", "no-store, max-age=300"));
}
TEST_F(ProxyInterfaceTest, PropCacheFilter) {
RewriteOptions* options = server_context()->global_options();
options->ClearSignatureForTesting();
options->DisableFilter(RewriteOptions::kAddHead);
server_context()->ComputeSignature(options);
CreateFilterCallback create_filter_callback;
factory()->AddCreateFilterCallback(&create_filter_callback);
EnableDomCohortWritesWithDnsPrefetch();
SetResponseWithDefaultHeaders(kPageUrl, kContentTypeHtml,
"<div><p></p></div>", 0);
GoogleString text_out;
ResponseHeaders headers_out;
FetchFromProxy(kPageUrl, true, &text_out, &headers_out);
EXPECT_EQ("<!-- --><div><p></p></div>", text_out);
FetchFromProxy(kPageUrl, true, &text_out, &headers_out);
EXPECT_EQ("<!-- 2 elements unstable --><div><p></p></div>", text_out);
// How many refreshes should we require before it's stable? That
// tuning can be done in the PropertyCacheTest. For this
// system-test just do a hundred blind refreshes and check again for
// stability.
const int kFetchIterations = 100;
for (int i = 0; i < kFetchIterations; ++i) {
FetchFromProxy(kPageUrl, true, &text_out, &headers_out);
}
// Must be stable by now!
EXPECT_EQ("<!-- 2 elements stable --><div><p></p></div>", text_out);
// In this algorithm we will spend a property-cache-write per fetch.
//
// We'll also check that we do no cache writes when there are no properties
// to save.
EXPECT_EQ(2 + kFetchIterations, lru_cache()->num_inserts());
// Now change the HTML and watch the #elements change.
SetResponseWithDefaultHeaders(kPageUrl, kContentTypeHtml,
"<div><span><p></p></span></div>", 0);
FetchFromProxy(kPageUrl, true, &text_out, &headers_out);
FetchFromProxy(kPageUrl, true, &text_out, &headers_out);
EXPECT_EQ("<!-- 3 elements stable --><div><span><p></p></span></div>",
text_out);
ClearStats();
// Finally, disable the property-cache and note that the element-count
// annotatation reverts to "unknown mode"
server_context_->set_enable_property_cache(false);
FetchFromProxy(kPageUrl, true, &text_out, &headers_out);
EXPECT_EQ("<!-- --><div><span><p></p></span></div>", text_out);
}
TEST_F(ProxyInterfaceTest, DomCohortWritten) {
// Other than the write of DomCohort, there will be no properties added to
// the cache in this test because we have not enabled the filter with
// CreateFilterCallback create_filter_callback;
// factory()->AddCreateFilterCallback(&callback);
DisableAjax();
GoogleString text_out;
ResponseHeaders headers_out;
// No writes should occur if no filter that uses the dom cohort is enabled.
FetchFromProxy(kPageUrl, true, &text_out, &headers_out);
EXPECT_EQ(0, lru_cache()->num_inserts());
EXPECT_EQ(2, lru_cache()->num_misses()); // 1 property-cache + 1 http-cache
// Enable a filter that uses the dom cohort and make sure property cache is
// updated.
ClearStats();
EnableDomCohortWritesWithDnsPrefetch();
FetchFromProxy(kPageUrl, true, &text_out, &headers_out);
EXPECT_EQ(1, lru_cache()->num_inserts());
EXPECT_EQ(2, lru_cache()->num_misses()); // 1 property-cache + 1 http-cache
ClearStats();
server_context_->set_enable_property_cache(false);
FetchFromProxy(kPageUrl, true, &text_out, &headers_out);
EXPECT_EQ(0, lru_cache()->num_inserts());
EXPECT_EQ(1, lru_cache()->num_misses()); // http-cache only.
}
TEST_F(ProxyInterfaceTest, StatusCodePropertyWritten) {
DisableAjax();
EnableDomCohortWritesWithDnsPrefetch();
GoogleString text_out;
ResponseHeaders headers_out;
// Status code 404 gets written when page is not available.
SetFetchResponse404(kPageUrl);
FetchFromProxy(kPageUrl, true, &text_out, &headers_out);
EXPECT_EQ(HttpStatus::kNotFound,
GetStatusCodeInPropertyCache(StrCat(kTestDomain, kPageUrl)));
// Status code 200 gets written when page is available.
SetResponseWithDefaultHeaders(kPageUrl, kContentTypeHtml,
"<html></html>", kHtmlCacheTimeSec);
lru_cache()->Clear();
FetchFromProxy(kPageUrl, true, &text_out, &headers_out);
EXPECT_EQ(HttpStatus::kOK,
GetStatusCodeInPropertyCache(StrCat(kTestDomain, kPageUrl)));
// Status code 301 gets written when it is a permanent redirect.
headers_out.Clear();
text_out.clear();
headers_out.SetStatusAndReason(HttpStatus::kMovedPermanently);
SetFetchResponse(StrCat(kTestDomain, kPageUrl), headers_out, text_out);
lru_cache()->Clear();
FetchFromProxy(kPageUrl, true, &text_out, &headers_out);
EXPECT_EQ(HttpStatus::kMovedPermanently,
GetStatusCodeInPropertyCache(StrCat(kTestDomain, kPageUrl)));
}
TEST_F(ProxyInterfaceTest, PropCacheNoWritesIfHtmlEndsWithTxt) {
CreateFilterCallback create_filter_callback;
factory()->AddCreateFilterCallback(&create_filter_callback);
// There will be no properties added to the cache set in this test because
// we have not enabled the filter with
// CreateFilterCallback create_filter_callback;
// factory()->AddCreateFilterCallback(&callback);
DisableAjax();
SetResponseWithDefaultHeaders("page.txt", kContentTypeHtml,
"<div><p></p></div>", 0);
GoogleString text_out;
ResponseHeaders headers_out;
FetchFromProxy("page.txt", true, &text_out, &headers_out);
EXPECT_EQ(0, lru_cache()->num_inserts());
EXPECT_EQ(1, lru_cache()->num_misses()); // http-cache only
ClearStats();
server_context_->set_enable_property_cache(false);
FetchFromProxy("page.txt", true, &text_out, &headers_out);
EXPECT_EQ(0, lru_cache()->num_inserts());
EXPECT_EQ(1, lru_cache()->num_misses()); // http-cache only
}
TEST_F(ProxyInterfaceTest, PropCacheNoWritesForNonGetRequests) {
CreateFilterCallback create_filter_callback;
factory()->AddCreateFilterCallback(&create_filter_callback);
DisableAjax();
SetResponseWithDefaultHeaders("page.txt", kContentTypeHtml,
"<div><p></p></div>", 0);
GoogleString text_out;
ResponseHeaders headers_out;
RequestHeaders request_headers;
request_headers.set_method(RequestHeaders::kPost);
FetchFromProxy("page.txt", true, &text_out, &headers_out);
EXPECT_EQ(0, lru_cache()->num_inserts());
EXPECT_EQ(1, lru_cache()->num_misses()); // http-cache only
ClearStats();
server_context_->set_enable_property_cache(false);
FetchFromProxy("page.txt", true, &text_out, &headers_out);
EXPECT_EQ(0, lru_cache()->num_inserts());
EXPECT_EQ(1, lru_cache()->num_misses()); // http-cache only
}
TEST_F(ProxyInterfaceTest, PropCacheNoWritesIfNonHtmlDelayedCache) {
DisableAjax();
TestPropertyCache(kImageFilenameLackingExt, true, false, true);
}
TEST_F(ProxyInterfaceTest, PropCacheNoWritesIfNonHtmlImmediateCache) {
// Tests rewriting a file that turns out to be a jpeg, but lacks an
// extension, where the property-cache lookup is delivered immediately.
DisableAjax();
TestPropertyCache(kImageFilenameLackingExt, false, false, true);
}
TEST_F(ProxyInterfaceTest, PropCacheNoWritesIfNonHtmlThreadedCache) {
// Tests rewriting a file that turns out to be a jpeg, but lacks an
// extension, where the property-cache lookup is delivered in a
// separate thread.
DisableAjax();
TestPropertyCache(kImageFilenameLackingExt, true, true, true);
}
TEST_F(ProxyInterfaceTest, StatusCodeUpdateRace) {
// Tests rewriting a file that turns out to be a jpeg, but lacks an
// extension, where the property-cache lookup is delivered in a
// separate thread. Use sync points to ensure that Done() deletes the
// collector just after the Detach() critical block is executed.
DisableAjax();
TestPropertyCache(kImageFilenameLackingExt, false, true, true);
}
TEST_F(ProxyInterfaceTest, ThreadedHtml) {
// Tests rewriting HTML resource where property-cache lookup is delivered
// in a separate thread.
DisableAjax();
EnableDomCohortWritesWithDnsPrefetch();
TestPropertyCache(kPageUrl, true, true, true);
}
TEST_F(ProxyInterfaceTest, ThreadedHtmlFetcherFailure) {
// Tests rewriting HTML resource where property-cache lookup is delivered
// in a separate thread, but the HTML lookup fails after emitting the
// body.
DisableAjax();
EnableDomCohortWritesWithDnsPrefetch();
mock_url_fetcher()->SetResponseFailure(AbsolutifyUrl(kPageUrl));
TestPropertyCache(kPageUrl, true, true, false);
}
TEST_F(ProxyInterfaceTest, HtmlFetcherFailure) {
// Tests rewriting HTML resource where property-cache lookup is
// delivered in a blocking fashion, and the HTML lookup fails after
// emitting the body.
DisableAjax();
EnableDomCohortWritesWithDnsPrefetch();
mock_url_fetcher()->SetResponseFailure(AbsolutifyUrl(kPageUrl));
TestPropertyCache(kPageUrl, false, false, false);
}
TEST_F(ProxyInterfaceTest, HeadersSetupRace) {
//
// This crash occured where an Idle-callback is used to flush HTML.
// In this bug, we were connecting the property-cache callback to
// the ProxyFetch and then mutating response-headers. The property-cache
// callback was waking up the QueuedWorkerPool::Sequence used by
// the ProxyFetch, which was waking up and calling HeadersComplete.
// If the implementation of HeadersComplete mutated headers itself,
// we'd have a deadly race.
//
// This test uses the ThreadSynchronizer class to induce the desired
// race, with strategically placed calls to Signal and Wait.
//
// Note that the fix for the race means that one of the Signals does
// not occur at all, so we have to declare it as "Sloppy" so the
// ThreadSynchronizer class doesn't vomit on destruction.
const int kIdleCallbackTimeoutMs = 10;
RewriteOptions* options = server_context()->global_options();
options->ClearSignatureForTesting();
options->set_idle_flush_time_ms(kIdleCallbackTimeoutMs);
options->set_flush_html(true);
server_context()->ComputeSignature(options);
DisableAjax();
EnableDomCohortWritesWithDnsPrefetch();
ThreadSynchronizer* sync = server_context()->thread_synchronizer();
sync->EnableForPrefix(ProxyFetch::kHeadersSetupRacePrefix);
ThreadSystem* thread_system = server_context()->thread_system();
QueuedWorkerPool pool(1, "test", thread_system);
QueuedWorkerPool::Sequence* sequence = pool.NewSequence();
WorkerTestBase::SyncPoint sync_point(thread_system);
sequence->Add(MakeFunction(static_cast<ProxyInterfaceTestBase*>(this),
&ProxyInterfaceTest::TestHeadersSetupRace));
sequence->Add(new WorkerTestBase::NotifyRunFunction(&sync_point));
sync->TimedWait(ProxyFetch::kHeadersSetupRaceAlarmQueued,
ProxyFetch::kTestSignalTimeoutMs);
{
// Trigger the idle-callback, if it has been queued.
ScopedMutex lock(mock_scheduler()->mutex());
mock_scheduler()->ProcessAlarmsOrWaitUs(
kIdleCallbackTimeoutMs * Timer::kMsUs);
}
sync->Wait(ProxyFetch::kHeadersSetupRaceDone);
sync_point.Wait();
pool.ShutDown();
sync->AllowSloppyTermination(ProxyFetch::kHeadersSetupRaceAlarmQueued);
}
// TODO(jmarantz): add a test with a simulated slow cache to see what happens
// when the rest of the system must block, buffering up incoming HTML text,
// waiting for the property-cache lookups to complete.
// Test that we set the Experiment cookie up appropriately.
TEST_F(ProxyInterfaceTest, ExperimentTest) {
RewriteOptions* options = server_context()->global_options();
options->ClearSignatureForTesting();
options->set_ga_id("123-455-2341");
options->set_running_experiment(true);
NullMessageHandler handler;
options->AddExperimentSpec("id=2;enable=extend_cache;percent=100", &handler);
server_context()->ComputeSignature(options);
SetResponseWithDefaultHeaders("example.jpg", kContentTypeJpeg,
"image data", 300);
ResponseHeaders headers;
const char kContent[] = "<html><head></head><body>A very compelling "
"article with an image: <img src=example.jpg></body></html>";
headers.Add(HttpAttributes::kContentType, kContentTypeHtml.mime_type());
headers.SetStatusAndReason(HttpStatus::kOK);
SetFetchResponse(AbsolutifyUrl("text.html"), headers, kContent);
headers.Clear();
GoogleString text;
FetchFromProxy("text.html", true, &text, &headers);
// Assign all visitors to an experiment_spec.
EXPECT_TRUE(headers.Has(HttpAttributes::kSetCookie));
ConstStringStarVector values;
headers.Lookup(HttpAttributes::kSetCookie, &values);
bool found = false;
for (int i = 0, n = values.size(); i < n; ++i) {
if (values[i]->find(experiment::kExperimentCookie) == 0) {
found = true;
break;
}
}
EXPECT_TRUE(found);
// Image cache-extended and including experiment_spec 'a'.
EXPECT_TRUE(text.find("example.jpg.pagespeed.a.ce") != GoogleString::npos);
headers.Clear();
headers.Add(HttpAttributes::kContentType, kContentTypeHtml.mime_type());
headers.SetStatusAndReason(HttpStatus::kOK);
SetFetchResponse(AbsolutifyUrl("text2.html"), headers, kContent);
headers.Clear();
text.clear();
RequestHeaders req_headers;
req_headers.Add(HttpAttributes::kCookie, "PageSpeedExperiment=2");
FetchFromProxy("text2.html", req_headers, true, &text, &headers);
// Visitor already has cookie with id=2; don't give them a new one.
EXPECT_FALSE(headers.Has(HttpAttributes::kSetCookie));
// Image cache-extended and including experiment_spec 'a'.
EXPECT_TRUE(text.find("example.jpg.pagespeed.a.ce") != GoogleString::npos);
// Check that we don't include an experiment_spec index in urls for the "no
// experiment" group (id=0).
headers.Clear();
headers.Add(HttpAttributes::kContentType, kContentTypeHtml.mime_type());
headers.SetStatusAndReason(HttpStatus::kOK);
SetFetchResponse(AbsolutifyUrl("text3.html"), headers, kContent);
headers.Clear();
text.clear();
RequestHeaders req_headers2;
req_headers2.Add(HttpAttributes::kCookie, "PageSpeedExperiment=0");
FetchFromProxy("text3.html", req_headers2, true, &text, &headers);
EXPECT_FALSE(headers.Has(HttpAttributes::kSetCookie));
EXPECT_TRUE(text.find("example.jpg.pagespeed.ce") != GoogleString::npos);
}
TEST_F(ProxyInterfaceTest, UrlAttributeTest) {
RewriteOptions* options = server_context()->global_options();
options->ClearSignatureForTesting();
options->EnableFilter(RewriteOptions::kRewriteDomains);
options->set_domain_rewrite_hyperlinks(true);
NullMessageHandler handler;
options->WriteableDomainLawyer()->AddRewriteDomainMapping(
"http://dst.example.com", "http://src.example.com", &handler);
options->AddUrlValuedAttribute(
"span", "src", semantic_type::kHyperlink);
options->AddUrlValuedAttribute("hr", "imgsrc", semantic_type::kImage);
server_context()->ComputeSignature(options);
SetResponseWithDefaultHeaders(
"http://src.example.com/null", kContentTypeHtml, "", 0);
ResponseHeaders headers;
const char kContent[] = "<html><head></head><body>"
"<img src=\"http://src.example.com/null\">"
"<hr imgsrc=\"http://src.example.com/null\">"
"<span src=\"http://src.example.com/null\"></span>"
"<other src=\"http://src.example.com/null\"></other></body></html>";
headers.Add(HttpAttributes::kContentType, kContentTypeHtml.mime_type());
headers.SetStatusAndReason(HttpStatus::kOK);
SetFetchResponse(AbsolutifyUrl("text.html"), headers, kContent);
headers.Clear();
GoogleString text;
FetchFromProxy("text.html", true, &text, &headers);
// img.src, hr.imgsrc, and span.src are all rewritten
EXPECT_TRUE(text.find("<img src=\"http://dst.example.com/null\"") !=
GoogleString::npos);
EXPECT_TRUE(text.find("<hr imgsrc=\"http://dst.example.com/null\"") !=
GoogleString::npos);
EXPECT_TRUE(text.find("<span src=\"http://dst.example.com/null\"") !=
GoogleString::npos);
// other.src not rewritten
EXPECT_TRUE(text.find("<other src=\"http://src.example.com/null\"") !=
GoogleString::npos);
}
TEST_F(ProxyInterfaceTest, TestFallbackPropertiesUsageWithQueryParams) {
GoogleString url("http://www.test.com/a/b.html?withquery=some");
GoogleString fallback_url("http://www.test.com/a/b.html?withquery=different");
TestFallbackPageProperties(url, fallback_url);
}
TEST_F(ProxyInterfaceTest, TestFallbackPropertiesUsageWithLeafNode) {
GoogleString url("http://www.test.com/a/b.html");
GoogleString fallback_url("http://www.test.com/a/c.html");
TestFallbackPageProperties(url, fallback_url);
}
TEST_F(ProxyInterfaceTest,
TestFallbackPropertiesUsageWithLeafNodeHavingTrailingSlash) {
GoogleString url("http://www.test.com/a/b/");
GoogleString fallback_url("http://www.test.com/a/c/");
TestFallbackPageProperties(url, fallback_url);
}
TEST_F(ProxyInterfaceTest, TestNoFallbackCallWithNoLeaf) {
GoogleUrl gurl("http://www.test.com/");
options()->set_use_fallback_property_cache_values(true);
StringAsyncFetch callback(
RequestContext::NewTestRequestContext(
server_context()->thread_system()));
RequestHeaders request_headers;
callback.set_request_headers(&request_headers);
scoped_ptr<ProxyFetchPropertyCallbackCollector> callback_collector(
proxy_interface_->InitiatePropertyCacheLookup(
false, gurl, options(), &callback, false));
PropertyPage* fallback_page = callback_collector->fallback_property_page()
->property_page_with_fallback_values();
// No PropertyPage with fallback values.
EXPECT_EQ(NULL, fallback_page);
}
TEST_F(ProxyInterfaceTest, TestSkipBlinkCohortLookUp) {
GoogleUrl gurl("http://www.test.com/");
StringAsyncFetch callback(
RequestContext::NewTestRequestContext(server_context()->thread_system()));
RequestHeaders request_headers;
callback.set_request_headers(&request_headers);
scoped_ptr<ProxyFetchPropertyCallbackCollector> callback_collector(
proxy_interface_->InitiatePropertyCacheLookup(
false, gurl, options(), &callback, false));
// Cache lookup only for dom cohort.
EXPECT_EQ(0, lru_cache()->num_hits());
EXPECT_EQ(1, lru_cache()->num_misses());
}
TEST_F(ProxyInterfaceTest, TestSkipBlinkCohortLookUpInFallbackPage) {
GoogleUrl gurl("http://www.test.com/1.html?a=b");
options()->set_use_fallback_property_cache_values(true);
StringAsyncFetch callback(
RequestContext::NewTestRequestContext(server_context()->thread_system()));
RequestHeaders request_headers;
callback.set_request_headers(&request_headers);
scoped_ptr<ProxyFetchPropertyCallbackCollector> callback_collector(
proxy_interface_->InitiatePropertyCacheLookup(
false, gurl, options(), &callback, true));
// Cache lookup for:
// dom and blink cohort for actual property page.
// dom cohort for fallback property page.
EXPECT_EQ(0, lru_cache()->num_hits());
EXPECT_EQ(3, lru_cache()->num_misses());
}
TEST_F(ProxyInterfaceTest, BailOutOfParsing) {
RewriteOptions* options = server_context()->global_options();
options->ClearSignatureForTesting();
options->EnableExtendCacheFilters();
options->set_max_html_parse_bytes(60);
options->DisableFilter(RewriteOptions::kAddHead);
server_context()->ComputeSignature(options);
SetResponseWithDefaultHeaders(StrCat(kTestDomain, "1.jpg"), kContentTypeJpeg,
"image", kHtmlCacheTimeSec * 2);
// This is larger than 60 bytes.
const char kContent[] = "<html><head></head><body>"
"<img src=\"1.jpg\">"
"<p>Some very long and very boring text</p>"
"</body></html>";
SetResponseWithDefaultHeaders(kPageUrl, kContentTypeHtml, kContent, 0);
ResponseHeaders headers;
GoogleString text;
FetchFromProxy(kPageUrl, true, &text, &headers);
// For the first request, we bail out of parsing and insert the redirect. We
// also update the pcache.
EXPECT_STREQ(
"<html><script type=\"text/javascript\">"
"window.location=\"http://test.com/page.html?PageSpeed=off\";"
"</script></html>",
text);
headers.Clear();
text.clear();
// We look up the pcache and find that we should skip parsing. Hence, we just
// pass the bytes through.
FetchFromProxy(kPageUrl, true, &text, &headers);
EXPECT_EQ(kContent, text);
// This is smaller than 60 bytes.
const char kNewContent[] = "<html><head></head><body>"
"<img src=\"1.jpg\"></body></html>";
SetResponseWithDefaultHeaders(kPageUrl, kContentTypeHtml, kNewContent, 0);
headers.Clear();
text.clear();
// We still remember that we should skip parsing. Hence, we pass the bytes
// through. However, after this request, we update the pcache to indicate that
// we should no longer skip parsing.
FetchFromProxy(kPageUrl, true, &text, &headers);
EXPECT_EQ(kNewContent, text);
headers.Clear();
text.clear();
// This request is rewritten.
FetchFromProxy(kPageUrl, true, &text, &headers);
EXPECT_EQ("<html><head></head><body>"
"<img src=\"1.jpg.pagespeed.ce.0.jpg\">"
"</body></html>", text);
}
TEST_F(ProxyInterfaceTest, LoggingInfoRewriteInfoMaxSize) {
RewriteOptions* options = server_context()->global_options();
options->ClearSignatureForTesting();
options->set_max_rewrite_info_log_size(10);
server_context()->ComputeSignature(options);
SetResponseWithDefaultHeaders(StrCat(kTestDomain, "1.jpg"), kContentTypeJpeg,
"image", kHtmlCacheTimeSec * 2);
GoogleString content = "<html><head></head><body>";
for (int i = 0; i < 50; ++i) {
StrAppend(&content, "<img src=\"1.jpg\">");
}
StrAppend(&content, "</body></html>");
SetResponseWithDefaultHeaders(kPageUrl, kContentTypeHtml, content, 0);
ResponseHeaders headers;
GoogleString text;
FetchFromProxy(kPageUrl, true, &text, &headers);
GoogleString expected_response(content);
GlobalReplaceSubstring("1.jpg", "1.jpg.pagespeed.ce.0.jpg",
&expected_response);
EXPECT_STREQ(expected_response, text);
EXPECT_EQ(10, logging_info()->rewriter_info_size());
EXPECT_TRUE(logging_info()->rewriter_info_size_limit_exceeded());
}
TEST_F(ProxyInterfaceTest, WebpImageReconstruction) {
RewriteOptions* options = server_context()->global_options();
options->ClearSignatureForTesting();
options->EnableFilter(RewriteOptions::kConvertJpegToWebp);
server_context()->ComputeSignature(options);
AddFileToMockFetcher(StrCat(kTestDomain, "1.jpg"), "Puzzle.jpg",
kContentTypeJpeg, 100);
ResponseHeaders response_headers;
GoogleString text;
RequestHeaders request_headers;
request_headers.Replace(HttpAttributes::kUserAgent, "webp");
request_headers.Replace(HttpAttributes::kAccept, "image/webp");
const GoogleString kWebpUrl = Encode("", "ic", "0", "1.jpg", "webp");
FetchFromProxy(
kWebpUrl,
request_headers,
true, /* expect_success */
&text,
&response_headers,
false /* proxy_fetch_property_callback_collector_created */);
response_headers.ComputeCaching();
EXPECT_STREQ(kContentTypeWebp.mime_type(),
response_headers.Lookup1(HttpAttributes::kContentType));
EXPECT_EQ(ServerContext::kGeneratedMaxAgeMs, response_headers.cache_ttl_ms());
const char kCssWithEmbeddedImage[] = "*{background-image:url(%s)}";
SetResponseWithDefaultHeaders(
"embedded.css", kContentTypeCss,
StringPrintf(kCssWithEmbeddedImage, "1.jpg"), kHtmlCacheTimeSec * 2);
FetchFromProxy(Encode("", "cf", "0", "embedded.css", "css"),
request_headers,
true, /* expect_success */
&text,
&response_headers,
false /* proxy_fetch_property_callback_collector_created */);
response_headers.ComputeCaching();
EXPECT_EQ(ServerContext::kGeneratedMaxAgeMs, response_headers.cache_ttl_ms());
EXPECT_EQ(StringPrintf(kCssWithEmbeddedImage, kWebpUrl.c_str()), text);
}
class ProxyInterfaceOriginPropertyPageTest : public ProxyInterfaceTest {
protected:
class PerOriginPageReaderFilter : public EmptyHtmlFilter {
public:
explicit PerOriginPageReaderFilter(RewriteDriver* driver)
: driver_(driver) {}
virtual void StartDocument() {
// This keeps a little per-site visits counter in per-origin pcache
// and dumps it into a comment.
PropertyPage* sitewide_page = driver_->origin_property_page();
const PropertyCache::Cohort* dom_cohort =
driver_->server_context()->dom_cohort();
const char kVisits[] = "visits";
PropertyValue* val = sitewide_page->GetProperty(dom_cohort, kVisits);
int count = 0;
if (val->has_value()) {
EXPECT_TRUE(StringToInt(val->value(), &count));
}
driver_->InsertComment(StrCat("Site visit:", IntegerToString(count)));
// Update counter.
sitewide_page->UpdateValue(dom_cohort, kVisits,
IntegerToString(count + 1));
sitewide_page->WriteCohort(dom_cohort);
}
virtual void StartElement(HtmlElement* element) {}
virtual void EndDocument() {}
virtual const char* Name() const { return "PerOriginPageReaderFilter"; }
private:
RewriteDriver* driver_;
DISALLOW_COPY_AND_ASSIGN(PerOriginPageReaderFilter);
};
class PerOriginPageReaderFilterCreator
: public TestRewriteDriverFactory::CreateFilterCallback {
public:
PerOriginPageReaderFilterCreator() {}
virtual ~PerOriginPageReaderFilterCreator() {}
virtual HtmlFilter* Done(RewriteDriver* driver) {
return new PerOriginPageReaderFilter(driver);
}
private:
DISALLOW_COPY_AND_ASSIGN(PerOriginPageReaderFilterCreator);
};
virtual void SetUp() {
RewriteOptions* options = server_context()->global_options();
// mobilizer turns on the per-domain page.
options->ClearSignatureForTesting();
options->EnableFilter(RewriteOptions::kMobilize);
ProxyInterfaceTest::SetUp();
}
};
TEST_F(ProxyInterfaceOriginPropertyPageTest, Basic) {
PerOriginPageReaderFilterCreator filter_creator;
factory()->AddCreateFilterCallback(&filter_creator);
ResponseHeaders headers;
GoogleString body;
FetchFromProxy(kPageUrl, true, &body, &headers);
EXPECT_TRUE(HasPrefixString(body, "<!--Site visit:0-->")) << body;
FetchFromProxy(kPageUrl, true, &body, &headers);
EXPECT_TRUE(HasPrefixString(body, "<!--Site visit:1-->")) << body;
// Count increases on a different page, too.
GoogleString other_page = StrCat("totally/different/from/", kPageUrl);
SetResponseWithDefaultHeaders(other_page, kContentTypeHtml,
"<div><p></p></div>", 0);
FetchFromProxy(other_page, true, &body, &headers);
EXPECT_TRUE(HasPrefixString(body, "<!--Site visit:2-->")) << body;
}
TEST_F(ProxyInterfaceOriginPropertyPageTest, PostWithDelayCache) {
// Test for not crashing.
PerOriginPageReaderFilterCreator filter_creator;
factory()->AddCreateFilterCallback(&filter_creator);
RequestHeaders request_headers;
ResponseHeaders headers;
request_headers.set_method(RequestHeaders::kPost);
SetCacheDelayUs(100);
FetchFromProxyNoWait(kPageUrl, request_headers, true /*expect_success*/,
false /*log_flush*/, &headers);
mock_scheduler()->AdvanceTimeUs(200);
WaitForFetch(true);
}
} // namespace net_instaweb