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

#include "net/device_bound_sessions/url_fetcher.h"

#include <optional>
#include <utility>

#include "base/check.h"
#include "base/feature_list.h"
#include "net/base/features.h"
#include "net/base/io_buffer.h"
#include "net/base/net_errors.h"
#include "net/base/url_util.h"
#include "net/cert/x509_certificate.h"
#include "net/device_bound_sessions/session_binding_utils.h"
#include "net/device_bound_sessions/session_service.h"
#include "net/http/http_request_headers.h"
#include "net/ssl/ssl_cert_request_info.h"
#include "net/ssl/ssl_private_key.h"
#include "net/url_request/url_request_context.h"

namespace net::device_bound_sessions {

namespace {

constexpr net::NetworkTrafficAnnotationTag kRegistrationTrafficAnnotation =
    net::DefineNetworkTrafficAnnotation("dbsc_registration", R"(
        semantics {
          sender: "Device Bound Session Credentials API"
          description:
            "Device Bound Session Credentials (DBSC) let a server create a "
            "session with the local device. For more info see "
            "https://github.com/WICG/dbsc."
          trigger:
            "Server sending a response with a Sec-Session-Registration header."
          data: "A signed JWT with the new key created for this session."
          destination: WEBSITE
          last_reviewed: "2024-04-10"
          user_data {
            type: ACCESS_TOKEN
          }
          internal {
            contacts {
              email: "kristianm@chromium.org"
            }
            contacts {
              email: "chrome-counter-abuse-alerts@google.com"
            }
          }
        }
        policy {
          cookies_allowed: YES
          cookies_store: "user"
          setting: "There is no separate setting for this feature, but it will "
            "follow the cookie settings."
          policy_exception_justification: "Not implemented."
        })");

constexpr int kBufferSize = 4096;

}  // namespace

URLFetcher::URLFetcher(const URLRequestContext* context,
                       GURL url,
                       const url::Origin& referring_origin,
                       std::optional<net::NetLogSource> net_log_source,
                       bool is_refresh)
    : request_(context->CreateRequest(url,
                                      IDLE,
                                      this,
                                      kRegistrationTrafficAnnotation,
                                      // TODO(crbug.com/533319700): Support
                                      // targeting a network for DBSC fetches.
                                      net::handles::kInvalidNetworkHandle,
                                      /*is_for_websockets=*/false,
                                      net_log_source)),
      buf_(base::MakeRefCounted<IOBufferWithSize>(kBufferSize)),
      referring_origin_(referring_origin) {
  if (is_refresh &&
      base::FeatureList::IsEnabled(
          net::features::
              kDeviceBoundSessionsBypassDeferralsForRefreshRequests)) {
    request_->set_device_bound_session_mode(
        net::DeviceBoundSessionMode::kBypassDeferral);
  }
}

URLFetcher::~URLFetcher() = default;

void URLFetcher::Start(base::OnceClosure complete_callback) {
  callback_ = std::move(complete_callback);
  request_->Start();
}

void URLFetcher::OnReceivedRedirect(URLRequest* request,
                                    const RedirectInfo& redirect_info,
                                    bool* defer_redirect) {
  CHECK_EQ(request, request_.get());

  // 1. Strict Protocol-Downgrade Defense:
  // DBSC authentication state and cryptographic headers are strictly bound to
  // authenticated, cryptographic origins. Redirects targeting unencrypted or
  // non-trustworthy (HTTP) transports are rejected immediately to guarantee
  // zero plaintext leakage of session cookies and tokens.
  if (!IsSecure(redirect_info.new_url)) {
    request->CancelWithError(net::ERR_UNSAFE_REDIRECT);
    *defer_redirect = true;
    return;
  }

  // 2. Fetch-Metadata Synchronization:
  // Since `Sec-Fetch-Site` is dynamically calculated based on the relationship
  // between the `referring_origin_` and the target URI, HTTP redirects across
  // cross-origin boundaries render the initial header state stale.
  // Directly append the updated 'Sec-Fetch-Site' assertion onto the active
  // URLRequest before allowing the redirect execution chain to proceed.
  std::string_view updated_sec_fetch_site =
      SecFetchSiteForReferringOrigin(referring_origin_, redirect_info.new_url);
  request->SetExtraRequestHeaderByName(kSecFetchSiteHeaderName,
                                       updated_sec_fetch_site,
                                       /*overwrite=*/true);

  // 3. Synchronous Redirection Dispatch:
  // For synchronous //net URLRequest delegates, setting `*defer_redirect =
  // false` instructs the net::URLRequest state-machine to automatically forward
  // the redirect wire request upon returning from this callback, absorbing all
  // SetExtraRequestHeaderByName modifications.
  *defer_redirect = false;
}

void URLFetcher::OnResponseStarted(URLRequest* request, int net_error) {
  net_error_ = net_error;
  if (net_error != OK) {
    std::move(callback_).Run();
    // `this` may be deleted.
    return;
  }

  // Initiate the first read.
  int bytes_read_or_error = request->Read(buf_.get(), kBufferSize);
  if (bytes_read_or_error >= 0) {
    OnReadCompleted(request, bytes_read_or_error);
    // `this` may be deleted.
    return;
  } else if (bytes_read_or_error != ERR_IO_PENDING) {
    net_error_ = bytes_read_or_error;
    std::move(callback_).Run();
    // `this` may be deleted.
    return;
  }
}

void URLFetcher::OnReadCompleted(URLRequest* request, int bytes_read_or_error) {
  if (bytes_read_or_error > 0) {
    data_received_.append(buf_->data(), bytes_read_or_error);
  }

  while (bytes_read_or_error > 0) {
    bytes_read_or_error = request->Read(buf_.get(), kBufferSize);
    if (bytes_read_or_error > 0) {
      data_received_.append(buf_->data(), bytes_read_or_error);
    }
  }

  if (bytes_read_or_error < 0 && bytes_read_or_error != ERR_IO_PENDING) {
    net_error_ = bytes_read_or_error;
  }

  if (bytes_read_or_error != ERR_IO_PENDING) {
    std::move(callback_).Run();
    // `this` may be deleted.
    return;
  }
}

std::string URLFetcher::TakeDataReceived() {
  return std::move(data_received_);
}

void URLFetcher::OnCertificateRequested(URLRequest* request,
                                        SSLCertRequestInfo* cert_request_info) {
  SessionService* service = request->context()->device_bound_session_service();
  if (!service) {
    request->CancelWithError(ERR_SSL_CLIENT_AUTH_CERT_NEEDED);
    return;
  }

  service->SelectClientCertificate(
      request->url(), base::WrapRefCounted(cert_request_info),
      base::BindOnce(&URLFetcher::ContinueWithSelectedCertificate,
                     weak_factory_.GetWeakPtr()));
}

void URLFetcher::ContinueWithSelectedCertificate(
    scoped_refptr<X509Certificate> cert,
    scoped_refptr<SSLPrivateKey> key,
    bool cancel) {
  if (cancel) {
    request_->CancelWithError(ERR_SSL_CLIENT_AUTH_CERT_NEEDED);
  } else if (cert && key) {
    request_->ContinueWithCertificate(std::move(cert), std::move(key));
  } else {
    request_->ContinueWithCertificate(nullptr, nullptr);
  }
}

}  // namespace net::device_bound_sessions
