910e62b5创建于 1月15日历史提交
// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "extensions/browser/api/storage/storage_api.h"

#include <stdint.h>

#include <limits>
#include <memory>
#include <set>

#include "base/command_line.h"
#include "base/containers/contains.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/memory/ref_counted.h"
#include "base/strings/stringprintf.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "components/crx_file/id_util.h"
#include "components/value_store/leveldb_value_store.h"
#include "components/value_store/value_store.h"
#include "components/value_store/value_store_factory_impl.h"
#include "content/public/test/mock_render_process_host.h"
#include "content/public/test/test_browser_context.h"
#include "extensions/browser/api/extensions_api_client.h"
#include "extensions/browser/api/storage/settings_storage_quota_enforcer.h"
#include "extensions/browser/api/storage/settings_test_util.h"
#include "extensions/browser/api/storage/storage_frontend.h"
#include "extensions/browser/api/storage/storage_utils.h"
#include "extensions/browser/api/storage/value_store_cache.h"
#include "extensions/browser/api_test_utils.h"
#include "extensions/browser/api_unittest.h"
#include "extensions/browser/event_router.h"
#include "extensions/browser/event_router_factory.h"
#include "extensions/browser/extension_function.h"
#include "extensions/browser/test_event_router_observer.h"
#include "extensions/browser/test_extensions_browser_client.h"
#include "extensions/common/api/storage.h"
#include "extensions/common/manifest.h"
#include "third_party/leveldatabase/src/include/leveldb/db.h"
#include "third_party/leveldatabase/src/include/leveldb/write_batch.h"

namespace extensions {

namespace {

// Caller owns the returned object.
std::unique_ptr<KeyedService> CreateStorageFrontendForTesting(
    content::BrowserContext* context) {
  scoped_refptr<value_store::ValueStoreFactory> factory =
      new value_store::ValueStoreFactoryImpl(context->GetPath());
  return StorageFrontend::CreateForTesting(factory, context);
}

std::unique_ptr<KeyedService> BuildEventRouter(
    content::BrowserContext* context) {
  return std::make_unique<extensions::EventRouter>(context, nullptr);
}

// A fake ValueStore that can be used to mock a ValueStore for testing.
class FakeValueStore : public value_store::ValueStore {
 public:
  FakeValueStore() = default;
  // Constructor for GetBytesInUseIntOverflow test.
  explicit FakeValueStore(size_t bytes_in_use) : bytes_in_use_(bytes_in_use) {}
  // Constructor for GetOperationExceedsSizeLimit test.
  explicit FakeValueStore(base::Value::Dict large_value)
      : large_value_(std::move(large_value)) {}

  // value_store::ValueStore:
  size_t GetBytesInUse(const std::string& key) override {
    return bytes_in_use_;
  }
  size_t GetBytesInUse(const std::vector<std::string>& keys) override {
    return bytes_in_use_;
  }
  size_t GetBytesInUse() override { return bytes_in_use_; }
  ReadResult Get(const std::vector<std::string>& keys) override {
    return ReadResult(large_value_.Clone(), Status());
  }

  // The following methods are not used in tests and are not implemented.
  ReadResult GetKeys() override { NOTREACHED(); }
  ReadResult Get(const std::string& key) override { NOTREACHED(); }
  ReadResult Get() override { NOTREACHED(); }
  WriteResult Set(WriteOptions options,
                  const std::string& key,
                  const base::Value& value) override {
    NOTREACHED();
  }
  WriteResult Set(WriteOptions options,
                  const base::Value::Dict& values) override {
    NOTREACHED();
  }
  WriteResult Remove(const std::string& key) override { NOTREACHED(); }
  WriteResult Remove(const std::vector<std::string>& keys) override {
    NOTREACHED();
  }
  WriteResult Clear() override { NOTREACHED(); }

 private:
  size_t bytes_in_use_ = 0;
  base::Value::Dict large_value_;
};

// A fake ValueStoreCache that we can assign to a storage area in the
// StorageFrontend. This allows us to call StorageFrontend using an extension
// API and access our mock ValueStore.
class FakeValueStoreCache : public ValueStoreCache {
 public:
  explicit FakeValueStoreCache(FakeValueStore&& store)
      : store_(std::move(store)) {}

  // ValueStoreCache:
  void ShutdownOnUI() override {}
  void RunWithValueStoreForExtension(
      StorageCallback callback,
      scoped_refptr<const Extension> extension) override {
    std::move(callback).Run(&store_);
  }
  void DeleteStorageSoon(const ExtensionId& extension_id) override {}

 private:
  FakeValueStore store_;
};

}  // namespace

class StorageApiUnittest : public ApiUnitTest {
 public:
  StorageApiUnittest() = default;
  ~StorageApiUnittest() override = default;

 protected:
  void SetUp() override {
    ApiUnitTest::SetUp();

    EventRouterFactory::GetInstance()->SetTestingFactory(
        browser_context(), base::BindRepeating(&BuildEventRouter));

    // Ensure a StorageFrontend can be created on demand. The StorageFrontend
    // will be owned by the KeyedService system.
    StorageFrontend::GetFactoryInstance()->SetTestingFactory(
        browser_context(),
        base::BindRepeating(&CreateStorageFrontendForTesting));

    render_process_host_ =
        std::make_unique<content::MockRenderProcessHost>(browser_context());
  }

  void TearDown() override {
    render_process_host_.reset();
    ApiUnitTest::TearDown();
  }

  content::RenderProcessHost* render_process_host() const {
    return render_process_host_.get();
  }

  // Runs the storage.set() API function with local storage.
  void RunSetFunction(const std::string& key, const std::string& value) {
    scoped_refptr<StorageStorageAreaSetFunction> function =
        base::MakeRefCounted<StorageStorageAreaSetFunction>();
    function->set_source_context_type(mojom::ContextType::kPrivilegedExtension);
    RunFunction(function.get(),
                base::StringPrintf("[\"local\", {\"%s\": \"%s\"}]", key.c_str(),
                                   value.c_str()));
  }

  testing::AssertionResult RunSetFunctionWithContextAndExpectSuccess(
      const std::string& storage_area_name,
      const std::string& key,
      const std::string& value,
      mojom::ContextType context_type) {
    scoped_refptr<StorageStorageAreaSetFunction> function =
        base::MakeRefCounted<StorageStorageAreaSetFunction>();
    function->set_source_context_type(context_type);
    function->set_extension(extension());
    std::string args = base::StringPrintf("[\"%s\", {\"%s\": \"%s\"}]",
                                          storage_area_name.c_str(),
                                          key.c_str(), value.c_str());

    api_test_utils::RunFunction(function.get(), args, browser_context());
    std::string error = function->GetError();

    if (error.empty()) {
      return testing::AssertionSuccess();
    }
    return testing::AssertionFailure()
           << "Function failed unexpectedly for " << storage_area_name
           << " with context " << static_cast<int>(context_type)
           << ". Error: " << error;
  }

  testing::AssertionResult RunSetFunctionWithContextAndExpectError(
      const std::string& storage_area_name,
      const std::string& key,
      const std::string& value,
      mojom::ContextType context_type,
      const std::string& expected_error_substring) {
    scoped_refptr<StorageStorageAreaSetFunction> function =
        base::MakeRefCounted<StorageStorageAreaSetFunction>();
    function->set_source_context_type(context_type);
    function->set_extension(extension());
    std::string args = base::StringPrintf("[\"%s\", {\"%s\": \"%s\"}]",
                                          storage_area_name.c_str(),
                                          key.c_str(), value.c_str());

    std::string error = RunFunctionAndReturnError(function.get(), args);
    if (error.empty()) {
      return testing::AssertionFailure()
             << "Expected error containing '" << expected_error_substring
             << "' but function succeeded for " << storage_area_name
             << " with context " << static_cast<int>(context_type);
    }
    if (error.find(expected_error_substring) != std::string::npos) {
      return testing::AssertionSuccess();
    }
    return testing::AssertionFailure()
           << "Expected error containing '" << expected_error_substring
           << "' for " << storage_area_name << " with context "
           << static_cast<int>(context_type) << " but got error: '" << error
           << "'.";
  }

  // Runs the storage.get() API function with the local storage, and populates
  // |out_value| with the string result.
  testing::AssertionResult RunGetFunction(const std::string& key,
                                          std::string* out_value) {
    scoped_refptr<StorageStorageAreaGetFunction> function =
        base::MakeRefCounted<StorageStorageAreaGetFunction>();
    function->set_source_context_type(mojom::ContextType::kPrivilegedExtension);
    std::optional<base::Value> result = RunFunctionAndReturnValue(
        function.get(), base::StringPrintf("[\"local\", \"%s\"]", key.c_str()));
    if (!result) {
      return testing::AssertionFailure() << "No result";
    }

    const base::Value::Dict* dict = result->GetIfDict();
    if (!dict) {
      return testing::AssertionFailure() << *result << " was not a dictionary.";
    }

    const std::string* dict_value = dict->FindString(key);
    if (!dict_value) {
      return testing::AssertionFailure() << " could not retrieve a string from"
          << dict << " at " << key;
    }
    *out_value = *dict_value;

    return testing::AssertionSuccess();
  }

  ExtensionsAPIClient extensions_api_client_;
  std::unique_ptr<content::RenderProcessHost> render_process_host_;
};

TEST_F(StorageApiUnittest,
       StorageAreaAccessControlByContextTypeAndAccessLevel) {
  const std::string kAccessErrorMsg =
      "Access to storage is not allowed from this context.";

  const struct {
    const char* area_name;
    StorageAreaNamespace area_namespace;
  } areas_to_test[] = {
      // `local` and `sync` have similar access control logic and both have
      // untrusted access level by default. Since sync isn't available in this
      // instance of Chrome, we won't test it here.
      {"local", StorageAreaNamespace::kLocal},
      {"session", StorageAreaNamespace::kSession},
  };

  for (const auto& area_info : areas_to_test) {
    // Test Case 1: AccessLevel = TRUSTED_CONTEXTS
    storage_utils::SetAccessLevelForArea(
        extension()->id(), *browser_context(), area_info.area_namespace,
        api::storage::AccessLevel::kTrustedContexts);

    // Privileged context should succeed.
    EXPECT_TRUE(RunSetFunctionWithContextAndExpectSuccess(
        area_info.area_name, "key_priv_ok", "value",
        mojom::ContextType::kPrivilegedExtension));

    // Unprivileged context should fail and get the expected access control
    // error.
    EXPECT_TRUE(RunSetFunctionWithContextAndExpectError(
        area_info.area_name, "key_unpriv_fail", "value",
        mojom::ContextType::kUnprivilegedExtension, kAccessErrorMsg));

    // Test Case 2: AccessLevel = TRUSTED_AND_UNTRUSTED_CONTEXTS
    storage_utils::SetAccessLevelForArea(
        extension()->id(), *browser_context(), area_info.area_namespace,
        api::storage::AccessLevel::kTrustedAndUntrustedContexts);

    // Privileged context should succeed.
    EXPECT_TRUE(RunSetFunctionWithContextAndExpectSuccess(
        area_info.area_name, "key_priv_ok_all_access", "value",
        mojom::ContextType::kPrivilegedExtension));

    // Unprivileged context should also succeed.
    EXPECT_TRUE(RunSetFunctionWithContextAndExpectSuccess(
        area_info.area_name, "key_unpriv_ok_all_access", "value",
        mojom::ContextType::kUnprivilegedExtension));
  }
}

TEST_F(StorageApiUnittest, RestoreCorruptedStorage) {
  const char kKey[] = "key";
  const char kValue[] = "value";
  std::string result;

  // Do a simple set/get combo to make sure the API works.
  RunSetFunction(kKey, kValue);
  EXPECT_TRUE(RunGetFunction(kKey, &result));
  EXPECT_EQ(kValue, result);

  // Corrupt the store. This is not as pretty as ideal, because we use knowledge
  // of the underlying structure, but there's no real good way to corrupt a
  // store other than directly modifying the files.
  value_store::ValueStore* store =
      settings_test_util::GetStorage(extension_ref(), settings_namespace::LOCAL,
                                     StorageFrontend::Get(browser_context()));
  ASSERT_TRUE(store);
  // TODO(cmumford): Modify test as this requires that the factory always
  //                 creates instances of LeveldbValueStore.
  SettingsStorageQuotaEnforcer* quota_store =
      static_cast<SettingsStorageQuotaEnforcer*>(store);
  value_store::LeveldbValueStore* leveldb_store =
      static_cast<value_store::LeveldbValueStore*>(
          quota_store->get_delegate_for_test());
  leveldb::WriteBatch batch;
  batch.Put(kKey, "[{(.*+\"\'\\");
  EXPECT_TRUE(leveldb_store->WriteToDbForTest(&batch));
  EXPECT_TRUE(leveldb_store->Get(kKey).status().IsCorrupted());

  // Running another set should end up working (even though it will restore the
  // store behind the scenes).
  RunSetFunction(kKey, kValue);
  EXPECT_TRUE(RunGetFunction(kKey, &result));
  EXPECT_EQ(kValue, result);
}

TEST_F(StorageApiUnittest, StorageAreaOnChanged) {
  TestEventRouterObserver event_observer(EventRouter::Get(browser_context()));

  EventRouter* event_router = EventRouter::Get(browser_context());
  event_router->AddEventListener(api::storage::OnChanged::kEventName,
                                 render_process_host(), extension()->id());
  event_router->AddEventListener("storage.local.onChanged",
                                 render_process_host(), extension()->id());

  RunSetFunction("key", "value");
  EXPECT_EQ(2u, event_observer.events().size());

  EXPECT_TRUE(base::Contains(event_observer.events(),
                             api::storage::OnChanged::kEventName));
  EXPECT_TRUE(
      base::Contains(event_observer.events(), "storage.local.onChanged"));
}

// Test that no event is dispatched if no listener is added.
TEST_F(StorageApiUnittest, StorageAreaOnChangedNoListener) {
  TestEventRouterObserver event_observer(EventRouter::Get(browser_context()));

  RunSetFunction("key", "value");
  EXPECT_EQ(0u, event_observer.events().size());
}

// Test that no event is dispatched if a listener for a different extension is
// added.
TEST_F(StorageApiUnittest, StorageAreaOnChangedOtherListener) {
  TestEventRouterObserver event_observer(EventRouter::Get(browser_context()));

  EventRouter* event_router = EventRouter::Get(browser_context());
  std::string other_listener_id =
      crx_file::id_util::GenerateId("other-listener");
  event_router->AddEventListener(api::storage::OnChanged::kEventName,
                                 render_process_host(), other_listener_id);
  event_router->AddEventListener("storage.local.onChanged",
                                 render_process_host(), other_listener_id);

  RunSetFunction("key", "value");
  EXPECT_EQ(0u, event_observer.events().size());
}

TEST_F(StorageApiUnittest, StorageAreaOnChangedOnlyOneListener) {
  TestEventRouterObserver event_observer(EventRouter::Get(browser_context()));

  EventRouter* event_router = EventRouter::Get(browser_context());
  event_router->AddEventListener(api::storage::OnChanged::kEventName,
                                 render_process_host(), extension()->id());

  RunSetFunction("key", "value");
  EXPECT_EQ(1u, event_observer.events().size());

  EXPECT_TRUE(base::Contains(event_observer.events(),
                             api::storage::OnChanged::kEventName));
}

// This is a regression test for crbug.com/1483828.
TEST_F(StorageApiUnittest, GetBytesInUseIntOverflow) {
  static constexpr struct TestCase {
    size_t bytes_in_use;
    double result;
  } test_cases[] = {
      {1, 1.0},
      {std::numeric_limits<int>::max(), std::numeric_limits<int>::max()},
      // Test the overflow case from the bug. It's enough to have a value
      // that exceeds the max value that an int can represent.
      {static_cast<size_t>(std::numeric_limits<int>::max()) + 1,
       static_cast<size_t>(std::numeric_limits<int>::max()) + 1}};

  StorageFrontend* frontend = StorageFrontend::Get(browser_context());

  for (const auto& test_case : test_cases) {
    FakeValueStore value_store(test_case.bytes_in_use);
    frontend->SetCacheForTesting(
        settings_namespace::Namespace::LOCAL,
        std::make_unique<FakeValueStoreCache>(std::move(value_store)));

    auto function =
        base::MakeRefCounted<StorageStorageAreaGetBytesInUseFunction>();

    function->set_extension(extension());
    function->set_source_context_type(mojom::ContextType::kPrivilegedExtension);

    std::optional<base::Value> result =
        api_test_utils::RunFunctionAndReturnSingleResult(
            function.get(),
            base::Value::List().Append("local").Append(base::Value()),
            browser_context());
    ASSERT_TRUE(result);
    ASSERT_TRUE(result->is_double());
    EXPECT_EQ(test_case.result, result->GetDouble());
    frontend->DisableStorageForTesting(settings_namespace::Namespace::LOCAL);
  }
}

TEST_F(StorageApiUnittest, GetOperationExceedsSizeLimit) {
  base::test::ScopedFeatureList feature_list;
  feature_list.InitFromCommandLine("EnforceStorageGetSizeLimit", "");

  constexpr size_t kMaxSingleGetSizeBytes = 512 * 1024 * 1024;
  base::Value::Dict dict;
  dict.Set("kKeyWithLargeValue", std::string(kMaxSingleGetSizeBytes, 'a'));

  StorageFrontend* frontend = StorageFrontend::Get(browser_context());
  frontend->SetCacheForTesting(
      settings_namespace::Namespace::LOCAL,
      std::make_unique<FakeValueStoreCache>(FakeValueStore(std::move(dict))));

  auto function = base::MakeRefCounted<StorageStorageAreaGetFunction>();
  function->set_extension(extension());
  function->set_source_context_type(mojom::ContextType::kPrivilegedExtension);

  std::string error = api_test_utils::RunFunctionAndReturnError(
      function.get(), "[\"local\", \"kKeyWithLargeValue\"]", browser_context());

  const std::string expected_error_substring = "exceeds the maximum limit";
  EXPECT_TRUE(base::Contains(error, expected_error_substring));

  frontend->DisableStorageForTesting(settings_namespace::Namespace::LOCAL);
}

}  // namespace extensions