| // Copyright 2021 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/media/cdm_document_service_impl.h" |
| |
| #include <memory> |
| #include <tuple> |
| |
| #include "base/files/file.h" |
| #include "base/files/file_util.h" |
| #include "base/json/values_util.h" |
| #include "base/logging.h" |
| #include "base/run_loop.h" |
| #include "base/test/gmock_callback_support.h" |
| #include "base/test/mock_callback.h" |
| #include "base/time/time.h" |
| #include "base/unguessable_token.h" |
| #include "base/values.h" |
| #include "chrome/browser/media/cdm_pref_service_helper.h" |
| #include "chrome/common/pref_names.h" |
| #include "chrome/test/base/chrome_render_view_host_test_harness.h" |
| #include "chrome/test/base/testing_browser_process.h" |
| #include "components/prefs/scoped_user_pref_update.h" |
| #include "components/sync_preferences/testing_pref_service_syncable.h" |
| #include "components/user_prefs/user_prefs.h" |
| #include "content/public/browser/web_contents.h" |
| #include "media/cdm/win/media_foundation_cdm.h" |
| #include "media/mojo/mojom/cdm_document_service.mojom.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "url/gurl.h" |
| #include "url/origin.h" |
| |
| using testing::_; |
| using testing::DoAll; |
| using testing::SaveArg; |
| |
| namespace { |
| // copied from cdm_pref_service_helper.cc for testing |
| const char kOriginId[] = "origin_id"; |
| |
| base::FilePath CreateDummyCdmDataFile(const base::FilePath& cdm_store_path_root, |
| const base::UnguessableToken& origin_id) { |
| // Create a fake CDM file |
| auto cdm_store_path = cdm_store_path_root.AppendASCII(origin_id.ToString()); |
| base::CreateDirectory(cdm_store_path); |
| auto cdm_data_file_path = cdm_store_path.AppendASCII("cdm_data_file.txt"); |
| base::File file(cdm_data_file_path, |
| base::File::FLAG_CREATE | base::File::FLAG_WRITE); |
| return cdm_data_file_path; |
| } |
| } // namespace |
| |
| namespace content { |
| |
| const char kTestOrigin[] = "https://foo.bar"; |
| const char kTestOrigin2[] = "https://bar.foo"; |
| |
| using GetMediaFoundationCdmDataMockCB = base::MockOnceCallback<void( |
| std::unique_ptr<media::MediaFoundationCdmData>)>; |
| |
| class CdmDocumentServiceImplTest : public ChromeRenderViewHostTestHarness { |
| public: |
| void SetUp() override { |
| ChromeRenderViewHostTestHarness::SetUp(); |
| // The Media Foundation CDM depends on functionalities only available in |
| // Windows 10 and newer versions. |
| if (!media::MediaFoundationCdm::IsAvailable()) { |
| GTEST_SKIP() << "skipping all test for this fixture when not running on " |
| "Windows 10."; |
| } |
| } |
| |
| void NavigateToUrlAndCreateCdmDocumentService(GURL url) { |
| // The lifetime of `cdm_document_service_` is tied to the lifetime of the |
| // Frame. When changing URL we need to unbind `cdm_document_service_` before |
| // we can bind it to the new frame. |
| if (cdm_document_service_.is_bound()) |
| ASSERT_TRUE(cdm_document_service_.Unbind()); |
| NavigateAndCommit(url); |
| CdmDocumentServiceImpl::Create( |
| web_contents()->GetPrimaryMainFrame(), |
| cdm_document_service_.BindNewPipeAndPassReceiver()); |
| } |
| |
| std::unique_ptr<media::MediaFoundationCdmData> GetMediaFoundationCdmData() { |
| std::unique_ptr<media::MediaFoundationCdmData> media_foundation_cdm_data; |
| GetMediaFoundationCdmDataMockCB mock_cb; |
| base::RunLoop run_loop; |
| EXPECT_CALL(mock_cb, Run(_)) |
| .WillOnce([&media_foundation_cdm_data, &run_loop]( |
| std::unique_ptr<media::MediaFoundationCdmData> ptr) { |
| media_foundation_cdm_data = std::move(ptr); |
| run_loop.Quit(); |
| }); |
| |
| cdm_document_service_->GetMediaFoundationCdmData(mock_cb.Get()); |
| run_loop.Run(); |
| |
| return media_foundation_cdm_data; |
| } |
| |
| void SetCdmClientToken(const std::vector<uint8_t>& client_token) { |
| cdm_document_service_->SetCdmClientToken(client_token); |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| void CorruptCdmPreference() { |
| PrefService* user_prefs = profile()->GetPrefs(); |
| |
| // Create (or overwrite) an entry with only an origin id to simulate some |
| // kind of corruption or simply an update to the preference format. |
| base::Value entry(base::Value::Type::DICTIONARY); |
| entry.SetKey(kOriginId, base::UnguessableTokenToValue( |
| base::UnguessableToken::Create())); |
| |
| ScopedDictPrefUpdate update(user_prefs, prefs::kMediaCdmOriginData); |
| base::Value::Dict& dict = update.Get(); |
| const std::string serialized_origin = web_contents() |
| ->GetPrimaryMainFrame() |
| ->GetLastCommittedOrigin() |
| .Serialize(); |
| dict.Set(serialized_origin, std::move(entry)); |
| } |
| |
| protected: |
| mojo::Remote<media::mojom::CdmDocumentService> cdm_document_service_; |
| }; |
| |
| // Verify that we get a non null origin id. |
| TEST_F(CdmDocumentServiceImplTest, GetOriginId) { |
| NavigateToUrlAndCreateCdmDocumentService(GURL(kTestOrigin)); |
| auto data = GetMediaFoundationCdmData(); |
| ASSERT_FALSE(data->origin_id.is_empty()); |
| } |
| |
| // Verify that we get a non null and different origin id if the preference gets |
| // corrupted. |
| TEST_F(CdmDocumentServiceImplTest, GetOriginIdAfterCorruption) { |
| NavigateToUrlAndCreateCdmDocumentService(GURL(kTestOrigin)); |
| auto data_before = GetMediaFoundationCdmData(); |
| |
| CorruptCdmPreference(); |
| auto data_after = GetMediaFoundationCdmData(); |
| ASSERT_FALSE(data_after->origin_id.is_empty()); |
| ASSERT_NE(data_before->origin_id, data_after->origin_id); |
| } |
| |
| // Verify that we can correctly get an existing origin id. |
| TEST_F(CdmDocumentServiceImplTest, GetSameOriginId) { |
| NavigateToUrlAndCreateCdmDocumentService(GURL(kTestOrigin)); |
| base::UnguessableToken origin_id1 = GetMediaFoundationCdmData()->origin_id; |
| |
| // Create an unrelated origin id |
| NavigateToUrlAndCreateCdmDocumentService(GURL(kTestOrigin2)); |
| base::UnguessableToken origin_id2 = GetMediaFoundationCdmData()->origin_id; |
| |
| // Get the origin id for the first origin |
| NavigateToUrlAndCreateCdmDocumentService(GURL(kTestOrigin)); |
| base::UnguessableToken origin_id3 = GetMediaFoundationCdmData()->origin_id; |
| |
| ASSERT_NE(origin_id2, origin_id1); |
| ASSERT_EQ(origin_id1, origin_id3); |
| } |
| |
| TEST_F(CdmDocumentServiceImplTest, GetNullClientToken) { |
| NavigateToUrlAndCreateCdmDocumentService(GURL(kTestOrigin)); |
| auto media_foundation_cdm_data = GetMediaFoundationCdmData(); |
| |
| ASSERT_FALSE(media_foundation_cdm_data->client_token); |
| } |
| |
| TEST_F(CdmDocumentServiceImplTest, SetClientToken) { |
| NavigateToUrlAndCreateCdmDocumentService(GURL(kTestOrigin)); |
| // Call GetMediaFoundationCdmData to create the origin id first, otherwise |
| // `SetCdmClientToken()` will assume the preference data associated with the |
| // origin was recently cleared and will not save the client token. |
| std::ignore = GetMediaFoundationCdmData(); |
| |
| std::vector<uint8_t> expected_client_token = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; |
| SetCdmClientToken(expected_client_token); |
| |
| auto media_foundation_cdm_data = GetMediaFoundationCdmData(); |
| |
| ASSERT_EQ(media_foundation_cdm_data->client_token, expected_client_token); |
| } |
| |
| // Sets a client token for one origin and check that we get the same |
| // client token after navigating back to that origin. |
| TEST_F(CdmDocumentServiceImplTest, GetSameClientToken) { |
| const auto kOrigin = url::Origin::Create(GURL(kTestOrigin)); |
| const auto kOtherOrigin = url::Origin::Create(GURL(kTestOrigin2)); |
| |
| NavigateToUrlAndCreateCdmDocumentService(GURL(kTestOrigin)); |
| // Call GetMediaFoundationCdmData to create the origin id first, otherwise |
| // `SetCdmClientToken()` will assume the preference data associated with the |
| // origin was recently cleared and will not save the client token. |
| std::ignore = GetMediaFoundationCdmData(); |
| std::vector<uint8_t> expected_client_token = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; |
| SetCdmClientToken(expected_client_token); |
| |
| NavigateToUrlAndCreateCdmDocumentService(GURL(kTestOrigin2)); |
| std::ignore = GetMediaFoundationCdmData(); |
| SetCdmClientToken({1, 2, 3, 4, 5}); |
| |
| NavigateToUrlAndCreateCdmDocumentService(GURL(kTestOrigin)); |
| auto media_foundation_cdm_data = GetMediaFoundationCdmData(); |
| |
| ASSERT_EQ(media_foundation_cdm_data->client_token, expected_client_token); |
| } |
| |
| // If an entry cannot be parsed correctly, `SetCdmClientToken` should simply |
| // remove that entry and return without saving the client token. |
| TEST_F(CdmDocumentServiceImplTest, SetClientTokenAfterCorruption) { |
| NavigateToUrlAndCreateCdmDocumentService(GURL(kTestOrigin)); |
| std::ignore = GetMediaFoundationCdmData(); |
| CorruptCdmPreference(); |
| |
| std::vector<uint8_t> expected_client_token = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; |
| SetCdmClientToken(expected_client_token); |
| |
| auto media_foundation_cdm_data = GetMediaFoundationCdmData(); |
| ASSERT_FALSE(media_foundation_cdm_data->client_token.has_value()); |
| } |
| |
| // Check that we can clear the CDM preferences. `GetMediaFoundationCdmData()` |
| // should return a new origin_id after the clearing operation. |
| TEST_F(CdmDocumentServiceImplTest, ClearCdmPreferenceData) { |
| const auto kOrigin = url::Origin::Create(GURL(kTestOrigin)); |
| |
| NavigateToUrlAndCreateCdmDocumentService(GURL(kTestOrigin)); |
| auto cdm_data = GetMediaFoundationCdmData(); |
| base::UnguessableToken origin_id = cdm_data->origin_id; |
| |
| base::FilePath cdm_data_file_path = CreateDummyCdmDataFile( |
| cdm_data->cdm_store_path_root, cdm_data->origin_id); |
| |
| base::Time start = base::Time::Now() - base::Hours(1); |
| base::Time end; // null time |
| |
| base::RunLoop loop1; |
| |
| // With the filter returning false, the origin id should not be destroyed. |
| CdmDocumentServiceImpl::ClearCdmData( |
| profile(), start, end, |
| base::BindRepeating([](const GURL& url) { return false; }), |
| loop1.QuitClosure()); |
| |
| loop1.Run(); |
| base::UnguessableToken same_origin_id = |
| GetMediaFoundationCdmData()->origin_id; |
| ASSERT_EQ(origin_id, same_origin_id); |
| ASSERT_TRUE(base::PathExists(cdm_data_file_path)); |
| |
| base::RunLoop loop2; |
| |
| CdmDocumentServiceImpl::ClearCdmData( |
| profile(), start, end, |
| base::BindRepeating([](const GURL& url) { return true; }), |
| loop2.QuitClosure()); |
| |
| loop2.Run(); |
| |
| base::UnguessableToken new_origin_id = GetMediaFoundationCdmData()->origin_id; |
| ASSERT_NE(origin_id, new_origin_id); |
| ASSERT_FALSE(base::PathExists(cdm_data_file_path)); |
| } |
| |
| // Check that we only clear the CDM preference that were set between start and |
| // end. |
| TEST_F(CdmDocumentServiceImplTest, ClearCdmPreferenceDataWrongTime) { |
| const auto kOrigin = url::Origin::Create(GURL(kTestOrigin)); |
| |
| NavigateToUrlAndCreateCdmDocumentService(GURL(kTestOrigin)); |
| auto cdm_data = GetMediaFoundationCdmData(); |
| base::UnguessableToken origin_id = cdm_data->origin_id; |
| base::FilePath cdm_data_file_path = |
| CreateDummyCdmDataFile(cdm_data->cdm_store_path_root, origin_id); |
| |
| base::Time start = base::Time::Now() - base::Hours(4); |
| base::Time end = start - base::Hours(2); |
| |
| auto null_filter = base::RepeatingCallback<bool(const GURL&)>(); |
| |
| base::RunLoop loop; |
| |
| CdmDocumentServiceImpl::ClearCdmData(profile(), start, end, null_filter, |
| loop.QuitClosure()); |
| |
| loop.Run(); |
| |
| base::UnguessableToken new_origin_id = GetMediaFoundationCdmData()->origin_id; |
| ASSERT_EQ(origin_id, new_origin_id); |
| ASSERT_TRUE(base::PathExists(cdm_data_file_path)); |
| } |
| |
| TEST_F(CdmDocumentServiceImplTest, ClearCdmPreferenceDataNullFilter) { |
| const auto kOrigin = url::Origin::Create(GURL(kTestOrigin)); |
| |
| NavigateToUrlAndCreateCdmDocumentService(GURL(kTestOrigin)); |
| base::UnguessableToken origin_id_1 = GetMediaFoundationCdmData()->origin_id; |
| |
| NavigateToUrlAndCreateCdmDocumentService(GURL(kTestOrigin2)); |
| base::UnguessableToken origin_id_2 = GetMediaFoundationCdmData()->origin_id; |
| |
| base::Time start = base::Time::Now() - base::Hours(1); |
| base::Time end; // null time |
| |
| auto null_filter = base::RepeatingCallback<bool(const GURL&)>(); |
| |
| base::RunLoop loop; |
| |
| CdmDocumentServiceImpl::ClearCdmData(profile(), start, end, null_filter, |
| loop.QuitClosure()); |
| |
| loop.Run(); |
| |
| base::UnguessableToken new_origin_id = GetMediaFoundationCdmData()->origin_id; |
| ASSERT_NE(origin_id_2, new_origin_id); |
| |
| NavigateToUrlAndCreateCdmDocumentService(GURL(kTestOrigin)); |
| new_origin_id = GetMediaFoundationCdmData()->origin_id; |
| ASSERT_NE(origin_id_1, new_origin_id); |
| } |
| |
| } // namespace content |