blob: c0f2c8bf48b4cafa43e88c5e8b1bcad1f8031892 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <list>
#include <set>
#include <string>
#include "base/macros.h"
#include "base/scoped_observer.h"
#include "chrome/browser/extensions/extension_browsertest.h"
#include "chrome/common/chrome_switches.h"
#include "content/public/test/test_utils.h"
#include "extensions/browser/content_verifier.h"
#include "extensions/browser/content_verify_job.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_registry_observer.h"
namespace extensions {
namespace {
// Helper for observing extension unloads.
class UnloadObserver : public ExtensionRegistryObserver {
public:
explicit UnloadObserver(ExtensionRegistry* registry) : observer_(this) {
observer_.Add(registry);
}
~UnloadObserver() override {}
void WaitForUnload(const ExtensionId& id) {
if (ContainsKey(observed_, id))
return;
ASSERT_TRUE(loop_runner_.get() == NULL);
awaited_id_ = id;
loop_runner_ = new content::MessageLoopRunner();
loop_runner_->Run();
}
void OnExtensionUnloaded(content::BrowserContext* browser_context,
const Extension* extension,
UnloadedExtensionInfo::Reason reason) override {
observed_.insert(extension->id());
if (awaited_id_ == extension->id())
loop_runner_->Quit();
}
private:
ExtensionId awaited_id_;
std::set<ExtensionId> observed_;
scoped_refptr<content::MessageLoopRunner> loop_runner_;
ScopedObserver<ExtensionRegistry, UnloadObserver> observer_;
};
// Helper for forcing ContentVerifyJob's to return an error.
class JobDelegate : public ContentVerifyJob::TestDelegate {
public:
JobDelegate()
: fail_next_read_(false),
fail_next_done_(false),
bytes_read_failed_(0),
done_reading_failed_(0) {}
virtual ~JobDelegate() {}
void set_id(const ExtensionId& id) { id_ = id; }
void fail_next_read() { fail_next_read_ = true; }
void fail_next_done() { fail_next_done_ = true; }
// Return the number of BytesRead/DoneReading calls we actually failed,
// respectively.
int bytes_read_failed() { return bytes_read_failed_; }
int done_reading_failed() { return done_reading_failed_; }
ContentVerifyJob::FailureReason BytesRead(const ExtensionId& id,
int count,
const char* data) override {
if (id == id_ && fail_next_read_) {
fail_next_read_ = false;
bytes_read_failed_++;
return ContentVerifyJob::HASH_MISMATCH;
}
return ContentVerifyJob::NONE;
}
ContentVerifyJob::FailureReason DoneReading(const ExtensionId& id) override {
if (id == id_ && fail_next_done_) {
fail_next_done_ = false;
done_reading_failed_++;
return ContentVerifyJob::HASH_MISMATCH;
}
return ContentVerifyJob::NONE;
}
private:
ExtensionId id_;
bool fail_next_read_;
bool fail_next_done_;
int bytes_read_failed_;
int done_reading_failed_;
DISALLOW_COPY_AND_ASSIGN(JobDelegate);
};
class JobObserver : public ContentVerifyJob::TestObserver {
public:
JobObserver();
virtual ~JobObserver();
enum class Result { SUCCESS, FAILURE };
// Call this to add an expected job result.
void ExpectJobResult(const std::string& extension_id,
const base::FilePath& relative_path,
Result expected_result);
// Wait to see expected jobs. Returns true when we've seen all expected jobs
// finish, or false if there was an error or timeout.
bool WaitForExpectedJobs();
// ContentVerifyJob::TestObserver interface
void JobStarted(const std::string& extension_id,
const base::FilePath& relative_path) override;
void JobFinished(const std::string& extension_id,
const base::FilePath& relative_path,
bool failed) override;
private:
struct ExpectedResult {
public:
std::string extension_id;
base::FilePath path;
Result result;
ExpectedResult(const std::string& extension_id, const base::FilePath& path,
Result result) {
this->extension_id = extension_id;
this->path = path;
this->result = result;
}
};
std::list<ExpectedResult> expectations_;
content::BrowserThread::ID creation_thread_;
scoped_refptr<content::MessageLoopRunner> loop_runner_;
};
void JobObserver::ExpectJobResult(const std::string& extension_id,
const base::FilePath& relative_path,
Result expected_result) {
expectations_.push_back(ExpectedResult(
extension_id, relative_path, expected_result));
}
JobObserver::JobObserver() {
EXPECT_TRUE(
content::BrowserThread::GetCurrentThreadIdentifier(&creation_thread_));
}
JobObserver::~JobObserver() {
}
bool JobObserver::WaitForExpectedJobs() {
EXPECT_TRUE(content::BrowserThread::CurrentlyOn(creation_thread_));
if (!expectations_.empty()) {
loop_runner_ = new content::MessageLoopRunner();
loop_runner_->Run();
loop_runner_ = nullptr;
}
return expectations_.empty();
}
void JobObserver::JobStarted(const std::string& extension_id,
const base::FilePath& relative_path) {
}
void JobObserver::JobFinished(const std::string& extension_id,
const base::FilePath& relative_path,
bool failed) {
if (!content::BrowserThread::CurrentlyOn(creation_thread_)) {
content::BrowserThread::PostTask(
creation_thread_, FROM_HERE,
base::Bind(&JobObserver::JobFinished, base::Unretained(this),
extension_id, relative_path, failed));
return;
}
Result result = failed ? Result::FAILURE : Result::SUCCESS;
bool found = false;
for (std::list<ExpectedResult>::iterator i = expectations_.begin();
i != expectations_.end(); ++i) {
if (i->extension_id == extension_id && i->path == relative_path &&
i->result == result) {
found = true;
expectations_.erase(i);
break;
}
}
if (found) {
if (expectations_.empty() && loop_runner_.get())
loop_runner_->Quit();
} else {
LOG(WARNING) << "Ignoring unexpected JobFinished " << extension_id << "/"
<< relative_path.value() << " failed:" << failed;
}
}
class VerifierObserver : public ContentVerifier::TestObserver {
public:
VerifierObserver();
virtual ~VerifierObserver();
const std::set<std::string>& completed_fetches() {
return completed_fetches_;
}
// Returns when we've seen OnFetchComplete for |extension_id|.
void WaitForFetchComplete(const std::string& extension_id);
// ContentVerifier::TestObserver
void OnFetchComplete(const std::string& extension_id, bool success) override;
private:
std::set<std::string> completed_fetches_;
std::string id_to_wait_for_;
scoped_refptr<content::MessageLoopRunner> loop_runner_;
};
VerifierObserver::VerifierObserver() {
}
VerifierObserver::~VerifierObserver() {
}
void VerifierObserver::WaitForFetchComplete(const std::string& extension_id) {
EXPECT_TRUE(id_to_wait_for_.empty());
EXPECT_EQ(loop_runner_.get(), nullptr);
id_to_wait_for_ = extension_id;
loop_runner_ = new content::MessageLoopRunner();
loop_runner_->Run();
id_to_wait_for_.clear();
loop_runner_ = nullptr;
}
void VerifierObserver::OnFetchComplete(const std::string& extension_id,
bool success) {
completed_fetches_.insert(extension_id);
if (extension_id == id_to_wait_for_)
loop_runner_->Quit();
}
} // namespace
class ContentVerifierTest : public ExtensionBrowserTest {
public:
ContentVerifierTest() {}
~ContentVerifierTest() override {}
void SetUpCommandLine(base::CommandLine* command_line) override {
ExtensionBrowserTest::SetUpCommandLine(command_line);
command_line->AppendSwitchASCII(
switches::kExtensionContentVerification,
switches::kExtensionContentVerificationEnforce);
}
void SetUpOnMainThread() override {
ExtensionBrowserTest::SetUpOnMainThread();
}
void TearDownOnMainThread() override {
ContentVerifier::SetObserverForTests(NULL);
ContentVerifyJob::SetDelegateForTests(NULL);
ContentVerifyJob::SetObserverForTests(NULL);
ExtensionBrowserTest::TearDownOnMainThread();
}
virtual void OpenPageAndWaitForUnload() {
ContentVerifyJob::SetDelegateForTests(&delegate_);
std::string id = "npnbmohejbjohgpjnmjagbafnjhkmgko";
delegate_.set_id(id);
unload_observer_.reset(
new UnloadObserver(ExtensionRegistry::Get(profile())));
const Extension* extension = InstallExtensionFromWebstore(
test_data_dir_.AppendASCII("content_verifier/v1.crx"), 1);
ASSERT_TRUE(extension);
ASSERT_EQ(id, extension->id());
page_url_ = extension->GetResourceURL("page.html");
// This call passes false for |check_navigation_success|, because checking
// for navigation success needs the WebContents to still exist after the
// navigation, whereas this navigation triggers an unload which destroys
// the WebContents.
AddTabAtIndexToBrowser(browser(), 1, page_url_, ui::PAGE_TRANSITION_LINK,
false);
unload_observer_->WaitForUnload(id);
ExtensionPrefs* prefs = ExtensionPrefs::Get(profile());
int reasons = prefs->GetDisableReasons(id);
EXPECT_TRUE(reasons & Extension::DISABLE_CORRUPTED);
// This needs to happen before the ExtensionRegistry gets deleted, which
// happens before TearDownOnMainThread is called.
unload_observer_.reset();
}
protected:
JobDelegate delegate_;
scoped_ptr<UnloadObserver> unload_observer_;
GURL page_url_;
};
IN_PROC_BROWSER_TEST_F(ContentVerifierTest, FailOnRead) {
EXPECT_EQ(0, delegate_.bytes_read_failed());
delegate_.fail_next_read();
OpenPageAndWaitForUnload();
EXPECT_EQ(1, delegate_.bytes_read_failed());
}
IN_PROC_BROWSER_TEST_F(ContentVerifierTest, FailOnDone) {
EXPECT_EQ(0, delegate_.done_reading_failed());
delegate_.fail_next_done();
OpenPageAndWaitForUnload();
EXPECT_EQ(1, delegate_.done_reading_failed());
}
IN_PROC_BROWSER_TEST_F(ContentVerifierTest, DotSlashPaths) {
JobObserver job_observer;
ContentVerifyJob::SetObserverForTests(&job_observer);
std::string id = "hoipipabpcoomfapcecilckodldhmpgl";
job_observer.ExpectJobResult(
id, base::FilePath(FILE_PATH_LITERAL("background.js")),
JobObserver::Result::SUCCESS);
job_observer.ExpectJobResult(id,
base::FilePath(FILE_PATH_LITERAL("page.html")),
JobObserver::Result::SUCCESS);
job_observer.ExpectJobResult(id, base::FilePath(FILE_PATH_LITERAL("page.js")),
JobObserver::Result::SUCCESS);
job_observer.ExpectJobResult(
id, base::FilePath(FILE_PATH_LITERAL("dir/page2.html")),
JobObserver::Result::SUCCESS);
job_observer.ExpectJobResult(id,
base::FilePath(FILE_PATH_LITERAL("page2.js")),
JobObserver::Result::SUCCESS);
job_observer.ExpectJobResult(id, base::FilePath(FILE_PATH_LITERAL("cs1.js")),
JobObserver::Result::SUCCESS);
job_observer.ExpectJobResult(id, base::FilePath(FILE_PATH_LITERAL("cs2.js")),
JobObserver::Result::SUCCESS);
VerifierObserver verifier_observer;
ContentVerifier::SetObserverForTests(&verifier_observer);
// Install a test extension we copied from the webstore that has actual
// signatures, and contains paths with a leading "./" in various places.
const Extension* extension = InstallExtensionFromWebstore(
test_data_dir_.AppendASCII("content_verifier/dot_slash_paths.crx"), 1);
ASSERT_TRUE(extension);
ASSERT_EQ(extension->id(), id);
// The content scripts might fail verification the first time since the
// one-time processing might not be finished yet - if that's the case then
// we want to wait until that work is done.
if (!ContainsKey(verifier_observer.completed_fetches(), id))
verifier_observer.WaitForFetchComplete(id);
// Now disable/re-enable the extension to cause the content scripts to be
// read again.
DisableExtension(id);
EnableExtension(id);
EXPECT_TRUE(job_observer.WaitForExpectedJobs());
ContentVerifyJob::SetObserverForTests(NULL);
}
IN_PROC_BROWSER_TEST_F(ContentVerifierTest, ContentScripts) {
VerifierObserver verifier_observer;
ContentVerifier::SetObserverForTests(&verifier_observer);
// Install an extension with content scripts. The initial read of the content
// scripts will fail verification because they are read before the content
// verification system has completed a one-time processing of the expected
// hashes. (The extension only contains the root level hashes of the merkle
// tree, but the content verification system builds the entire tree and
// caches it in the extension install directory - see ContentHashFetcher for
// more details).
std::string id = "jmllhlobpjcnnomjlipadejplhmheiif";
const Extension* extension = InstallExtensionFromWebstore(
test_data_dir_.AppendASCII("content_verifier/content_script.crx"), 1);
ASSERT_TRUE(extension);
ASSERT_EQ(extension->id(), id);
// Wait for the content verification code to finish processing the hashes.
if (!ContainsKey(verifier_observer.completed_fetches(), id))
verifier_observer.WaitForFetchComplete(id);
// Now disable the extension, since content scripts are read at enable time,
// set up our job observer, and re-enable, expecting a success this time.
DisableExtension(id);
JobObserver job_observer;
ContentVerifyJob::SetObserverForTests(&job_observer);
job_observer.ExpectJobResult(id,
base::FilePath(FILE_PATH_LITERAL("script.js")),
JobObserver::Result::SUCCESS);
EnableExtension(id);
EXPECT_TRUE(job_observer.WaitForExpectedJobs());
// Now alter the contents of the content script, reload the extension, and
// expect to see a job failure due to the content script content hash not
// being what was signed by the webstore.
base::FilePath scriptfile = extension->path().AppendASCII("script.js");
std::string extra = "some_extra_function_call();";
ASSERT_TRUE(base::AppendToFile(scriptfile, extra.data(), extra.size()));
DisableExtension(id);
job_observer.ExpectJobResult(id,
base::FilePath(FILE_PATH_LITERAL("script.js")),
JobObserver::Result::FAILURE);
EnableExtension(id);
EXPECT_TRUE(job_observer.WaitForExpectedJobs());
ContentVerifyJob::SetObserverForTests(NULL);
}
} // namespace extensions