use crate::sources::registry::LoadResponse;
use crate::sources::registry::MaybeLock;
use crate::sources::registry::RegistryConfig;
use crate::sources::registry::RegistryData;
use crate::sources::registry::download;
use crate::util::Filesystem;
use crate::util::GlobalContext;
use crate::util::IntoUrl;
use crate::util::Progress;
use crate::util::ProgressStyle;
use crate::util::auth;
use crate::util::cache_lock::CacheLockMode;
use crate::util::data_structures::HashSet;
use crate::util::errors::CargoResult;
use crate::util::errors::HttpNotSuccessful;
use crate::util::interning::InternedString;
use crate::util::network::retry::Retry;
use crate::util::network::retry::RetryResult;
use crate::workspace::PackageId;
use crate::workspace::SourceId;
use crate::workspace::global_cache_tracker;
use anyhow::Context as _;
use cargo_credential::Operation;
use cargo_util::paths;
use futures::lock::Mutex;
use http::HeaderName;
use http::HeaderValue;
use http::Response;
use std::cell::Cell;
use std::cell::RefCell;
use std::fs;
use std::fs::File;
use std::io::ErrorKind;
use std::path::Path;
use std::str;
use std::time::Duration;
use tracing::debug;
use tracing::trace;
use tracing::warn;
use url::Url;
const ETAG: &'static str = "etag";
const LAST_MODIFIED: &'static str = "last-modified";
const UNKNOWN: &'static str = "Unknown";
pub struct HttpRegistry<'gctx> {
name: InternedString,
registry_config: Mutex<Option<RegistryConfig>>,
inner: HttpBackend<'gctx>,
}
impl<'gctx> HttpRegistry<'gctx> {
pub fn new(
source_id: SourceId,
gctx: &'gctx GlobalContext,
name: &str,
) -> CargoResult<HttpRegistry<'gctx>> {
Ok(HttpRegistry {
name: name.into(),
registry_config: Mutex::new(None),
inner: HttpBackend::new(source_id, gctx, name)?,
})
}
fn inner(&self) -> &HttpBackend<'gctx> {
&self.inner
}
async fn config(&self) -> CargoResult<RegistryConfig> {
let Some(config) = self.config_opt().await? else {
return Err(anyhow::anyhow!("config.json not found"));
};
Ok(config)
}
async fn config_opt(&self) -> CargoResult<Option<RegistryConfig>> {
let mut config = self.registry_config.lock().await;
if let Some(config) = &*config
&& self.inner().is_fresh(RegistryConfig::NAME)
{
Ok(Some(config.clone()))
} else {
let result = self.config_opt_inner().await?;
*config = result.clone();
Ok(result)
}
}
async fn config_opt_inner(&self) -> CargoResult<Option<RegistryConfig>> {
debug!("loading config");
let index_path = self.assert_index_locked(&self.inner().index_cache_path);
let config_json_path = index_path.join(RegistryConfig::NAME);
if self.inner().is_fresh(RegistryConfig::NAME)
&& let Some(config) = self.config_from_filesystem()
{
return Ok(Some(config.clone()));
}
if let Some(c) = self.config_from_filesystem() {
self.inner().auth_required.update(|v| v || c.auth_required);
}
let response = self
.inner()
.fetch_uncached(RegistryConfig::NAME, None)
.await;
let response = match response {
Err(e)
if !self.inner().auth_required.get()
&& e.downcast_ref::<HttpNotSuccessful>()
.map(|e| e.code == 401)
.unwrap_or_default() =>
{
self.inner().auth_required.set(true);
debug!(target: "network", "re-attempting request for config.json with authorization included.");
self.inner()
.fetch_uncached(RegistryConfig::NAME, None)
.await
}
resp => resp,
}?;
match response {
LoadResponse::Data {
raw_data,
index_version: _,
} => {
trace!("config loaded");
let config = Some(serde_json::from_slice(&raw_data)?);
if paths::create_dir_all(&config_json_path.parent().unwrap()).is_ok() {
if let Err(e) = fs::write(&config_json_path, &raw_data) {
tracing::debug!("failed to write config.json cache: {}", e);
}
}
Ok(config)
}
LoadResponse::NotFound => Ok(None),
LoadResponse::CacheValid => Err(crate::util::internal(
"config.json is never stored in the index cache",
)),
}
}
fn config_from_filesystem(&self) -> Option<RegistryConfig> {
let config_json_path = self
.assert_index_locked(&self.inner().index_cache_path)
.join(RegistryConfig::NAME);
match fs::read(&config_json_path) {
Ok(raw_data) => match serde_json::from_slice(&raw_data) {
Ok(json) => return Some(json),
Err(e) => tracing::debug!("failed to decode cached config.json: {}", e),
},
Err(e) => {
if e.kind() != ErrorKind::NotFound {
tracing::debug!("failed to read config.json cache: {}", e)
}
}
}
None
}
async fn sparse_fetch(
&self,
path: &str,
index_version: Option<&str>,
) -> CargoResult<LoadResponse> {
if let Some(index_version) = index_version {
trace!("local cache of {path} is available at version `{index_version}`",);
if self.inner().is_fresh(&path) {
return Ok(LoadResponse::CacheValid);
}
} else if self.inner().fresh.borrow().contains(path) {
debug!("cache did not contain previously downloaded file {path}",);
return Ok(LoadResponse::NotFound);
}
let index_version =
index_version
.and_then(|v| v.split_once(':'))
.and_then(|(key, value)| match key {
ETAG => Some((
http::header::IF_NONE_MATCH,
HeaderValue::from_str(value.trim()).ok()?,
)),
LAST_MODIFIED => Some((
http::header::IF_MODIFIED_SINCE,
HeaderValue::from_str(value.trim()).ok()?,
)),
_ => {
debug!("unexpected index version: {}", index_version.unwrap());
None
}
});
let index_version = index_version.as_ref().map(|(k, v)| (k, v));
self.inner().fetch_uncached(&path, index_version).await
}
}
#[async_trait::async_trait(?Send)]
impl<'gctx> RegistryData for HttpRegistry<'gctx> {
fn prepare(&self) -> CargoResult<()> {
self.inner()
.gctx
.deferred_global_last_use()?
.mark_registry_index_used(global_cache_tracker::RegistryIndex {
encoded_registry_name: self.name,
});
Ok(())
}
fn index_path(&self) -> &Filesystem {
&self.inner().index_cache_path
}
fn cache_path(&self) -> &Filesystem {
&self.inner().crate_cache_path
}
fn assert_index_locked<'a>(&self, path: &'a Filesystem) -> &'a Path {
self.inner()
.gctx
.assert_package_cache_locked(CacheLockMode::DownloadExclusive, path)
}
fn is_updated(&self) -> bool {
self.inner().requested_update.get()
}
async fn load(
&self,
_root: &Path,
path: &Path,
index_version: Option<&str>,
) -> CargoResult<LoadResponse> {
let Some(config) = self.config_opt().await? else {
return Ok(LoadResponse::NotFound);
};
self.inner()
.auth_required
.update(|v| v || config.auth_required);
let path = path
.to_str()
.ok_or_else(|| anyhow::anyhow!("non UTF8 path: {}", path.display()))?;
self.sparse_fetch(path, index_version).await
}
async fn config(&self) -> CargoResult<Option<RegistryConfig>> {
Ok(Some(self.config().await?))
}
fn invalidate_cache(&self) {
debug!("invalidated index cache");
self.inner().fresh.borrow_mut().clear();
self.inner().requested_update.set(true);
}
fn set_quiet(&mut self, quiet: bool) {
self.inner().quiet.set(quiet);
self.inner().progress.replace(None);
}
async fn download(&self, pkg: PackageId, checksum: &str) -> CargoResult<MaybeLock> {
let registry_config = self.config().await?;
download::download(
&self.inner().crate_cache_path,
&self.inner().gctx,
self.name.clone(),
pkg,
checksum,
registry_config,
)
}
async fn finish_download(
&self,
pkg: PackageId,
checksum: &str,
data: &[u8],
) -> CargoResult<File> {
download::finish_download(
&self.inner().crate_cache_path,
&self.inner().gctx,
self.name.clone(),
pkg,
checksum,
data,
)
}
fn is_crate_downloaded(&self, pkg: PackageId) -> bool {
download::is_crate_downloaded(&self.inner().crate_cache_path, &self.inner().gctx, pkg)
}
}
struct HttpBackend<'gctx> {
index_cache_path: Filesystem,
crate_cache_path: Filesystem,
source_id: SourceId,
gctx: &'gctx GlobalContext,
url: Url,
requested_update: Cell<bool>,
progress: RefCell<Option<Progress<'gctx>>>,
pending: Cell<usize>,
fresh: RefCell<HashSet<String>>,
fetch_started: Cell<bool>,
auth_required: Cell<bool>,
login_url: RefCell<Option<Url>>,
auth_error_headers: RefCell<Vec<String>>,
quiet: Cell<bool>,
}
impl<'gctx> HttpBackend<'gctx> {
pub fn new(
source_id: SourceId,
gctx: &'gctx GlobalContext,
name: &str,
) -> CargoResult<HttpBackend<'gctx>> {
let url = source_id.url().as_str();
if !url.ends_with('/') {
anyhow::bail!("sparse registry url must end in a slash `/`: {url}")
}
assert!(source_id.is_sparse());
let url = url
.strip_prefix("sparse+")
.expect("sparse registry needs sparse+ prefix")
.into_url()
.expect("a url with the sparse+ stripped should still be valid");
let index_cache_path = gctx.registry_index_path().join(name);
Ok(HttpBackend {
index_cache_path: index_cache_path.clone(),
crate_cache_path: gctx.registry_cache_path().join(name),
source_id,
gctx,
url,
progress: RefCell::new(Some(Progress::with_style(
"Fetch",
ProgressStyle::Indeterminate,
gctx,
))),
fresh: RefCell::new(HashSet::default()),
requested_update: Cell::new(false),
fetch_started: Cell::new(false),
auth_required: Cell::new(false),
login_url: RefCell::new(None),
auth_error_headers: RefCell::new(vec![]),
quiet: Cell::new(false),
pending: Cell::new(0),
})
}
fn full_url(&self, path: &str) -> String {
format!("{}{}", self.url, path)
}
fn start_fetch(&self) -> CargoResult<()> {
if self.fetch_started.get() {
return Ok(());
}
self.fetch_started.set(true);
if !self.quiet.get() {
self.gctx
.shell()
.status("Updating", self.source_id.display_index())?;
}
Ok(())
}
fn offline(&self) -> bool {
!self.gctx.network_allowed() || self.gctx.cli_unstable().no_index_update
}
fn is_fresh(&self, path: &str) -> bool {
if !self.requested_update.get() {
trace!("using local {path} as user did not request update",);
true
} else if self.offline() {
trace!("using local {path} in offline mode");
true
} else if self.fresh.borrow().contains(path) {
trace!("using local {path} as it was already fetched");
true
} else {
debug!("checking freshness of {path}");
false
}
}
async fn fetch_uncached(
&self,
path: &str,
extra_header: Option<(&HeaderName, &HeaderValue)>,
) -> CargoResult<LoadResponse> {
if self.offline() {
return Ok(LoadResponse::NotFound);
}
let mut r = Retry::new(self.gctx)?;
self.pending.update(|v| v + 1);
let response = loop {
let response = self.fetch_uncached_no_retry(path, extra_header).await;
match r.r#try(|| response) {
RetryResult::Success(result) => break Ok(result),
RetryResult::Err(error) => break Err(error),
RetryResult::Retry(delay_ms) => {
futures_timer::Delay::new(Duration::from_millis(delay_ms)).await;
}
}
};
self.pending.update(|v| v - 1);
if !self.fresh.borrow_mut().insert(path.to_string()) {
warn!("downloaded the index file `{path}` twice");
}
response
}
async fn fetch_uncached_no_retry(
&self,
path: &str,
extra_header: Option<(&HeaderName, &HeaderValue)>,
) -> CargoResult<LoadResponse> {
trace!("load: {path}");
self.start_fetch()?;
let full_url = self.full_url(path);
let mut request = http::Request::get(&full_url);
request = request.header("cargo-protocol", "version=1");
request = request.header(http::header::ACCEPT, "text/plain");
if let Some((k, v)) = extra_header {
request = request.header(k, v);
}
if self.auth_required.get() {
let authorization = auth::auth_token(
self.gctx,
&self.source_id,
self.login_url.borrow().clone().as_ref(),
Operation::Read,
self.auth_error_headers.borrow().clone(),
true,
)?;
request = request.header(http::header::AUTHORIZATION, authorization);
trace!(target: "network", "including authorization for {}", full_url);
}
let response = self
.gctx
.http_async()?
.request(request.body(Vec::new())?)
.await
.with_context(|| format!("download of {path} failed"))?;
self.tick()?;
let (response, body) = response.into_parts();
match response.status {
http::StatusCode::OK => {
let response_index_version =
if let Some(etag) = response.headers.get(http::header::ETAG) {
format!("{}: {}", ETAG, etag.to_str().unwrap())
} else if let Some(lm) = response.headers.get(http::header::LAST_MODIFIED) {
format!("{}: {}", LAST_MODIFIED, lm.to_str().unwrap())
} else {
UNKNOWN.to_string()
};
trace!("index file version: {}", response_index_version);
Ok(LoadResponse::Data {
raw_data: body,
index_version: Some(response_index_version),
})
}
http::StatusCode::NOT_MODIFIED => {
Ok(LoadResponse::CacheValid)
}
http::StatusCode::NOT_FOUND => {
return Ok(LoadResponse::NotFound);
}
http::StatusCode::UNAUTHORIZED => {
self.auth_error_headers.replace(
response
.headers
.iter()
.map(|(name, value)| {
format!("{}: {}", name.as_str(), value.to_str().unwrap_or_default())
})
.collect(),
);
for value in &response.headers.get_all(http::header::WWW_AUTHENTICATE) {
for challenge in
http_auth::ChallengeParser::new(value.to_str().unwrap_or_default())
{
match challenge {
Ok(challenge) if challenge.scheme.eq_ignore_ascii_case("Cargo") => {
for (param, value) in challenge.params {
if param.eq_ignore_ascii_case("login_url") {
self.login_url
.replace(Some(value.to_unescaped().into_url()?));
}
}
}
Ok(challenge) => {
debug!(target: "network", "ignoring non-Cargo challenge: {}", challenge.scheme)
}
Err(e) => {
debug!(target: "network", "failed to parse challenge: {}", e)
}
}
}
}
let mut err = Err(HttpNotSuccessful::new_from_response(
Response::from_parts(response, body),
&full_url,
)
.into());
if self.auth_required.get() {
let auth_error = auth::AuthorizationError::new(
self.gctx,
self.source_id,
self.login_url.borrow().clone(),
auth::AuthorizationErrorReason::TokenRejected,
)?;
err = err.context(auth_error)
}
err
}
_ => Err(HttpNotSuccessful::new_from_response(
Response::from_parts(response, body),
&full_url,
)
.into()),
}
}
fn tick(&self) -> CargoResult<()> {
let mut progress = self.progress.borrow_mut();
let Some(progress) = progress.as_mut() else {
return Ok(());
};
if progress.update_allowed() {
let complete = self.fresh.borrow().len();
let pending = self.pending.get();
progress.print_now(&format!("{complete} complete; {pending} pending"))?;
}
Ok(())
}
}