|  | // Copyright 2016 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 "net/url_request/url_request_file_dir_job.h" | 
|  |  | 
|  | #include <memory> | 
|  | #include <string> | 
|  | #include <vector> | 
|  |  | 
|  | #include "base/files/file_path.h" | 
|  | #include "base/files/file_util.h" | 
|  | #include "base/files/scoped_temp_dir.h" | 
|  | #include "base/macros.h" | 
|  | #include "base/memory/ref_counted.h" | 
|  | #include "base/run_loop.h" | 
|  | #include "base/strings/string_split.h" | 
|  | #include "base/strings/string_util.h" | 
|  | #include "build/build_config.h" | 
|  | #include "net/base/filename_util.h" | 
|  | #include "net/base/io_buffer.h" | 
|  | #include "net/base/net_errors.h" | 
|  | #include "net/test/gtest_util.h" | 
|  | #include "net/test/test_with_scoped_task_environment.h" | 
|  | #include "net/traffic_annotation/network_traffic_annotation_test_helper.h" | 
|  | #include "net/url_request/url_request.h" | 
|  | #include "net/url_request/url_request_test_util.h" | 
|  | #include "testing/gmock/include/gmock/gmock.h" | 
|  | #include "testing/gtest/include/gtest/gtest.h" | 
|  |  | 
|  | using base::StartsWith; | 
|  | using net::test::IsError; | 
|  | using net::test::IsOk; | 
|  |  | 
|  | namespace net { | 
|  |  | 
|  | namespace { | 
|  |  | 
|  | const int kBufferSize = 4096; | 
|  |  | 
|  | // Snippets of JS code from net/base/dir_header.html. | 
|  | const char kHeaderStart[] = "<script>start(\""; | 
|  | const char kEntryStart[] = "<script>addRow(\""; | 
|  | const char kParentDirLink[] = "<script>onHasParentDirectory();"; | 
|  |  | 
|  | bool HasHeader(const std::string& response_body, const base::FilePath& dir) { | 
|  | std::vector<std::string> lines = base::SplitString( | 
|  | response_body, "\n", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL); | 
|  | for (const std::string& line : lines) { | 
|  | if (StartsWith(line, kHeaderStart, base::CompareCase::INSENSITIVE_ASCII)) | 
|  | return line.find(dir.BaseName().MaybeAsASCII()) != std::string::npos; | 
|  | } | 
|  | return false; | 
|  | } | 
|  |  | 
|  | bool HasParentDirLink(const std::string& response_body) { | 
|  | return response_body.find(kParentDirLink) != std::string::npos; | 
|  | } | 
|  |  | 
|  | // There should not be any entries for the parent dir, so this should always | 
|  | // return false. | 
|  | bool HasParentDirEntry(const std::string& response_body) { | 
|  | std::string needle = kEntryStart; | 
|  | needle += "..\""; | 
|  | return response_body.find(needle) != std::string::npos; | 
|  | } | 
|  |  | 
|  | bool HasEntry(const std::string& response_body, const base::FilePath& entry) { | 
|  | std::string needle = kEntryStart + entry.BaseName().MaybeAsASCII(); | 
|  | return response_body.find(needle) != std::string::npos; | 
|  | } | 
|  |  | 
|  | int GetEntryCount(const std::string& response_body) { | 
|  | int count = 0; | 
|  | std::vector<std::string> lines = base::SplitString( | 
|  | response_body, "\n", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL); | 
|  | for (const std::string& line : lines) { | 
|  | if (StartsWith(line, kEntryStart, base::CompareCase::INSENSITIVE_ASCII)) | 
|  | ++count; | 
|  | } | 
|  | return count; | 
|  | } | 
|  |  | 
|  | class TestJobFactory : public URLRequestJobFactory { | 
|  | public: | 
|  | explicit TestJobFactory(const base::FilePath& path) : path_(path) {} | 
|  | ~TestJobFactory() override = default; | 
|  |  | 
|  | URLRequestJob* MaybeCreateJobWithProtocolHandler( | 
|  | const std::string& scheme, | 
|  | URLRequest* request, | 
|  | NetworkDelegate* network_delegate) const override { | 
|  | URLRequestJob* job = | 
|  | new URLRequestFileDirJob(request, network_delegate, path_); | 
|  |  | 
|  | return job; | 
|  | } | 
|  |  | 
|  | URLRequestJob* MaybeInterceptRedirect(URLRequest* request, | 
|  | NetworkDelegate* network_delegate, | 
|  | const GURL& location) const override { | 
|  | return nullptr; | 
|  | } | 
|  |  | 
|  | URLRequestJob* MaybeInterceptResponse( | 
|  | URLRequest* request, | 
|  | NetworkDelegate* network_delegate) const override { | 
|  | return nullptr; | 
|  | } | 
|  |  | 
|  | bool IsHandledProtocol(const std::string& scheme) const override { | 
|  | return scheme == "file"; | 
|  | } | 
|  |  | 
|  | bool IsSafeRedirectTarget(const GURL& location) const override { | 
|  | return false; | 
|  | } | 
|  |  | 
|  | private: | 
|  | const base::FilePath path_; | 
|  |  | 
|  | DISALLOW_COPY_AND_ASSIGN(TestJobFactory); | 
|  | }; | 
|  |  | 
|  | class TestDirectoryURLRequestDelegate : public TestDelegate { | 
|  | public: | 
|  | TestDirectoryURLRequestDelegate() = default; | 
|  |  | 
|  | ~TestDirectoryURLRequestDelegate() override = default; | 
|  |  | 
|  | void OnResponseStarted(URLRequest* request, int net_error) override { | 
|  | got_response_started_ = true; | 
|  | } | 
|  |  | 
|  | bool got_response_started() const { return got_response_started_; } | 
|  |  | 
|  | private: | 
|  | bool got_response_started_ = false; | 
|  |  | 
|  | DISALLOW_COPY_AND_ASSIGN(TestDirectoryURLRequestDelegate); | 
|  | }; | 
|  |  | 
|  | class URLRequestFileDirTest : public TestWithScopedTaskEnvironment { | 
|  | public: | 
|  | URLRequestFileDirTest() | 
|  | : buffer_(base::MakeRefCounted<IOBuffer>(kBufferSize)) {} | 
|  |  | 
|  | protected: | 
|  | TestURLRequestContext context_; | 
|  | TestDirectoryURLRequestDelegate delegate_; | 
|  | scoped_refptr<IOBuffer> buffer_; | 
|  | }; | 
|  |  | 
|  | TEST_F(URLRequestFileDirTest, ListCompletionOnNoPending) { | 
|  | base::ScopedTempDir directory; | 
|  | // It is necessary to pass an existing directory to UrlRequest object, | 
|  | // but it will be deleted for testing purpose after request is started. | 
|  | ASSERT_TRUE(directory.CreateUniqueTempDir()); | 
|  | TestJobFactory factory(directory.GetPath()); | 
|  | context_.set_job_factory(&factory); | 
|  | std::unique_ptr<URLRequest> request(context_.CreateRequest( | 
|  | FilePathToFileURL( | 
|  | directory.GetPath().AppendASCII("this_path_does_not_exist")), | 
|  | DEFAULT_PRIORITY, &delegate_, TRAFFIC_ANNOTATION_FOR_TESTS)); | 
|  |  | 
|  | request->Start(); | 
|  | ASSERT_TRUE(directory.Delete()); | 
|  |  | 
|  | // Since the DirectoryLister is running on the network thread, this | 
|  | // will spin the message loop until the read error is returned to the | 
|  | // URLRequestFileDirJob. | 
|  | RunUntilIdle(); | 
|  | ASSERT_TRUE(delegate_.got_response_started()); | 
|  |  | 
|  | int bytes_read = request->Read(buffer_.get(), kBufferSize); | 
|  |  | 
|  | // The URLRequestFileDirJobShould return the cached read error synchronously. | 
|  | // If it's not returned synchronously, the code path this is intended to test | 
|  | // was not executed. | 
|  | EXPECT_THAT(bytes_read, IsError(ERR_FILE_NOT_FOUND)); | 
|  | } | 
|  |  | 
|  | // Test the case where reading the response completes synchronously. | 
|  | TEST_F(URLRequestFileDirTest, DirectoryWithASingleFileSync) { | 
|  | base::ScopedTempDir directory; | 
|  | ASSERT_TRUE(directory.CreateUniqueTempDir()); | 
|  | base::FilePath path; | 
|  | base::CreateTemporaryFileInDir(directory.GetPath(), &path); | 
|  |  | 
|  | TestJobFactory factory(directory.GetPath()); | 
|  | context_.set_job_factory(&factory); | 
|  |  | 
|  | std::unique_ptr<URLRequest> request( | 
|  | context_.CreateRequest(FilePathToFileURL(path), DEFAULT_PRIORITY, | 
|  | &delegate_, TRAFFIC_ANNOTATION_FOR_TESTS)); | 
|  | request->Start(); | 
|  | EXPECT_TRUE(request->is_pending()); | 
|  |  | 
|  | // Since the DirectoryLister is running on the network thread, this will spin | 
|  | // the message loop until the URLRequetsFileDirJob has received the | 
|  | // entire directory listing and cached it. | 
|  | RunUntilIdle(); | 
|  |  | 
|  | // This will complete synchronously, since the URLRequetsFileDirJob had | 
|  | // directory listing cached in memory. | 
|  | int bytes_read = request->Read(buffer_.get(), kBufferSize); | 
|  |  | 
|  | ASSERT_GT(bytes_read, 0); | 
|  | ASSERT_LE(bytes_read, kBufferSize); | 
|  | std::string data(buffer_->data(), bytes_read); | 
|  | EXPECT_TRUE(HasHeader(data, directory.GetPath())); | 
|  | EXPECT_TRUE(HasParentDirLink(data)); | 
|  | ASSERT_EQ(1, GetEntryCount(data)); | 
|  | EXPECT_TRUE(HasEntry(data, path)); | 
|  | EXPECT_FALSE(HasParentDirEntry(data)); | 
|  | } | 
|  |  | 
|  | // Test the case where reading the response completes asynchronously. | 
|  | TEST_F(URLRequestFileDirTest, DirectoryWithASingleFileAsync) { | 
|  | base::ScopedTempDir directory; | 
|  | ASSERT_TRUE(directory.CreateUniqueTempDir()); | 
|  | base::FilePath path; | 
|  | base::CreateTemporaryFileInDir(directory.GetPath(), &path); | 
|  |  | 
|  | TestJobFactory factory(directory.GetPath()); | 
|  | context_.set_job_factory(&factory); | 
|  |  | 
|  | TestDelegate delegate; | 
|  | std::unique_ptr<URLRequest> request( | 
|  | context_.CreateRequest(FilePathToFileURL(path), DEFAULT_PRIORITY, | 
|  | &delegate, TRAFFIC_ANNOTATION_FOR_TESTS)); | 
|  | request->Start(); | 
|  | EXPECT_TRUE(request->is_pending()); | 
|  |  | 
|  | base::RunLoop().Run(); | 
|  |  | 
|  | ASSERT_GT(delegate.bytes_received(), 0); | 
|  | ASSERT_LE(delegate.bytes_received(), kBufferSize); | 
|  | EXPECT_TRUE(HasHeader(delegate.data_received(), directory.GetPath())); | 
|  | EXPECT_TRUE(HasParentDirLink(delegate.data_received())); | 
|  | ASSERT_EQ(1, GetEntryCount(delegate.data_received())); | 
|  | EXPECT_TRUE(HasEntry(delegate.data_received(), path)); | 
|  | EXPECT_FALSE(HasParentDirEntry(delegate.data_received())); | 
|  | } | 
|  |  | 
|  | TEST_F(URLRequestFileDirTest, DirectoryWithAFileAndSubdirectory) { | 
|  | base::ScopedTempDir directory; | 
|  | ASSERT_TRUE(directory.CreateUniqueTempDir()); | 
|  |  | 
|  | base::FilePath sub_dir; | 
|  | CreateTemporaryDirInDir(directory.GetPath(), | 
|  | FILE_PATH_LITERAL("CreateNewSubDirectoryInDirectory"), | 
|  | &sub_dir); | 
|  |  | 
|  | base::FilePath path; | 
|  | base::CreateTemporaryFileInDir(directory.GetPath(), &path); | 
|  |  | 
|  | TestJobFactory factory(directory.GetPath()); | 
|  | context_.set_job_factory(&factory); | 
|  |  | 
|  | TestDelegate delegate; | 
|  | std::unique_ptr<URLRequest> request( | 
|  | context_.CreateRequest(FilePathToFileURL(path), DEFAULT_PRIORITY, | 
|  | &delegate, TRAFFIC_ANNOTATION_FOR_TESTS)); | 
|  | request->Start(); | 
|  | EXPECT_TRUE(request->is_pending()); | 
|  |  | 
|  | base::RunLoop().Run(); | 
|  |  | 
|  | ASSERT_GT(delegate.bytes_received(), 0); | 
|  | ASSERT_LE(delegate.bytes_received(), kBufferSize); | 
|  | EXPECT_TRUE(HasHeader(delegate.data_received(), directory.GetPath())); | 
|  | EXPECT_TRUE(HasParentDirLink(delegate.data_received())); | 
|  | ASSERT_EQ(2, GetEntryCount(delegate.data_received())); | 
|  | EXPECT_TRUE(HasEntry(delegate.data_received(), sub_dir)); | 
|  | EXPECT_TRUE(HasEntry(delegate.data_received(), path)); | 
|  | EXPECT_FALSE(HasParentDirEntry(delegate.data_received())); | 
|  | } | 
|  |  | 
|  | TEST_F(URLRequestFileDirTest, EmptyDirectory) { | 
|  | base::ScopedTempDir directory; | 
|  | ASSERT_TRUE(directory.CreateUniqueTempDir()); | 
|  |  | 
|  | TestJobFactory factory(directory.GetPath()); | 
|  | context_.set_job_factory(&factory); | 
|  |  | 
|  | TestDelegate delegate; | 
|  | std::unique_ptr<URLRequest> request(context_.CreateRequest( | 
|  | FilePathToFileURL(directory.GetPath()), DEFAULT_PRIORITY, &delegate, | 
|  | TRAFFIC_ANNOTATION_FOR_TESTS)); | 
|  | request->Start(); | 
|  | EXPECT_TRUE(request->is_pending()); | 
|  |  | 
|  | base::RunLoop().Run(); | 
|  |  | 
|  | ASSERT_GT(delegate.bytes_received(), 0); | 
|  | ASSERT_LE(delegate.bytes_received(), kBufferSize); | 
|  | EXPECT_TRUE(HasHeader(delegate.data_received(), directory.GetPath())); | 
|  | EXPECT_TRUE(HasParentDirLink(delegate.data_received())); | 
|  | ASSERT_EQ(0, GetEntryCount(delegate.data_received())); | 
|  | EXPECT_FALSE(HasParentDirEntry(delegate.data_received())); | 
|  | } | 
|  |  | 
|  | // Android security policies prevent access to the root directory, so skip this | 
|  | // test there. | 
|  | #if !defined(OS_ANDROID) | 
|  | TEST_F(URLRequestFileDirTest, RootDirectory) { | 
|  | for (int slashes_to_test = 1; slashes_to_test < 4; ++slashes_to_test) { | 
|  | base::FilePath::StringType root_dir_string; | 
|  | #if defined(OS_WIN) | 
|  | root_dir_string = L"C:"; | 
|  | #endif | 
|  | root_dir_string.append(slashes_to_test, base::FilePath::kSeparators[0]); | 
|  | base::FilePath root_dir(root_dir_string); | 
|  | TestJobFactory factory(root_dir); | 
|  | context_.set_job_factory(&factory); | 
|  |  | 
|  | TestDelegate delegate; | 
|  | std::unique_ptr<URLRequest> request( | 
|  | context_.CreateRequest(FilePathToFileURL(root_dir), DEFAULT_PRIORITY, | 
|  | &delegate, TRAFFIC_ANNOTATION_FOR_TESTS)); | 
|  | request->Start(); | 
|  | EXPECT_TRUE(request->is_pending()); | 
|  |  | 
|  | base::RunLoop().Run(); | 
|  |  | 
|  | ASSERT_GT(delegate.bytes_received(), 0); | 
|  | ASSERT_LE(delegate.bytes_received(), kBufferSize); | 
|  | EXPECT_TRUE(HasHeader(delegate.data_received(), root_dir)); | 
|  | EXPECT_FALSE(HasParentDirLink(delegate.data_received())); | 
|  | EXPECT_GT(GetEntryCount(delegate.data_received()), 0); | 
|  | EXPECT_FALSE(HasParentDirEntry(delegate.data_received())); | 
|  | } | 
|  | } | 
|  | #endif  // !defined(OS_ANDROID) | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | }  // namespace net |