// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "fuchsia_web/webengine/browser/content_directory_loader_factory.h"

#include <fuchsia/io/cpp/fidl.h>
#include <lib/fdio/directory.h>
#include <lib/fdio/fd.h>

#include <algorithm>
#include <memory>
#include <string_view>
#include <utility>

#include "base/byte_size.h"
#include "base/command_line.h"
#include "base/compiler_specific.h"
#include "base/files/memory_mapped_file.h"
#include "base/fuchsia/file_utils.h"
#include "base/fuchsia/fuchsia_logging.h"
#include "base/json/json_reader.h"
#include "base/lazy_instance.h"
#include "base/logging.h"
#include "base/strings/strcat.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/string_view_util.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "fuchsia_web/common/fuchsia_dir_scheme.h"
#include "fuchsia_web/webengine/common/web_engine_content_client.h"
#include "fuchsia_web/webengine/switches.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "mojo/public/cpp/system/data_pipe_producer.h"
#include "mojo/public/cpp/system/string_data_source.h"
#include "net/base/filename_util.h"
#include "net/base/mime_sniffer.h"
#include "net/base/parse_number.h"
#include "net/http/http_byte_range.h"
#include "net/http/http_util.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/mojom/url_loader.mojom.h"
#include "services/network/public/mojom/url_response_head.mojom.h"

namespace {

// Maximum number of bytes to read when "sniffing" its MIME type.
constexpr size_t kMaxBytesToSniff = 1024 * 10;  // Read up to 10KB.

// The MIME type to use if "sniffing" fails to compute a result.
constexpr char kFallbackMimeType[] = "application/octet-stream";

// Returns a list of populated response HTTP headers.
// |mime_type|: The MIME type of the resource.
// |charset|: The resource's character set. Optional. If omitted, the browser
//            will assume the charset to be "text/plain" by default.
scoped_refptr<net::HttpResponseHeaders> CreateHeaders(
    std::string_view mime_type,
    const std::optional<std::string>& charset) {
  constexpr char kXFrameOptions[] = "X-Frame-Options";
  constexpr char kXFrameOptionsValue[] = "DENY";
  constexpr char kCacheControl[] = "Cache-Control";
  constexpr char kCacheControlValue[] = "no-cache";
  constexpr char kContentType[] = "Content-Type";
  constexpr char kCharsetSeparator[] = "; charset=";

  auto headers =
      base::MakeRefCounted<net::HttpResponseHeaders>("HTTP/1.1 200 OK\r\n");
  headers->SetHeader(kXFrameOptions, kXFrameOptionsValue);
  headers->SetHeader(kCacheControl, kCacheControlValue);

  if (charset) {
    headers->SetHeader(kContentType,
                       base::StrCat({mime_type, kCharsetSeparator, *charset}));
  } else {
    headers->SetHeader(kContentType, mime_type);
  }

  return headers;
}

// Determines which range of bytes should be sent in the response.
// If a range is specified in |headers|, then |start| and |length| are set to
// the range's boundaries and the function returns true.
// If no range is specified in |headers|, then the entire range [0,
// |max_length|) is set and the function returns true.
// If the requested range is invalid, then the function returns false.
bool GetRangeForRequest(const net::HttpRequestHeaders& headers,
                        size_t max_length,
                        size_t* start,
                        size_t* length) {
  std::optional<std::string> range_header =
      headers.GetHeader(net::HttpRequestHeaders::kRange);
  net::HttpByteRange byte_range;
  if (range_header) {
    std::vector<net::HttpByteRange> ranges;
    if (net::HttpUtil::ParseRangeHeader(*range_header, &ranges) &&
        ranges.size() == 1) {
      byte_range = ranges[0];
    } else {
      // Only one range is allowed.
      return false;
    }
  }
  if (!byte_range.ComputeBounds(max_length)) {
    return false;
  }

  *start = byte_range.first_byte_position();
  *length =
      byte_range.last_byte_position() - byte_range.first_byte_position() + 1;
  return true;
}

// Copies data from a fuchsia.io.Node file into a URL response stream.
class ContentDirectoryURLLoader final : public network::mojom::URLLoader {
 public:
  ContentDirectoryURLLoader() = default;
  ~ContentDirectoryURLLoader() override {
    // As we destruct, the `body_writer_` will postTask to cancel the
    // SequenceState, however the SequenceState can refer to the `mmap_` file.
    // To avoid a use-after-free, reset the data_pipe_producer_ first
    // (triggering the PostTask) and then PostTask the deletion of the mmap_.
    // TODO(b/497400727): Remove this once this bug is fixed or the code is
    // refactored to avoid the dependency.
    body_writer_.reset();
    base::SequencedTaskRunner::GetCurrentDefault()->DeleteSoon(
        FROM_HERE, std::move(mmap_));
  }

  ContentDirectoryURLLoader(const ContentDirectoryURLLoader&) = delete;
  ContentDirectoryURLLoader& operator=(const ContentDirectoryURLLoader&) =
      delete;

  // Creates a read-only MemoryMappedFile view to |file|.
  bool MapFile(fidl::InterfaceHandle<fuchsia::io::Node> file,
               base::MemoryMappedFile* mmap) {
    // Bind the file channel to a file descriptor so that we can use it for
    // reading.
    base::ScopedFD fd;
    if (zx_status_t status = fdio_fd_create(file.TakeChannel().release(),
                                            base::ScopedFD::Receiver(fd).get());
        status != ZX_OK) {
      // File-not-found errors are expected in some cases, so handle this result
      // w/o logging error text.
      ZX_DLOG_IF(WARNING, status != ZX_ERR_PEER_CLOSED, status)
          << "fdio_fd_create";
      return false;
    }

    // Map the file into memory.
    return mmap->Initialize(base::File(std::move(fd)),
                            base::MemoryMappedFile::READ_ONLY);
  }

  // Initiates data transfer from |file_channel| to |client_remote|.
  // |metadata_channel|, if it is connected to a file, is accessed to get the
  // MIME type and charset of the file.
  static void CreateAndStart(
      mojo::PendingReceiver<network::mojom::URLLoader> url_loader_receiver,
      const network::ResourceRequest& request,
      mojo::PendingRemote<network::mojom::URLLoaderClient> client_remote,
      fidl::InterfaceHandle<fuchsia::io::Node> file_channel,
      fidl::InterfaceHandle<fuchsia::io::Node> metadata_channel) {
    std::unique_ptr<ContentDirectoryURLLoader> loader =
        std::make_unique<ContentDirectoryURLLoader>();
    loader->Start(request, std::move(client_remote), std::move(file_channel),
                  std::move(metadata_channel));

    // |loader|'s lifetime is bound to the lifetime of the URLLoader Mojo
    // client endpoint.
    mojo::MakeSelfOwnedReceiver(std::move(loader),
                                std::move(url_loader_receiver));
  }

  void Start(const network::ResourceRequest& request,
             mojo::PendingRemote<network::mojom::URLLoaderClient> client_remote,
             fidl::InterfaceHandle<fuchsia::io::Node> file_channel,
             fidl::InterfaceHandle<fuchsia::io::Node> metadata_channel) {
    client_.Bind(std::move(client_remote));

    if (!MapFile(std::move(file_channel), mmap_.get())) {
      client_->OnComplete(network::URLLoaderCompletionStatus(net::ERR_FAILED));
      return;
    }

    // Construct and deliver the HTTP response header.
    auto response = network::mojom::URLResponseHead::New();

    // Read the charset and MIME type from the optional _metadata file.
    std::optional<std::string> charset;
    std::optional<std::string> mime_type;
    base::MemoryMappedFile metadata_mmap;
    if (MapFile(std::move(metadata_channel), &metadata_mmap)) {
      std::optional<base::Value> metadata_parsed =
          base::JSONReader::Read(base::as_string_view(metadata_mmap.bytes()),
                                 base::JSON_PARSE_CHROMIUM_EXTENSIONS);

      if (metadata_parsed && metadata_parsed->is_dict()) {
        const auto& dict = metadata_parsed->GetDict();
        const std::string* parsed_charset = dict.FindString("charset");
        if (parsed_charset)
          charset = *parsed_charset;

        const std::string* parsed_mime = dict.FindString("mime");
        if (parsed_mime)
          mime_type = *parsed_mime;
      }
    }

    // If a MIME type wasn't specified, then fall back on inferring the type
    // from the file's contents.
    if (!mime_type) {
      if (auto bytes = mmap_->bytes();
          !net::SniffMimeType(base::as_string_view(bytes.first(
                                  std::min(bytes.size(), kMaxBytesToSniff))),
                              request.url, /*type_hint=*/{},
                              net::ForceSniffFileUrlsForHtml::kDisabled,
                              &mime_type.emplace())) {
        if (!mime_type) {
          // Only set the fallback type if SniffMimeType completely gave up on
          // generating a suggestion.
          *mime_type = kFallbackMimeType;
        }
      }
    }

    size_t start_offset;
    size_t content_length;
    if (!GetRangeForRequest(request.headers, mmap_->bytes().size(),
                            &start_offset, &content_length)) {
      client_->OnComplete(network::URLLoaderCompletionStatus(
          net::ERR_REQUEST_RANGE_NOT_SATISFIABLE));
      return;
    }

    response->mime_type = *mime_type;
    response->headers = CreateHeaders(*mime_type, charset);
    response->content_length = content_length;

    // Set up the Mojo DataPipe used for streaming the response payload to the
    // client.
    mojo::ScopedDataPipeProducerHandle producer_handle;
    mojo::ScopedDataPipeConsumerHandle consumer_handle;
    MojoResult rv = mojo::CreateDataPipe(0u, producer_handle, consumer_handle);
    if (rv != MOJO_RESULT_OK) {
      client_->OnComplete(
          network::URLLoaderCompletionStatus(net::ERR_INSUFFICIENT_RESOURCES));
      return;
    }

    client_->OnReceiveResponse(std::move(response), std::move(consumer_handle),
                               std::nullopt);

    // Start reading the contents of |mmap_| into the response DataPipe.
    body_writer_ =
        std::make_unique<mojo::DataPipeProducer>(std::move(producer_handle));
    body_writer_->Write(
        std::make_unique<mojo::StringDataSource>(
            base::as_string_view(
                mmap_->bytes().subspan(start_offset, content_length)),
            mojo::StringDataSource::AsyncWritingMode::
                STRING_STAYS_VALID_UNTIL_COMPLETION),
        base::BindOnce(&ContentDirectoryURLLoader::OnWriteComplete,
                       base::Unretained(this)));
  }

  // network::mojom::URLLoader implementation:
  void FollowRedirect(
      network::HttpRequestHeadersUpdateParams headers_update_params,
      const std::optional<GURL>& new_url) override {}
  void SetPriority(net::RequestPriority priority,
                   int32_t intra_priority_value) override {}

 private:
  // Called when body_writer_->Write() has completed asynchronously.
  void OnWriteComplete(MojoResult result) {
    // Signal stream EOF to the client.
    body_writer_.reset();

    if (result != MOJO_RESULT_OK) {
      client_->OnComplete(network::URLLoaderCompletionStatus(net::ERR_FAILED));
      return;
    }

    network::URLLoaderCompletionStatus status(net::OK);
    const auto content_length = base::ByteSize(mmap_->bytes().size());
    status.encoded_data_length = content_length;
    status.encoded_body_length = content_length;
    status.decoded_body_length = content_length;
    client_->OnComplete(std::move(status));
  }

  // Used for sending status codes and response payloads to the client.
  mojo::Remote<network::mojom::URLLoaderClient> client_;

  // A read-only, memory mapped view of the file being loaded.
  std::unique_ptr<base::MemoryMappedFile> mmap_ =
      std::make_unique<base::MemoryMappedFile>();

  // Manages chunked data transfer over the response DataPipe.
  std::unique_ptr<mojo::DataPipeProducer> body_writer_;
};

}  // namespace

// static
const char ContentDirectoryLoaderFactory::kContentDirectoriesPath[] =
    "/content-directories";

// static
mojo::PendingRemote<network::mojom::URLLoaderFactory>
ContentDirectoryLoaderFactory::Create() {
  mojo::PendingRemote<network::mojom::URLLoaderFactory> pending_remote;

  // The ContentDirectoryLoaderFactory will delete itself when there are no more
  // receivers - see the network::SelfDeletingURLLoaderFactory::OnDisconnect
  // method.
  base::MakeSelfDeleting<ContentDirectoryLoaderFactory>(
      pending_remote.InitWithNewPipeAndPassReceiver());

  return pending_remote;
}

ContentDirectoryLoaderFactory::ContentDirectoryLoaderFactory(
    mojo::PendingReceiver<network::mojom::URLLoaderFactory> factory_receiver,
    base::SelfDeletingPassKey key)
    : network::SelfDeletingURLLoaderFactory(std::move(factory_receiver), key),
      task_runner_(base::ThreadPool::CreateSequencedTaskRunner(
          {base::MayBlock(), base::TaskPriority::USER_VISIBLE,
           base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN})) {
  content_directories_handle_ =
      base::OpenDirectoryHandle(base::FilePath(kContentDirectoriesPath));
}

ContentDirectoryLoaderFactory::~ContentDirectoryLoaderFactory() = default;

net::Error ContentDirectoryLoaderFactory::OpenFileFromDirectory(
    const std::string& content_directory_name,
    const base::FilePath& relative_file_path,
    fidl::InterfaceRequest<fuchsia::io::Node> file_request) {
  DCHECK(file_request);
  DCHECK(!relative_file_path.IsAbsolute());
  DCHECK(!content_directory_name.empty());

  if (!IsValidContentDirectoryName(content_directory_name)) {
    return net::ERR_FILE_NOT_FOUND;
  }

  if (!content_directories_handle_.is_valid()) {
    return net::ERR_FILE_NOT_FOUND;
  }

  if (content_directory_name != cached_directory_name_ ||
      !cached_directory_.is_valid()) {
    fidl::InterfaceHandle<fuchsia::io::Directory> directory_handle;
    const zx_status_t status =
        fdio_open3_at(content_directories_handle_.channel().get(),
                      content_directory_name.c_str(),
                      static_cast<uint64_t>(fuchsia::io::PERM_READABLE),
                      directory_handle.NewRequest().TakeChannel().release());
    if (status != ZX_OK) {
      ZX_LOG(WARNING, status) << "fdio_open3_at " << content_directory_name;
      return net::ERR_FILE_NOT_FOUND;
    }
    cached_directory_ = std::move(directory_handle);
    cached_directory_name_ = content_directory_name;
  }

  const zx_status_t status = fdio_open3_at(
      cached_directory_.channel().get(), relative_file_path.value().c_str(),
      static_cast<uint64_t>(fuchsia::io::PERM_READABLE),
      file_request.TakeChannel().release());
  if (status != ZX_OK) {
    ZX_LOG(WARNING, status) << "fdio_open3_at " << relative_file_path;
    return net::ERR_FILE_NOT_FOUND;
  }

  return net::OK;
}

void ContentDirectoryLoaderFactory::CreateLoaderAndStart(
    mojo::PendingReceiver<network::mojom::URLLoader> loader,
    int32_t request_id,
    uint32_t options,
    const network::ResourceRequest& request,
    mojo::PendingRemote<network::mojom::URLLoaderClient> client,
    const net::MutableNetworkTrafficAnnotationTag& traffic_annotation) {
  if (!request.url.SchemeIs(kFuchsiaDirScheme) || !request.url.is_valid()) {
    mojo::Remote<network::mojom::URLLoaderClient>(std::move(client))
        ->OnComplete(network::URLLoaderCompletionStatus(net::ERR_INVALID_URL));
    return;
  }

  if (request.method != "GET") {
    mojo::Remote<network::mojom::URLLoaderClient>(std::move(client))
        ->OnComplete(
            network::URLLoaderCompletionStatus(net::ERR_METHOD_NOT_SUPPORTED));
    return;
  }

  // Fuchsia paths do not support the notion of absolute paths, so strip the
  // leading slash from the URL's path fragment.
  std::string_view requested_path = request.url.path();
  DCHECK(base::StartsWith(requested_path, "/"));
  requested_path.remove_prefix(1);

  fidl::InterfaceHandle<fuchsia::io::Node> file_handle;
  net::Error open_result = OpenFileFromDirectory(request.url.GetHost(),
                                                 base::FilePath(requested_path),
                                                 file_handle.NewRequest());
  if (open_result != net::OK) {
    mojo::Remote<network::mojom::URLLoaderClient>(std::move(client))
        ->OnComplete(network::URLLoaderCompletionStatus(open_result));
    return;
  }
  DCHECK(file_handle);

  // Metadata files are optional. In the event that the file is absent,
  // |metadata_channel| will produce a ZX_CHANNEL_PEER_CLOSED status inside
  // ContentDirectoryURLLoader::Start().
  fidl::InterfaceHandle<fuchsia::io::Node> metadata_handle;
  open_result = OpenFileFromDirectory(
      request.url.GetHost(),
      base::FilePath(base::StrCat({requested_path, "._metadata"})),
      metadata_handle.NewRequest());
  if (open_result != net::OK) {
    mojo::Remote<network::mojom::URLLoaderClient>(std::move(client))
        ->OnComplete(network::URLLoaderCompletionStatus(open_result));
    return;
  }

  // Load the resource on a blocking-capable TaskRunner.
  task_runner_->PostTask(
      FROM_HERE,
      base::BindOnce(&ContentDirectoryURLLoader::CreateAndStart,
                     std::move(loader), request, std::move(client),
                     std::move(file_handle), std::move(metadata_handle)));
}
