blob: f244c7a5ee29d823034ada8d2912803c6ea64ec6 [file] [log] [blame]
/*
* Copyright 2013 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: slamm@google.com (Stephen Lamm)
#include "net/instaweb/rewriter/public/critical_selector_finder.h"
#include "net/instaweb/rewriter/critical_keys.pb.h"
#include "net/instaweb/rewriter/public/critical_finder_support_util.h"
#include "net/instaweb/rewriter/public/property_cache_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/mock_property_page.h"
#include "net/instaweb/util/public/property_cache.h"
#include "pagespeed/kernel/base/gtest.h"
#include "pagespeed/kernel/base/mock_timer.h"
#include "pagespeed/kernel/base/statistics.h"
#include "pagespeed/kernel/base/timer.h"
namespace net_instaweb {
namespace {
const char kRequestUrl[] = "http://www.example.com";
class CriticalSelectorFinderTest : public RewriteTestBase {
protected:
CriticalSelectorFinderTest() { }
virtual void SetUp() {
RewriteTestBase::SetUp();
const PropertyCache::Cohort* beacon_cohort =
SetupCohort(page_property_cache(), RewriteDriver::kBeaconCohort);
server_context()->set_beacon_cohort(beacon_cohort);
finder_ = CreateFinder(beacon_cohort);
server_context()->set_critical_selector_finder(finder_);
candidates_.insert("#bar");
candidates_.insert(".a");
candidates_.insert(".b");
candidates_.insert("#c");
candidates_.insert(".foo");
ResetDriver();
}
virtual CriticalSelectorFinder* CreateFinder(
const PropertyCache::Cohort* cohort) {
return new BeaconCriticalSelectorFinder(
cohort, factory()->nonce_generator(), statistics());
}
void ResetDriver() {
ClearRewriteDriver();
MockPropertyPage* page = NewMockPage(kRequestUrl);
rewrite_driver()->set_property_page(page);
PropertyCache* pcache = server_context_->page_property_cache();
pcache->Read(page);
}
void WriteToPropertyCache() {
rewrite_driver()->property_page()->WriteCohort(
server_context()->beacon_cohort());
}
void WriteBackAndResetDriver() {
WriteToPropertyCache();
ResetDriver();
SetDriverRequestHeaders();
}
int TimedValue(StringPiece name) {
return statistics()->GetTimedVariable(name)->Get(TimedVariable::START);
}
void CheckCriticalSelectorFinderStats(int hits, int expiries, int not_found) {
EXPECT_EQ(hits, TimedValue(
CriticalSelectorFinder::kCriticalSelectorsValidCount));
EXPECT_EQ(expiries, TimedValue(
CriticalSelectorFinder::kCriticalSelectorsExpiredCount));
EXPECT_EQ(not_found, TimedValue(
CriticalSelectorFinder::kCriticalSelectorsNotFoundCount));
}
GoogleString CriticalSelectorsString() {
WriteBackAndResetDriver();
const StringSet& critical_selectors =
finder_->GetCriticalSelectors(rewrite_driver());
return JoinCollection(critical_selectors, ",");
}
// Write a raw critical selector set to pcache, used to test legacy
// compatibility since new code won't create legacy protos.
void WriteCriticalSelectorSetToPropertyCache(
const CriticalKeys& selector_set) {
PropertyCacheUpdateResult result = UpdateInPropertyCache(
selector_set, server_context()->beacon_cohort(),
CriticalSelectorFinder::kCriticalSelectorsPropertyName, true,
rewrite_driver()->property_page());
ASSERT_EQ(kPropertyCacheUpdateOk, result);
}
void WriteCriticalSelectorsToPropertyCache(const StringSet& selectors) {
finder_->WriteCriticalSelectorsToPropertyCache(
selectors, last_beacon_metadata_.nonce, rewrite_driver());
}
virtual BeaconStatus ExpectedBeaconStatus() {
return kBeaconWithNonce;
}
// Simulate beacon insertion, with candidates_.
void Beacon() {
WriteBackAndResetDriver();
factory()->mock_timer()->AdvanceMs(
options()->beacon_reinstrument_time_sec() * Timer::kSecondMs);
VerifyBeaconStatus(ExpectedBeaconStatus());
}
// Verify that no beacon injection occurs.
void VerifyNoBeaconing() {
VerifyBeaconStatus(kDoNotBeacon);
}
// Verify that beacon injection occurs.
void VerifyBeaconing() {
VerifyBeaconStatus(kBeaconWithNonce);
}
// Helper method used for verifying beacon injection status.
void VerifyBeaconStatus(BeaconStatus status) {
last_beacon_metadata_ =
finder_->PrepareForBeaconInsertion(candidates_, rewrite_driver());
EXPECT_EQ(status, last_beacon_metadata_.status);
if (status == kBeaconWithNonce) {
EXPECT_STREQ(ExpectedNonce(), last_beacon_metadata_.nonce);
} else {
EXPECT_TRUE(last_beacon_metadata_.nonce.empty());
}
}
CriticalKeys* RawCriticalSelectorSet(int expected_size) {
WriteBackAndResetDriver();
finder_->GetCriticalSelectors(rewrite_driver());
CriticalKeys* selectors =
&rewrite_driver()->critical_selector_info()->proto;
if (selectors != NULL) {
EXPECT_EQ(expected_size, selectors->key_evidence_size());
} else {
EXPECT_EQ(expected_size, 0);
}
return selectors;
}
void CheckFooBarBeaconSupport(int support) {
CheckFooBarBeaconSupport(support, support);
}
void CheckFooBarBeaconSupport(int foo_support, int bar_support) {
// Check for .foo and #bar support, with no support for other beaconed
// candidates.
CriticalKeys* read_selectors = RawCriticalSelectorSet(5);
ASSERT_TRUE(read_selectors != NULL);
EXPECT_EQ("#bar", read_selectors->key_evidence(0).key());
EXPECT_EQ(bar_support, read_selectors->key_evidence(0).support());
EXPECT_EQ("#c", read_selectors->key_evidence(1).key());
EXPECT_EQ(0, read_selectors->key_evidence(1).support());
EXPECT_EQ(".a", read_selectors->key_evidence(2).key());
EXPECT_EQ(0, read_selectors->key_evidence(2).support());
EXPECT_EQ(".b", read_selectors->key_evidence(3).key());
EXPECT_EQ(0, read_selectors->key_evidence(3).support());
EXPECT_EQ(".foo", read_selectors->key_evidence(4).key());
EXPECT_EQ(foo_support, read_selectors->key_evidence(4).support());
}
CriticalSelectorFinder* finder_;
StringSet candidates_;
BeaconMetadata last_beacon_metadata_;
};
TEST_F(CriticalSelectorFinderTest, StoreRestore) {
// Before beacon insertion, nothing in pcache.
CheckCriticalSelectorFinderStats(0, 0, 0);
CriticalSelectorInfo* read_selectors =
rewrite_driver()->critical_selector_info();
EXPECT_TRUE(read_selectors == NULL);
StringSet critical_selectors =
finder_->GetCriticalSelectors(rewrite_driver());
read_selectors = rewrite_driver()->critical_selector_info();
EXPECT_TRUE(read_selectors != NULL);
EXPECT_TRUE(critical_selectors.empty());
CheckCriticalSelectorFinderStats(0, 0, 1);
Beacon();
CheckCriticalSelectorFinderStats(0, 0, 2);
StringSet selectors;
selectors.insert(".foo");
selectors.insert("#bar");
WriteCriticalSelectorsToPropertyCache(selectors);
CheckFooBarBeaconSupport(finder_->SupportInterval());
CheckCriticalSelectorFinderStats(1, 0, 2);
// Now test expiration.
WriteBackAndResetDriver();
AdvanceTimeMs(2 * options()->finder_properties_cache_expiration_time_ms());
read_selectors = rewrite_driver()->critical_selector_info();
EXPECT_TRUE(read_selectors == NULL);
critical_selectors = finder_->GetCriticalSelectors(rewrite_driver());
CheckCriticalSelectorFinderStats(1, 1, 2);
}
// Verify that writing multiple beacon results are stored and aggregated. The
// critical selector set should contain all selectors seen in the last
// SupportInterval() beacon responses. After SupportInterval() responses,
// beacon results only seen once should no longer be considered critical.
TEST_F(CriticalSelectorFinderTest, StoreMultiple) {
Beacon();
StringSet selectors;
selectors.insert(".a");
WriteCriticalSelectorsToPropertyCache(selectors);
EXPECT_STREQ(".a", CriticalSelectorsString());
selectors.clear();
selectors.insert(".b");
for (int i = 0; i < finder_->SupportInterval() - 1; ++i) {
Beacon();
WriteCriticalSelectorsToPropertyCache(selectors);
EXPECT_STREQ(".a,.b", CriticalSelectorsString());
// We are sending enough beacons with the same selector set here that we
// will enter low frequency beaconing mode, so advance time more to ensure
// rebeaconing actually occurs.
factory()->mock_timer()->AdvanceMs(
options()->beacon_reinstrument_time_sec() * Timer::kSecondMs *
kLowFreqBeaconMult);
}
// We send one more beacon response, which should kick .a out of the critical
// selector set.
Beacon();
selectors.clear();
selectors.insert("#c");
WriteCriticalSelectorsToPropertyCache(selectors);
EXPECT_STREQ("#c,.b", CriticalSelectorsString());
}
// Make sure beacon results can arrive out of order (so long as the nonce
// doesn't time out).
TEST_F(CriticalSelectorFinderTest, OutOfOrder) {
// Make sure that the rebeaconing time is less than the time a nonce is valid,
// so that we can test having multiple outstanding nonces.
options()->set_beacon_reinstrument_time_sec(kBeaconTimeoutIntervalMs /
Timer::kSecondMs / 2);
Beacon();
GoogleString initial_nonce(last_beacon_metadata_.nonce);
// A second beacon occurs and the result comes back first.
Beacon();
StringSet selectors;
selectors.insert(".a");
WriteCriticalSelectorsToPropertyCache(selectors);
EXPECT_STREQ(".a", CriticalSelectorsString());
// Now the first beacon result comes back out of order. It should still work.
selectors.clear();
selectors.insert(".b");
finder_->WriteCriticalSelectorsToPropertyCache(
selectors, initial_nonce, rewrite_driver());
EXPECT_STREQ(".a,.b", CriticalSelectorsString());
// A duplicate beacon nonce will be dropped.
selectors.clear();
selectors.insert("#c");
finder_->WriteCriticalSelectorsToPropertyCache(
selectors, initial_nonce, rewrite_driver());
EXPECT_STREQ(".a,.b", CriticalSelectorsString());
// As will an entirely bogus nonce (here we use non-base64 characters).
const char kBogusNonce[] = "*&*";
finder_->WriteCriticalSelectorsToPropertyCache(
selectors, kBogusNonce, rewrite_driver());
EXPECT_STREQ(".a,.b", CriticalSelectorsString());
}
TEST_F(CriticalSelectorFinderTest, NonceTimeout) {
// Make sure that beacons time out after kBeaconTimeoutIntervalMs.
Beacon();
GoogleString initial_nonce(last_beacon_metadata_.nonce);
// beacon_reinstrument_time_sec() passes (in mock time) before the next call
// completes:
Beacon();
factory()->mock_timer()->AdvanceMs(kBeaconTimeoutIntervalMs);
StringSet selectors;
selectors.insert(".a");
// This beacon arrives right at its deadline, and is OK.
WriteCriticalSelectorsToPropertyCache(selectors);
EXPECT_STREQ(".a", CriticalSelectorsString());
// The first beacon arrives after its deadline, and is dropped.
selectors.clear();
selectors.insert(".b");
finder_->WriteCriticalSelectorsToPropertyCache(
selectors, initial_nonce, rewrite_driver());
EXPECT_STREQ(".a", CriticalSelectorsString());
}
// Make sure that inserting a non-candidate critical selector has no effect.
TEST_F(CriticalSelectorFinderTest, StoreNonCandidate) {
Beacon();
StringSet selectors;
selectors.insert(".a");
selectors.insert(".noncandidate");
selectors.insert("#noncandidate");
WriteCriticalSelectorsToPropertyCache(selectors);
EXPECT_STREQ(".a", CriticalSelectorsString());
}
// Make sure we aggregate duplicate beacon results.
TEST_F(CriticalSelectorFinderTest, DuplicateEntries) {
Beacon();
StringSet beacon_result;
beacon_result.insert("#bar");
beacon_result.insert(".foo");
beacon_result.insert(".a");
WriteCriticalSelectorsToPropertyCache(beacon_result);
Beacon();
beacon_result.clear();
beacon_result.insert("#bar");
beacon_result.insert(".foo");
beacon_result.insert(".b");
WriteCriticalSelectorsToPropertyCache(beacon_result);
// Now cross-check the critical selector set.
CriticalKeys* read_selectors = RawCriticalSelectorSet(5);
ASSERT_TRUE(read_selectors != NULL);
EXPECT_EQ("#bar", read_selectors->key_evidence(0).key());
EXPECT_EQ("#c", read_selectors->key_evidence(1).key());
EXPECT_EQ(".a", read_selectors->key_evidence(2).key());
EXPECT_EQ(".b", read_selectors->key_evidence(3).key());
EXPECT_EQ(".foo", read_selectors->key_evidence(4).key());
EXPECT_EQ(2 * finder_->SupportInterval() - 1,
read_selectors->key_evidence(0).support());
EXPECT_EQ(0, read_selectors->key_evidence(1).support());
EXPECT_EQ(finder_->SupportInterval() - 1,
read_selectors->key_evidence(2).support());
EXPECT_EQ(finder_->SupportInterval(),
read_selectors->key_evidence(3).support());
EXPECT_EQ(2 * finder_->SupportInterval() - 1,
read_selectors->key_evidence(4).support());
}
// Make sure overflow of evidence can't happen, otherwise an attacker can
// convince us CSS is so critical it's not critical at all.
TEST_F(CriticalSelectorFinderTest, EvidenceOverflow) {
// Set up pcache entry to be ready to overflow.
CriticalKeys selectors;
CriticalKeys::KeyEvidence* evidence = selectors.add_key_evidence();
evidence->set_key(".a");
evidence->set_support(kint32max);
WriteCriticalSelectorSetToPropertyCache(selectors);
// Now create a new critical selector set and add it repeatedly.
StringSet new_selectors;
new_selectors.insert(".a");
for (int i = 0; i < finder_->SupportInterval(); ++i) {
Beacon();
WriteCriticalSelectorsToPropertyCache(new_selectors);
EXPECT_STREQ(".a", CriticalSelectorsString());
// We are sending enough beacons with the same selector set here that we
// will enter low frequency beaconing mode, so advance time more to ensure
// rebeaconing actually occurs.
factory()->mock_timer()->AdvanceMs(
options()->beacon_reinstrument_time_sec() * Timer::kSecondMs *
kLowFreqBeaconMult);
}
}
// Make sure we don't beacon if we have an empty set of candidate selectors.
TEST_F(CriticalSelectorFinderTest, NoCandidatesNoBeacon) {
WriteBackAndResetDriver();
StringSet empty;
BeaconMetadata last_beacon_metadata =
finder_->PrepareForBeaconInsertion(empty, rewrite_driver());
EXPECT_EQ(kDoNotBeacon, last_beacon_metadata.status);
}
TEST_F(CriticalSelectorFinderTest, DontRebeaconBeforeTimeout) {
Beacon();
// Now simulate a beacon insertion attempt without timing out.
WriteBackAndResetDriver();
factory()->mock_timer()->AdvanceMs(options()->beacon_reinstrument_time_sec() *
Timer::kSecondMs / 2);
BeaconMetadata last_beacon_metadata =
finder_->PrepareForBeaconInsertion(candidates_, rewrite_driver());
EXPECT_EQ(kDoNotBeacon, last_beacon_metadata.status);
// But we'll re-beacon if some more time passes.
Beacon(); // beacon_reinstrument_time_sec() passes in Beacon() call.
}
TEST_F(CriticalSelectorFinderTest, RebeaconBeforeTimeoutWithHeader) {
Beacon();
// Write a dummy value to the property cache.
WriteToPropertyCache();
// If downstream caching is disabled, any beaconing key configuration and/or
// presence of PS-ShouldBeacon header should be ignored. In such situations,
// unless the reinstrumentation time interval is exceeded, beacon injection
// should not happen.
ResetDriver();
SetDownstreamCacheDirectives("", "", kConfiguredBeaconingKey);
SetShouldBeaconHeader(kConfiguredBeaconingKey);
VerifyNoBeaconing();
// Advance the timer past the beacon interval.
factory()->mock_timer()->AdvanceMs(options()->beacon_reinstrument_time_sec() *
Timer::kSecondMs + 1);
// When the reinstrumentation time interval is exceeded, beacon injection
// should happen as usual.
ResetDriver();
SetDownstreamCacheDirectives("", "", kConfiguredBeaconingKey);
SetShouldBeaconHeader(kConfiguredBeaconingKey);
VerifyBeaconing();
// Beacon injection should not happen when rebeaconing key is not configured.
ResetDriver();
SetDownstreamCacheDirectives("", "localhost:80", "");
SetShouldBeaconHeader(kConfiguredBeaconingKey);
VerifyNoBeaconing();
// Beacon injection should not happen when the PS-ShouldBeacon header is
// absent and both downstream caching and the associated rebeaconing key
// are configured.
ResetDriver();
SetDownstreamCacheDirectives("", "localhost:80", kConfiguredBeaconingKey);
SetDriverRequestHeaders();
VerifyNoBeaconing();
// Beacon injection should not happen when the PS-ShouldBeacon header is
// incorrect.
ResetDriver();
SetDownstreamCacheDirectives("", "localhost:80", kConfiguredBeaconingKey);
SetShouldBeaconHeader(kWrongBeaconingKey);
VerifyNoBeaconing();
// Beacon injection happens when the PS-ShouldBeacon header is present even
// when the pcache value has not expired and the reinstrumentation time
// interval has not been exceeded.
ResetDriver();
SetDownstreamCacheDirectives("", "localhost:80", kConfiguredBeaconingKey);
SetShouldBeaconHeader(kConfiguredBeaconingKey);
VerifyBeaconing();
// Advance the timer past the beacon interval.
factory()->mock_timer()->AdvanceMs(
options()->beacon_reinstrument_time_sec() * Timer::kSecondMs + 1);
// Beacon injection should happen after reinstrumentation time interval has
// passed when downstream caching is enabled but rebeaconing key is not
// configured.
ResetDriver();
SetDownstreamCacheDirectives("", "localhost:80", "");
SetShouldBeaconHeader(kConfiguredBeaconingKey);
VerifyBeaconing();
// Advance the timer past the beacon interval.
factory()->mock_timer()->AdvanceMs(
options()->beacon_reinstrument_time_sec() * Timer::kSecondMs + 1);
// Beacon injection should not happen when the PS-ShouldBeacon header is
// incorrect even if the reinstrumentation time interval has been exceeded.
ResetDriver();
SetDownstreamCacheDirectives("", "localhost:80", kConfiguredBeaconingKey);
SetShouldBeaconHeader(kWrongBeaconingKey);
VerifyNoBeaconing();
}
// If ShouldReplacePriorResult returns true, then a beacon result
// replaces any previous results.
class UnverifiedCriticalSelectorFinder : public CriticalSelectorFinder {
public:
UnverifiedCriticalSelectorFinder(const PropertyCache::Cohort* cohort,
Statistics* stats)
: CriticalSelectorFinder(cohort, NULL, stats) {}
virtual ~UnverifiedCriticalSelectorFinder() {}
virtual int SupportInterval() const { return 10; }
protected:
virtual bool ShouldReplacePriorResult() const { return true; }
private:
DISALLOW_COPY_AND_ASSIGN(UnverifiedCriticalSelectorFinder);
};
// Test that unverified results apply.
class UnverifiedSelectorsTest : public CriticalSelectorFinderTest {
protected:
virtual BeaconStatus ExpectedBeaconStatus() {
return kBeaconNoNonce;
}
virtual CriticalSelectorFinder* CreateFinder(
const PropertyCache::Cohort* cohort) {
return new UnverifiedCriticalSelectorFinder(cohort, statistics());
}
};
TEST_F(UnverifiedSelectorsTest, NonCandidatesAreStored) {
Beacon();
StringSet selectors;
selectors.insert(".a");
selectors.insert(".noncandidate");
selectors.insert("#noncandidate");
finder_->WriteCriticalSelectorsToPropertyCache(
selectors, NULL /* no nonce */, rewrite_driver());
EXPECT_STREQ("#noncandidate,.a,.noncandidate", CriticalSelectorsString());
}
// Each beacon replaces previous results.
TEST_F(UnverifiedSelectorsTest, MultipleResultsReplace) {
Beacon();
StringSet selectors;
selectors.insert(".noncandidate");
finder_->WriteCriticalSelectorsToPropertyCache(
selectors, NULL /* no nonce */, rewrite_driver());
EXPECT_STREQ(".noncandidate", CriticalSelectorsString());
selectors.clear();
selectors.insert(".another");
Beacon();
finder_->WriteCriticalSelectorsToPropertyCache(
selectors, NULL /* no nonce */, rewrite_driver());
EXPECT_STREQ(".another", CriticalSelectorsString());
}
} // namespace
} // namespace net_instaweb