| // Copyright 2020 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/browser/loader/file_url_loader_factory.h" |
| |
| #include <memory> |
| #include <string> |
| |
| #include "base/files/file_util.h" |
| #include "base/files/scoped_temp_dir.h" |
| #include "base/path_service.h" |
| #include "base/test/bind.h" |
| #include "base/test/gmock_callback_support.h" |
| #include "base/test/mock_callback.h" |
| #include "base/test/task_environment.h" |
| #include "base/test/test_future.h" |
| #include "base/threading/thread_restrictions.h" |
| #include "base/time/time.h" |
| #include "components/file_access/scoped_file_access.h" |
| #include "components/file_access/scoped_file_access_delegate.h" |
| #include "components/file_access/test/mock_scoped_file_access_delegate.h" |
| #include "content/public/browser/file_url_loader.h" |
| #include "content/public/browser/shared_cors_origin_access_list.h" |
| #include "content/public/common/content_paths.h" |
| #include "content/public/test/simple_url_loader_test_helper.h" |
| #include "mojo/public/cpp/bindings/pending_receiver.h" |
| #include "mojo/public/cpp/bindings/receiver.h" |
| #include "net/base/filename_util.h" |
| #include "net/base/net_errors.h" |
| #include "net/traffic_annotation/network_traffic_annotation_test_helper.h" |
| #include "services/network/public/cpp/cors/origin_access_list.h" |
| #include "services/network/public/cpp/resource_request.h" |
| #include "services/network/public/cpp/simple_url_loader.h" |
| #include "services/network/public/cpp/url_loader_completion_status.h" |
| #include "services/network/public/mojom/url_loader.mojom.h" |
| #include "services/network/test/mock_url_loader_client.h" |
| #include "services/network/test/test_url_loader_client.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "url/gurl.h" |
| #include "url/origin.h" |
| |
| namespace content { |
| |
| using testing::_; |
| |
| namespace { |
| |
| // Implementation of SharedCorsOriginAccessList that is used by some of the unit |
| // tests below for verifying that FileURLLoaderFactory correctly grants or |
| // denies CORS exemptions to specific initiator origins. |
| // |
| // The implementation below gives https://www.google.com origin access to all |
| // file:// URLs. |
| class SharedCorsOriginAccessListForTesting : public SharedCorsOriginAccessList { |
| public: |
| SharedCorsOriginAccessListForTesting() |
| : permitted_source_origin_( |
| url::Origin::Create(GURL("https://www.google.com"))) { |
| const std::string kFileProtocol("file"); |
| const std::string kAnyDomain; |
| constexpr uint16_t kAnyPort = 0; |
| list_.AddAllowListEntryForOrigin( |
| permitted_source_origin_, kFileProtocol, kAnyDomain, kAnyPort, |
| network::mojom::CorsDomainMatchMode::kDisallowSubdomains, |
| network::mojom::CorsPortMatchMode::kAllowAnyPort, |
| network::mojom::CorsOriginAccessMatchPriority::kDefaultPriority); |
| } |
| SharedCorsOriginAccessListForTesting( |
| const SharedCorsOriginAccessListForTesting&) = delete; |
| SharedCorsOriginAccessListForTesting& operator=( |
| const SharedCorsOriginAccessListForTesting&) = delete; |
| |
| void SetForOrigin( |
| const url::Origin& source_origin, |
| std::vector<network::mojom::CorsOriginPatternPtr> allow_patterns, |
| std::vector<network::mojom::CorsOriginPatternPtr> block_patterns, |
| base::OnceClosure closure) override {} |
| const network::cors::OriginAccessList& GetOriginAccessList() override { |
| return list_; |
| } |
| |
| url::Origin GetPermittedSourceOrigin() { return permitted_source_origin_; } |
| |
| private: |
| ~SharedCorsOriginAccessListForTesting() override = default; |
| |
| network::cors::OriginAccessList list_; |
| const url::Origin permitted_source_origin_; |
| }; |
| |
| GURL GetTestURL(const std::string& filename) { |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| base::FilePath path; |
| base::PathService::Get(DIR_TEST_DATA, &path); |
| path = path.AppendASCII("loader"); |
| return net::FilePathToFileURL(path.AppendASCII(filename)); |
| } |
| |
| class FileURLLoaderFactoryTest : public testing::Test { |
| public: |
| FileURLLoaderFactoryTest() |
| : access_list_( |
| base::MakeRefCounted<SharedCorsOriginAccessListForTesting>()) { |
| factory_.Bind(FileURLLoaderFactory::Create( |
| profile_dummy_path_, access_list_, base::TaskPriority::BEST_EFFORT)); |
| } |
| FileURLLoaderFactoryTest(const FileURLLoaderFactoryTest&) = delete; |
| FileURLLoaderFactoryTest& operator=(const FileURLLoaderFactoryTest&) = delete; |
| ~FileURLLoaderFactoryTest() override = default; |
| |
| protected: |
| int CreateLoaderAndRun(std::unique_ptr<network::ResourceRequest> request) { |
| auto loader = network::SimpleURLLoader::Create( |
| std::move(request), TRAFFIC_ANNOTATION_FOR_TESTS); |
| |
| SimpleURLLoaderTestHelper helper; |
| loader->DownloadToString( |
| factory_.get(), helper.GetCallbackDeprecated(), |
| network::SimpleURLLoader::kMaxBoundedStringDownloadSize); |
| |
| helper.WaitForCallback(); |
| if (loader->ResponseInfo()) { |
| response_info_ = loader->ResponseInfo()->Clone(); |
| } |
| return loader->NetError(); |
| } |
| |
| ::network::URLLoaderCompletionStatus CreateLoaderBypassingSecurityAndRun( |
| std::unique_ptr<network::ResourceRequest> request) { |
| mojo::Receiver<network::mojom::URLLoaderClient> client_receiver{&client_}; |
| base::test::TestFuture<::network::URLLoaderCompletionStatus> future; |
| content::CreateFileURLLoaderBypassingSecurityChecks( |
| *request, loader_.BindNewPipeAndPassReceiver(), |
| client_receiver.BindNewPipeAndPassRemote(), |
| /*observer*/ nullptr, |
| /*allow_directory_listing*/ true); |
| EXPECT_CALL(client_, OnComplete) |
| .WillOnce([&future](::network::URLLoaderCompletionStatus st) { |
| future.SetValue(st); |
| }); |
| return future.Get(); |
| } |
| |
| std::unique_ptr<network::ResourceRequest> CreateRequestWithMode( |
| network::mojom::RequestMode request_mode) { |
| auto request = std::make_unique<network::ResourceRequest>(); |
| request->url = GetTestURL("get.txt"); |
| request->mode = request_mode; |
| return request; |
| } |
| |
| std::unique_ptr<network::ResourceRequest> CreateCorsRequestWithInitiator( |
| const url::Origin initiator_origin) { |
| auto request = std::make_unique<network::ResourceRequest>(); |
| request->url = GetTestURL("get.txt"); |
| request->mode = network::mojom::RequestMode::kCors; |
| request->request_initiator = initiator_origin; |
| return request; |
| } |
| |
| url::Origin GetPermittedSourceOrigin() { |
| return access_list_->GetPermittedSourceOrigin(); |
| } |
| |
| network::mojom::URLResponseHead* ResponseInfo() { |
| return response_info_.get(); |
| } |
| |
| private: |
| base::test::TaskEnvironment task_environment_; |
| base::FilePath profile_dummy_path_; |
| scoped_refptr<SharedCorsOriginAccessListForTesting> access_list_; |
| mojo::Remote<network::mojom::URLLoaderFactory> factory_; |
| network::mojom::URLResponseHeadPtr response_info_; |
| testing::NiceMock<network::MockURLLoaderClient> client_; |
| mojo::Remote<network::mojom::URLLoader> loader_; |
| }; |
| |
| TEST_F(FileURLLoaderFactoryTest, LastModified) { |
| // The Last-Modified response header should be populated with the file |
| // modification time. |
| const char kTimeString[] = "Tue, 15 Nov 1994 12:45:26 GMT"; |
| |
| // Create a temporary file with an arbitrary last-modified timestamp. |
| base::FilePath file; |
| ASSERT_TRUE(base::CreateTemporaryFile(&file)); |
| base::Time time; |
| ASSERT_TRUE(base::Time::FromString(kTimeString, &time)); |
| ASSERT_TRUE(base::TouchFile(file, /*last_accessed=*/base::Time::Now(), |
| /*last_modified=*/time)); |
| |
| // Request the file and extract the Last-Modified header. |
| auto request = std::make_unique<network::ResourceRequest>(); |
| request->url = net::FilePathToFileURL(file); |
| std::string last_modified; |
| ASSERT_EQ(net::OK, CreateLoaderAndRun(std::move(request))); |
| ASSERT_NE(ResponseInfo(), nullptr); |
| ASSERT_TRUE(ResponseInfo()->headers->EnumerateHeader( |
| /*iter*/ nullptr, net::HttpResponseHeaders::kLastModified, |
| &last_modified)); |
| |
| // The header matches the file modification time. |
| ASSERT_EQ(kTimeString, last_modified); |
| } |
| |
| TEST_F(FileURLLoaderFactoryTest, Status) { |
| base::FilePath file; |
| ASSERT_TRUE(base::CreateTemporaryFile(&file)); |
| auto request = std::make_unique<network::ResourceRequest>(); |
| request->url = net::FilePathToFileURL(file); |
| ASSERT_EQ(net::OK, CreateLoaderAndRun(std::move(request))); |
| |
| ASSERT_NE(ResponseInfo(), nullptr); |
| ASSERT_NE(ResponseInfo()->headers, nullptr); |
| ASSERT_EQ(200, ResponseInfo()->headers->response_code()); |
| ASSERT_EQ("OK", ResponseInfo()->headers->GetStatusText()); |
| } |
| |
| #if BUILDFLAG(IS_WIN) |
| TEST_F(FileURLLoaderFactoryTest, LoadingFileLargerThan4GB) { |
| // Prepare big file (ie bigger than 4GB). |
| base::File file; |
| base::FilePath file_path; |
| const int64_t kSize = 5'000'000'000; |
| { |
| const base::ScopedAllowBlockingForTesting allow_io; |
| ASSERT_TRUE(base::CreateTemporaryFile(&file_path)); |
| |
| file.Initialize(file_path, (base::File::Flags::FLAG_CREATE_ALWAYS | |
| base::File::Flags::FLAG_WRITE | |
| base::File::Flags::FLAG_READ)); |
| ASSERT_TRUE(file.IsValid()); |
| ASSERT_TRUE(file.SetLength(kSize)); |
| } |
| |
| auto request = std::make_unique<network::ResourceRequest>(); |
| request->url = net::FilePathToFileURL(file_path); |
| // Any result is OK, as long as it doesn't crash. |
| CreateLoaderAndRun(std::move(request)); |
| } |
| #endif // BUILDFLAG(IS_WIN) |
| |
| TEST_F(FileURLLoaderFactoryTest, MissedRequestInitiator) { |
| // CORS-disabled requests can omit |request.request_initiator| though it is |
| // discouraged not to set |request.request_initiator|. |
| EXPECT_EQ(net::OK, CreateLoaderAndRun(CreateRequestWithMode( |
| network::mojom::RequestMode::kSameOrigin))); |
| |
| EXPECT_EQ(net::OK, CreateLoaderAndRun(CreateRequestWithMode( |
| network::mojom::RequestMode::kNoCors))); |
| |
| EXPECT_EQ(net::OK, CreateLoaderAndRun(CreateRequestWithMode( |
| network::mojom::RequestMode::kNavigate))); |
| |
| // CORS-enabled requests need |request.request_initiator| set. |
| EXPECT_EQ(net::ERR_INVALID_ARGUMENT, |
| CreateLoaderAndRun( |
| CreateRequestWithMode(network::mojom::RequestMode::kCors))); |
| |
| EXPECT_EQ(net::ERR_INVALID_ARGUMENT, |
| CreateLoaderAndRun(CreateRequestWithMode( |
| network::mojom::RequestMode::kCorsWithForcedPreflight))); |
| } |
| |
| // Verify that FileURLLoaderFactory takes OriginAccessList into account when |
| // deciding whether to exempt a request from CORS. See also |
| // https://crbug.com/1049604. |
| TEST_F(FileURLLoaderFactoryTest, Allowlist) { |
| const url::Origin not_permitted_origin = |
| url::Origin::Create(GURL("https://www.example.com")); |
| |
| // Request should fail unless the origin pair is not registered in the |
| // allowlist. |
| EXPECT_EQ( |
| net::ERR_FAILED, |
| CreateLoaderAndRun(CreateCorsRequestWithInitiator(not_permitted_origin))); |
| |
| // Registered access should pass. |
| EXPECT_EQ(net::OK, CreateLoaderAndRun(CreateCorsRequestWithInitiator( |
| GetPermittedSourceOrigin()))); |
| |
| // Isolated world origin should *not* be used to check the permission. |
| auto request = CreateCorsRequestWithInitiator(not_permitted_origin); |
| request->isolated_world_origin = GetPermittedSourceOrigin(); |
| EXPECT_EQ(net::ERR_FAILED, CreateLoaderAndRun(std::move(request))); |
| } |
| |
| // Test that response type is set correctly for directory listings. Regression |
| // test for https://crbug.com/41492103. |
| TEST_F(FileURLLoaderFactoryTest, ResponseTypeForDirectoryListings) { |
| base::ScopedTempDir dir; |
| ASSERT_TRUE(dir.CreateUniqueTempDir()); |
| auto request = std::make_unique<network::ResourceRequest>(); |
| request->url = |
| net::FilePathToFileURL(dir.GetPath().StripTrailingSeparators()); |
| request->mode = network::mojom::RequestMode::kNoCors; |
| ASSERT_EQ(net::OK, CreateLoaderAndRun(std::move(request))); |
| |
| ASSERT_NE(ResponseInfo(), nullptr); |
| EXPECT_EQ(network::mojom::FetchResponseType::kOpaque, |
| ResponseInfo()->response_type); |
| } |
| |
| } // namespace |
| |
| TEST_F(FileURLLoaderFactoryTest, DlpInitiatorAllow) { |
| file_access::MockScopedFileAccessDelegate file_access_delegate; |
| auto origin = url::Origin::Create(GURL("https://example.com")); |
| |
| EXPECT_CALL(file_access_delegate, RequestFilesAccess) |
| .WillOnce(base::test::RunOnceCallback<2>( |
| file_access::ScopedFileAccess::Allowed())); |
| |
| base::FilePath file; |
| ASSERT_TRUE(base::CreateTemporaryFile(&file)); |
| auto request = std::make_unique<network::ResourceRequest>(); |
| request->url = net::FilePathToFileURL(file); |
| request->request_initiator = origin; |
| ASSERT_EQ(net::OK, CreateLoaderAndRun(std::move(request))); |
| |
| ASSERT_NE(ResponseInfo(), nullptr); |
| ASSERT_NE(ResponseInfo()->headers, nullptr); |
| ASSERT_EQ(200, ResponseInfo()->headers->response_code()); |
| ASSERT_EQ("OK", ResponseInfo()->headers->GetStatusText()); |
| } |
| |
| TEST_F(FileURLLoaderFactoryTest, DlpNoInitiatorAllow) { |
| file_access::MockScopedFileAccessDelegate file_access_delegate; |
| |
| EXPECT_CALL(file_access_delegate, RequestFilesAccessForSystem) |
| .WillOnce(base::test::RunOnceCallback<1>( |
| file_access::ScopedFileAccess::Allowed())); |
| |
| base::FilePath file; |
| ASSERT_TRUE(base::CreateTemporaryFile(&file)); |
| auto request = std::make_unique<network::ResourceRequest>(); |
| request->url = net::FilePathToFileURL(file); |
| ASSERT_EQ(net::OK, CreateLoaderAndRun(std::move(request))); |
| |
| ASSERT_NE(ResponseInfo(), nullptr); |
| ASSERT_NE(ResponseInfo()->headers, nullptr); |
| ASSERT_EQ(200, ResponseInfo()->headers->response_code()); |
| ASSERT_EQ("OK", ResponseInfo()->headers->GetStatusText()); |
| } |
| |
| TEST_F(FileURLLoaderFactoryTest, DlpInitiatorDeny) { |
| file_access::MockScopedFileAccessDelegate file_access_delegate; |
| auto origin = url::Origin::Create(GURL("https://example.com")); |
| |
| EXPECT_CALL(file_access_delegate, RequestFilesAccess) |
| .WillOnce(base::test::RunOnceCallback<2>( |
| file_access::ScopedFileAccess::Denied())); |
| |
| base::FilePath file; |
| ASSERT_TRUE(base::CreateTemporaryFile(&file)); |
| auto request = std::make_unique<network::ResourceRequest>(); |
| request->url = net::FilePathToFileURL(file); |
| request->request_initiator = origin; |
| ASSERT_EQ(net::ERR_FAILED, CreateLoaderAndRun(std::move(request))); |
| } |
| |
| TEST_F(FileURLLoaderFactoryTest, DlpNoInitiatorDeny) { |
| file_access::MockScopedFileAccessDelegate file_access_delegate; |
| |
| EXPECT_CALL(file_access_delegate, RequestFilesAccessForSystem) |
| .WillOnce(base::test::RunOnceCallback<1>( |
| file_access::ScopedFileAccess::Denied())); |
| |
| base::FilePath file; |
| ASSERT_TRUE(base::CreateTemporaryFile(&file)); |
| auto request = std::make_unique<network::ResourceRequest>(); |
| request->url = net::FilePathToFileURL(file); |
| ASSERT_EQ(net::ERR_FAILED, CreateLoaderAndRun(std::move(request))); |
| } |
| |
| TEST_F(FileURLLoaderFactoryTest, DlpRemoteValidFileUrl) { |
| file_access::MockScopedFileAccessDelegate file_access_delegate; |
| base::MockCallback<base::RepeatingCallback<void( |
| const std::vector<base::FilePath>&, |
| base::OnceCallback<void(file_access::ScopedFileAccess)>)>> |
| cb; |
| EXPECT_CALL(cb, Run).WillOnce( |
| base::test::RunOnceCallback<1>(file_access::ScopedFileAccess::Denied())); |
| file_access::ScopedFileAccessDelegate:: |
| ScopedRequestFilesAccessCallbackForTesting scoped_callback(cb.Get()); |
| auto request = std::make_unique<network::ResourceRequest>(); |
| request->url = GURL("file:///test"); |
| |
| EXPECT_EQ(net::ERR_FAILED, |
| CreateLoaderBypassingSecurityAndRun(std::move(request)).error_code); |
| } |
| |
| TEST_F(FileURLLoaderFactoryTest, DlpRemoteInvalidFileUrl) { |
| file_access::MockScopedFileAccessDelegate file_access_delegate; |
| base::MockCallback<base::RepeatingCallback<void( |
| const std::vector<base::FilePath>&, |
| base::OnceCallback<void(file_access::ScopedFileAccess)>)>> |
| cb; |
| EXPECT_CALL(cb, Run).Times(0); |
| file_access::ScopedFileAccessDelegate:: |
| ScopedRequestFilesAccessCallbackForTesting scoped_callback(cb.Get()); |
| auto request = std::make_unique<network::ResourceRequest>(); |
| request->url = GURL("file:///%00"); |
| |
| EXPECT_EQ(net::ERR_INVALID_URL, |
| CreateLoaderBypassingSecurityAndRun(std::move(request)).error_code); |
| } |
| |
| } // namespace content |