From ea2aecef5999af8e45cbef0ef690b9fa29640072 Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Mon, 19 Jun 2023 23:32:24 +0100 Subject: [PATCH 01/11] refactor: rename registry auth module to auth/mod.rs To be honest I don't even feel like they belong here. Maybe we should have a dedicated directory for registry-related code. --- src/cargo/util/{auth.rs => auth/mod.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/cargo/util/{auth.rs => auth/mod.rs} (100%) diff --git a/src/cargo/util/auth.rs b/src/cargo/util/auth/mod.rs similarity index 100% rename from src/cargo/util/auth.rs rename to src/cargo/util/auth/mod.rs From 71654316f38d00dd93a029393ad16657417a40f1 Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Tue, 20 Jun 2023 00:15:59 +0100 Subject: [PATCH 02/11] refactor: extract asymmetric to its own module --- src/cargo/util/auth/asymmetric.rs | 155 ++++++++++++++++++++++++++++++ src/cargo/util/auth/mod.rs | 129 ++----------------------- 2 files changed, 162 insertions(+), 122 deletions(-) create mode 100644 src/cargo/util/auth/asymmetric.rs diff --git a/src/cargo/util/auth/asymmetric.rs b/src/cargo/util/auth/asymmetric.rs new file mode 100644 index 00000000000..50882a74597 --- /dev/null +++ b/src/cargo/util/auth/asymmetric.rs @@ -0,0 +1,155 @@ +//! Registry asymmetric authentication support. See [RFC 3231] for more. +//! +//! [RFC 3231]: https://rust-lang.github.io/rfcs/3231-cargo-asymmetric-tokens.html + +use pasetors::keys::AsymmetricPublicKey; +use pasetors::keys::AsymmetricSecretKey; +use pasetors::paserk; +use pasetors::paserk::FormatAsPaserk; +use pasetors::version3; +use pasetors::version3::PublicToken; +use time::format_description::well_known::Rfc3339; +use time::OffsetDateTime; + +use crate::core::SourceId; +use crate::ops::RegistryCredentialConfig; +use crate::CargoResult; + +use super::Mutation; +use super::Secret; + +/// The main body of an asymmetric token as describe in RFC 3231. +#[derive(serde::Serialize)] +struct Message<'a> { + iat: &'a str, + #[serde(skip_serializing_if = "Option::is_none")] + sub: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + mutation: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + name: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + vers: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + cksum: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + challenge: Option<&'a str>, + /// This field is not yet used. This field can be set to a value >1 to + /// indicate a breaking change in the token format. + #[serde(skip_serializing_if = "Option::is_none")] + v: Option, +} + +/// The footer of an asymmetric token as describe in RFC 3231. +#[derive(serde::Serialize)] +struct Footer<'a> { + url: &'a str, + kip: paserk::Id, +} + +/// Checks that a secret key is valid, and returns the associated public key in +/// Paserk format. +pub fn paserk_public_from_paserk_secret(secret_key: Secret<&str>) -> Option { + let secret: Secret> = + secret_key.map(|key| key.try_into()).transpose().ok()?; + let public: AsymmetricPublicKey = secret + .as_ref() + .map(|key| key.try_into()) + .transpose() + .ok()? + .expose(); + let mut paserk_pub_key = String::new(); + FormatAsPaserk::fmt(&public, &mut paserk_pub_key).unwrap(); + Some(paserk_pub_key) +} + +/// Generates a public token from a registry's `credential` configuration for +/// authenticating to a `source_id` +/// +/// An optional `mutation` for authenticating a mutation operation aganist the +/// registry. +pub fn public_token_from_credential( + credential: RegistryCredentialConfig, + source_id: &SourceId, + mutation: Option<&'_ Mutation<'_>>, +) -> CargoResult> { + let RegistryCredentialConfig::AsymmetricKey((secret_key, secret_key_subject)) = credential else { + anyhow::bail!("credential must be an asymmetric secret key") + }; + + let secret: Secret> = + secret_key.map(|key| key.as_str().try_into()).transpose()?; + let public: AsymmetricPublicKey = secret + .as_ref() + .map(|key| key.try_into()) + .transpose()? + .expose(); + let kip = (&public).try_into()?; + let iat = OffsetDateTime::now_utc(); + + let message = Message { + iat: &iat.format(&Rfc3339)?, + sub: secret_key_subject.as_deref(), + mutation: mutation.and_then(|m| { + Some(match m { + Mutation::PrePublish => return None, + Mutation::Publish { .. } => "publish", + Mutation::Yank { .. } => "yank", + Mutation::Unyank { .. } => "unyank", + Mutation::Owners { .. } => "owners", + }) + }), + name: mutation.and_then(|m| { + Some(match m { + Mutation::PrePublish => return None, + Mutation::Publish { name, .. } + | Mutation::Yank { name, .. } + | Mutation::Unyank { name, .. } + | Mutation::Owners { name, .. } => *name, + }) + }), + vers: mutation.and_then(|m| { + Some(match m { + Mutation::PrePublish | Mutation::Owners { .. } => return None, + Mutation::Publish { vers, .. } + | Mutation::Yank { vers, .. } + | Mutation::Unyank { vers, .. } => *vers, + }) + }), + cksum: mutation.and_then(|m| { + Some(match m { + Mutation::PrePublish + | Mutation::Yank { .. } + | Mutation::Unyank { .. } + | Mutation::Owners { .. } => return None, + Mutation::Publish { cksum, .. } => *cksum, + }) + }), + challenge: None, // todo: PASETO with challenges + v: None, + }; + + let footer = Footer { + url: &source_id.url().to_string(), + kip, + }; + + let secret = secret + .map(|secret| { + PublicToken::sign( + &secret, + serde_json::to_string(&message) + .expect("cannot serialize") + .as_bytes(), + Some( + serde_json::to_string(&footer) + .expect("cannot serialize") + .as_bytes(), + ), + None, + ) + }) + .transpose()?; + + Ok(secret) +} diff --git a/src/cargo/util/auth/mod.rs b/src/cargo/util/auth/mod.rs index f19acaebe08..58309964f49 100644 --- a/src/cargo/util/auth/mod.rs +++ b/src/cargo/util/auth/mod.rs @@ -1,11 +1,11 @@ //! Registry authentication support. +mod asymmetric; + use crate::util::{config, config::ConfigKey, CanonicalUrl, CargoResult, Config, IntoUrl}; use anyhow::{bail, format_err, Context as _}; use cargo_util::ProcessError; use core::fmt; -use pasetors::keys::{AsymmetricPublicKey, AsymmetricSecretKey}; -use pasetors::paserk::FormatAsPaserk; use serde::Deserialize; use std::collections::HashMap; use std::error::Error; @@ -13,13 +13,13 @@ use std::io::{Read, Write}; use std::ops::Deref; use std::path::PathBuf; use std::process::{Command, Stdio}; -use time::format_description::well_known::Rfc3339; -use time::OffsetDateTime; use url::Url; use crate::core::SourceId; use crate::ops::RegistryCredentialConfig; +pub use self::asymmetric::paserk_public_from_paserk_secret; + use super::config::CredentialCacheValue; /// A wrapper for values that should not be printed. @@ -466,82 +466,9 @@ fn auth_token_optional( run_command(config, &process, sid, Action::Get)?.unwrap(); (independent_of_endpoint, Secret::from(token)) } - RegistryCredentialConfig::AsymmetricKey((secret_key, secret_key_subject)) => { - let secret: Secret> = - secret_key.map(|key| key.as_str().try_into()).transpose()?; - let public: AsymmetricPublicKey = secret - .as_ref() - .map(|key| key.try_into()) - .transpose()? - .expose(); - let kip: pasetors::paserk::Id = (&public).try_into()?; - let iat = OffsetDateTime::now_utc(); - - let message = Message { - iat: &iat.format(&Rfc3339)?, - sub: secret_key_subject.as_deref(), - mutation: mutation.and_then(|m| { - Some(match m { - Mutation::PrePublish => return None, - Mutation::Publish { .. } => "publish", - Mutation::Yank { .. } => "yank", - Mutation::Unyank { .. } => "unyank", - Mutation::Owners { .. } => "owners", - }) - }), - name: mutation.and_then(|m| { - Some(match m { - Mutation::PrePublish => return None, - Mutation::Publish { name, .. } - | Mutation::Yank { name, .. } - | Mutation::Unyank { name, .. } - | Mutation::Owners { name, .. } => *name, - }) - }), - vers: mutation.and_then(|m| { - Some(match m { - Mutation::PrePublish | Mutation::Owners { .. } => return None, - Mutation::Publish { vers, .. } - | Mutation::Yank { vers, .. } - | Mutation::Unyank { vers, .. } => *vers, - }) - }), - cksum: mutation.and_then(|m| { - Some(match m { - Mutation::PrePublish - | Mutation::Yank { .. } - | Mutation::Unyank { .. } - | Mutation::Owners { .. } => return None, - Mutation::Publish { cksum, .. } => *cksum, - }) - }), - challenge: None, // todo: PASETO with challenges - v: None, - }; - let footer = Footer { - url: &sid.url().to_string(), - kip, - }; - - ( - false, - secret - .map(|secret| { - pasetors::version3::PublicToken::sign( - &secret, - serde_json::to_string(&message) - .expect("cannot serialize") - .as_bytes(), - Some( - serde_json::to_string(&footer) - .expect("cannot serialize") - .as_bytes(), - ), - None, - ) - }) - .transpose()?, - ) + cred @ RegistryCredentialConfig::AsymmetricKey(..) => { + let token = asymmetric::public_token_from_credential(cred, sid, mutation)?; + (false, token) } }; @@ -595,33 +522,6 @@ pub enum Mutation<'a> { }, } -/// The main body of an asymmetric token as describe in RFC 3231. -#[derive(serde::Serialize)] -struct Message<'a> { - iat: &'a str, - #[serde(skip_serializing_if = "Option::is_none")] - sub: Option<&'a str>, - #[serde(skip_serializing_if = "Option::is_none")] - mutation: Option<&'a str>, - #[serde(skip_serializing_if = "Option::is_none")] - name: Option<&'a str>, - #[serde(skip_serializing_if = "Option::is_none")] - vers: Option<&'a str>, - #[serde(skip_serializing_if = "Option::is_none")] - cksum: Option<&'a str>, - #[serde(skip_serializing_if = "Option::is_none")] - challenge: Option<&'a str>, - /// This field is not yet used. This field can be set to a value >1 to indicate a breaking change in the token format. - #[serde(skip_serializing_if = "Option::is_none")] - v: Option, -} -/// The footer of an asymmetric token as describe in RFC 3231. -#[derive(serde::Serialize)] -struct Footer<'a> { - url: &'a str, - kip: pasetors::paserk::Id, -} - enum Action { Get, Store(String), @@ -646,21 +546,6 @@ pub fn login(config: &Config, sid: &SourceId, token: RegistryCredentialConfig) - Ok(()) } -/// Checks that a secret key is valid, and returns the associated public key in Paserk format. -pub(crate) fn paserk_public_from_paserk_secret(secret_key: Secret<&str>) -> Option { - let secret: Secret> = - secret_key.map(|key| key.try_into()).transpose().ok()?; - let public: AsymmetricPublicKey = secret - .as_ref() - .map(|key| key.try_into()) - .transpose() - .ok()? - .expose(); - let mut paserk_pub_key = String::new(); - FormatAsPaserk::fmt(&public, &mut paserk_pub_key).unwrap(); - Some(paserk_pub_key) -} - /// Removes the token for the given registry. pub fn logout(config: &Config, sid: &SourceId) -> CargoResult<()> { match registry_credential_config(config, sid)? { From 81948a6e4397b5d7bd9c060bd35cad6c30e95668 Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Tue, 20 Jun 2023 13:48:43 +0100 Subject: [PATCH 03/11] refactor: rename registry module to registry/mod.rs --- src/cargo/ops/{registry.rs => registry/mod.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/cargo/ops/{registry.rs => registry/mod.rs} (100%) diff --git a/src/cargo/ops/registry.rs b/src/cargo/ops/registry/mod.rs similarity index 100% rename from src/cargo/ops/registry.rs rename to src/cargo/ops/registry/mod.rs From e5382fcbf7bf1a050498007e5ed2b6500233c087 Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Tue, 20 Jun 2023 14:25:05 +0100 Subject: [PATCH 04/11] refactor: extract publish to its own module --- src/cargo/ops/registry/mod.rs | 452 +---------------------------- src/cargo/ops/registry/publish.rs | 461 ++++++++++++++++++++++++++++++ 2 files changed, 475 insertions(+), 438 deletions(-) create mode 100644 src/cargo/ops/registry/publish.rs diff --git a/src/cargo/ops/registry/mod.rs b/src/cargo/ops/registry/mod.rs index 748ded210c0..a28513571be 100644 --- a/src/cargo/ops/registry/mod.rs +++ b/src/cargo/ops/registry/mod.rs @@ -1,6 +1,11 @@ +//! Operations that interact with the [registry web API][1]. +//! +//! [1]: https://doc.rust-lang.org/nightly/cargo/reference/registry-web-api.html + +mod publish; + use std::cmp; -use std::collections::{BTreeMap, HashSet}; -use std::fs::File; +use std::collections::HashSet; use std::io::{self, BufRead}; use std::iter::repeat; use std::path::PathBuf; @@ -9,8 +14,7 @@ use std::task::Poll; use std::time::Duration; use anyhow::{anyhow, bail, format_err, Context as _}; -use cargo_util::paths; -use crates_io::{self, NewCrate, NewCrateDependency, Registry}; +use crates_io::{self, Registry}; use curl::easy::{Easy, InfoType, SslOpt, SslVersion}; use log::{log, Level}; use pasetors::keys::{AsymmetricKeyPair, Generate}; @@ -19,27 +23,22 @@ use termcolor::Color::Green; use termcolor::ColorSpec; use url::Url; -use crate::core::dependency::DepKind; -use crate::core::dependency::Dependency; -use crate::core::manifest::ManifestMetadata; -use crate::core::resolver::CliFeatures; use crate::core::source::Source; -use crate::core::QueryKind; -use crate::core::{Package, SourceId, Workspace}; -use crate::ops; -use crate::ops::Packages; -use crate::sources::{RegistrySource, SourceConfigMap, CRATES_IO_DOMAIN, CRATES_IO_REGISTRY}; +use crate::core::{SourceId, Workspace}; +use crate::sources::{RegistrySource, SourceConfigMap, CRATES_IO_DOMAIN}; use crate::util::auth::{ paserk_public_from_paserk_secret, Secret, {self, AuthorizationError}, }; -use crate::util::config::{Config, JobsConfig, SslVersionConfig, SslVersionConfigRange}; +use crate::util::config::{Config, SslVersionConfig, SslVersionConfigRange}; use crate::util::errors::CargoResult; use crate::util::important_paths::find_root_manifest_for_wd; use crate::util::network; use crate::util::{truncate_with_ellipsis, IntoUrl}; -use crate::util::{Progress, ProgressStyle}; use crate::{drop_print, drop_println, version}; +pub use self::publish::publish; +pub use self::publish::PublishOpts; + /// Registry settings loaded from config files. /// /// This is loaded based on the `--registry` flag and the config settings. @@ -96,429 +95,6 @@ impl RegistryCredentialConfig { } } -pub struct PublishOpts<'cfg> { - pub config: &'cfg Config, - pub token: Option>, - pub index: Option, - pub verify: bool, - pub allow_dirty: bool, - pub jobs: Option, - pub keep_going: bool, - pub to_publish: ops::Packages, - pub targets: Vec, - pub dry_run: bool, - pub registry: Option, - pub cli_features: CliFeatures, -} - -pub fn publish(ws: &Workspace<'_>, opts: &PublishOpts<'_>) -> CargoResult<()> { - let specs = opts.to_publish.to_package_id_specs(ws)?; - if specs.len() > 1 { - bail!("the `-p` argument must be specified to select a single package to publish") - } - if Packages::Default == opts.to_publish && ws.is_virtual() { - bail!("the `-p` argument must be specified in the root of a virtual workspace") - } - let member_ids = ws.members().map(|p| p.package_id()); - // Check that the spec matches exactly one member. - specs[0].query(member_ids)?; - let mut pkgs = ws.members_with_features(&specs, &opts.cli_features)?; - // In `members_with_features_old`, it will add "current" package (determined by the cwd) - // So we need filter - pkgs = pkgs - .into_iter() - .filter(|(m, _)| specs.iter().any(|spec| spec.matches(m.package_id()))) - .collect(); - // Double check. It is safe theoretically, unless logic has updated. - assert_eq!(pkgs.len(), 1); - - let (pkg, cli_features) = pkgs.pop().unwrap(); - - let mut publish_registry = opts.registry.clone(); - if let Some(ref allowed_registries) = *pkg.publish() { - if publish_registry.is_none() && allowed_registries.len() == 1 { - // If there is only one allowed registry, push to that one directly, - // even though there is no registry specified in the command. - let default_registry = &allowed_registries[0]; - if default_registry != CRATES_IO_REGISTRY { - // Don't change the registry for crates.io and don't warn the user. - // crates.io will be defaulted even without this. - opts.config.shell().note(&format!( - "Found `{}` as only allowed registry. Publishing to it automatically.", - default_registry - ))?; - publish_registry = Some(default_registry.clone()); - } - } - - let reg_name = publish_registry - .clone() - .unwrap_or_else(|| CRATES_IO_REGISTRY.to_string()); - if allowed_registries.is_empty() { - bail!( - "`{}` cannot be published.\n\ - `package.publish` is set to `false` or an empty list in Cargo.toml and prevents publishing.", - pkg.name(), - ); - } else if !allowed_registries.contains(®_name) { - bail!( - "`{}` cannot be published.\n\ - The registry `{}` is not listed in the `package.publish` value in Cargo.toml.", - pkg.name(), - reg_name - ); - } - } - // This is only used to confirm that we can create a token before we build the package. - // This causes the credential provider to be called an extra time, but keeps the same order of errors. - let ver = pkg.version().to_string(); - let mutation = auth::Mutation::PrePublish; - - let (mut registry, reg_ids) = registry( - opts.config, - opts.token.as_ref().map(Secret::as_deref), - opts.index.as_deref(), - publish_registry.as_deref(), - true, - Some(mutation).filter(|_| !opts.dry_run), - )?; - verify_dependencies(pkg, ®istry, reg_ids.original)?; - - // Prepare a tarball, with a non-suppressible warning if metadata - // is missing since this is being put online. - let tarball = ops::package_one( - ws, - pkg, - &ops::PackageOpts { - config: opts.config, - verify: opts.verify, - list: false, - check_metadata: true, - allow_dirty: opts.allow_dirty, - to_package: ops::Packages::Default, - targets: opts.targets.clone(), - jobs: opts.jobs.clone(), - keep_going: opts.keep_going, - cli_features: cli_features, - }, - )? - .unwrap(); - - if !opts.dry_run { - let hash = cargo_util::Sha256::new() - .update_file(tarball.file())? - .finish_hex(); - let mutation = Some(auth::Mutation::Publish { - name: pkg.name().as_str(), - vers: &ver, - cksum: &hash, - }); - registry.set_token(Some(auth::auth_token( - &opts.config, - ®_ids.original, - None, - mutation, - )?)); - } - - opts.config - .shell() - .status("Uploading", pkg.package_id().to_string())?; - transmit( - opts.config, - pkg, - tarball.file(), - &mut registry, - reg_ids.original, - opts.dry_run, - )?; - if !opts.dry_run { - const DEFAULT_TIMEOUT: u64 = 60; - let timeout = if opts.config.cli_unstable().publish_timeout { - let timeout: Option = opts.config.get("publish.timeout")?; - timeout.unwrap_or(DEFAULT_TIMEOUT) - } else { - DEFAULT_TIMEOUT - }; - if 0 < timeout { - let timeout = std::time::Duration::from_secs(timeout); - wait_for_publish(opts.config, reg_ids.original, pkg, timeout)?; - } - } - - Ok(()) -} - -fn verify_dependencies( - pkg: &Package, - registry: &Registry, - registry_src: SourceId, -) -> CargoResult<()> { - for dep in pkg.dependencies().iter() { - if super::check_dep_has_version(dep, true)? { - continue; - } - // TomlManifest::prepare_for_publish will rewrite the dependency - // to be just the `version` field. - if dep.source_id() != registry_src { - if !dep.source_id().is_registry() { - // Consider making SourceId::kind a public type that we can - // exhaustively match on. Using match can help ensure that - // every kind is properly handled. - panic!("unexpected source kind for dependency {:?}", dep); - } - // Block requests to send to crates.io with alt-registry deps. - // This extra hostname check is mostly to assist with testing, - // but also prevents someone using `--index` to specify - // something that points to crates.io. - if registry_src.is_crates_io() || registry.host_is_crates_io() { - bail!("crates cannot be published to crates.io with dependencies sourced from other\n\ - registries. `{}` needs to be published to crates.io before publishing this crate.\n\ - (crate `{}` is pulled from {})", - dep.package_name(), - dep.package_name(), - dep.source_id()); - } - } - } - Ok(()) -} - -fn transmit( - config: &Config, - pkg: &Package, - tarball: &File, - registry: &mut Registry, - registry_id: SourceId, - dry_run: bool, -) -> CargoResult<()> { - let deps = pkg - .dependencies() - .iter() - .filter(|dep| { - // Skip dev-dependency without version. - dep.is_transitive() || dep.specified_req() - }) - .map(|dep| { - // If the dependency is from a different registry, then include the - // registry in the dependency. - let dep_registry_id = match dep.registry_id() { - Some(id) => id, - None => SourceId::crates_io(config)?, - }; - // In the index and Web API, None means "from the same registry" - // whereas in Cargo.toml, it means "from crates.io". - let dep_registry = if dep_registry_id != registry_id { - Some(dep_registry_id.url().to_string()) - } else { - None - }; - - Ok(NewCrateDependency { - optional: dep.is_optional(), - default_features: dep.uses_default_features(), - name: dep.package_name().to_string(), - features: dep.features().iter().map(|s| s.to_string()).collect(), - version_req: dep.version_req().to_string(), - target: dep.platform().map(|s| s.to_string()), - kind: match dep.kind() { - DepKind::Normal => "normal", - DepKind::Build => "build", - DepKind::Development => "dev", - } - .to_string(), - registry: dep_registry, - explicit_name_in_toml: dep.explicit_name_in_toml().map(|s| s.to_string()), - }) - }) - .collect::>>()?; - let manifest = pkg.manifest(); - let ManifestMetadata { - ref authors, - ref description, - ref homepage, - ref documentation, - ref keywords, - ref readme, - ref repository, - ref license, - ref license_file, - ref categories, - ref badges, - ref links, - ref rust_version, - } = *manifest.metadata(); - let readme_content = readme - .as_ref() - .map(|readme| { - paths::read(&pkg.root().join(readme)) - .with_context(|| format!("failed to read `readme` file for package `{}`", pkg)) - }) - .transpose()?; - if let Some(ref file) = *license_file { - if !pkg.root().join(file).exists() { - bail!("the license file `{}` does not exist", file) - } - } - - // Do not upload if performing a dry run - if dry_run { - config.shell().warn("aborting upload due to dry run")?; - return Ok(()); - } - - let string_features = match manifest.original().features() { - Some(features) => features - .iter() - .map(|(feat, values)| { - ( - feat.to_string(), - values.iter().map(|fv| fv.to_string()).collect(), - ) - }) - .collect::>>(), - None => BTreeMap::new(), - }; - - let warnings = registry - .publish( - &NewCrate { - name: pkg.name().to_string(), - vers: pkg.version().to_string(), - deps, - features: string_features, - authors: authors.clone(), - description: description.clone(), - homepage: homepage.clone(), - documentation: documentation.clone(), - keywords: keywords.clone(), - categories: categories.clone(), - readme: readme_content, - readme_file: readme.clone(), - repository: repository.clone(), - license: license.clone(), - license_file: license_file.clone(), - badges: badges.clone(), - links: links.clone(), - rust_version: rust_version.clone(), - }, - tarball, - ) - .with_context(|| format!("failed to publish to registry at {}", registry.host()))?; - - if !warnings.invalid_categories.is_empty() { - let msg = format!( - "the following are not valid category slugs and were \ - ignored: {}. Please see https://crates.io/category_slugs \ - for the list of all category slugs. \ - ", - warnings.invalid_categories.join(", ") - ); - config.shell().warn(&msg)?; - } - - if !warnings.invalid_badges.is_empty() { - let msg = format!( - "the following are not valid badges and were ignored: {}. \ - Either the badge type specified is unknown or a required \ - attribute is missing. Please see \ - https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata \ - for valid badge types and their required attributes.", - warnings.invalid_badges.join(", ") - ); - config.shell().warn(&msg)?; - } - - if !warnings.other.is_empty() { - for msg in warnings.other { - config.shell().warn(&msg)?; - } - } - - Ok(()) -} - -fn wait_for_publish( - config: &Config, - registry_src: SourceId, - pkg: &Package, - timeout: std::time::Duration, -) -> CargoResult<()> { - let version_req = format!("={}", pkg.version()); - let mut source = SourceConfigMap::empty(config)?.load(registry_src, &HashSet::new())?; - // Disable the source's built-in progress bars. Repeatedly showing a bunch - // of independent progress bars can be a little confusing. There is an - // overall progress bar managed here. - source.set_quiet(true); - let source_description = source.source_id().to_string(); - let query = Dependency::parse(pkg.name(), Some(&version_req), registry_src)?; - - let now = std::time::Instant::now(); - let sleep_time = std::time::Duration::from_secs(1); - let max = timeout.as_secs() as usize; - // Short does not include the registry name. - let short_pkg_description = format!("{} v{}", pkg.name(), pkg.version()); - config.shell().status( - "Uploaded", - format!("{short_pkg_description} to {source_description}"), - )?; - config.shell().note(format!( - "Waiting for `{short_pkg_description}` to be available at {source_description}.\n\ - You may press ctrl-c to skip waiting; the crate should be available shortly." - ))?; - let mut progress = Progress::with_style("Waiting", ProgressStyle::Ratio, config); - progress.tick_now(0, max, "")?; - let is_available = loop { - { - let _lock = config.acquire_package_cache_lock()?; - // Force re-fetching the source - // - // As pulling from a git source is expensive, we track when we've done it within the - // process to only do it once, but we are one of the rare cases that needs to do it - // multiple times - config - .updated_sources() - .remove(&source.replaced_source_id()); - source.invalidate_cache(); - let summaries = loop { - // Exact to avoid returning all for path/git - match source.query_vec(&query, QueryKind::Exact) { - std::task::Poll::Ready(res) => { - break res?; - } - std::task::Poll::Pending => source.block_until_ready()?, - } - }; - if !summaries.is_empty() { - break true; - } - } - - let elapsed = now.elapsed(); - if timeout < elapsed { - config.shell().warn(format!( - "timed out waiting for `{short_pkg_description}` to be available in {source_description}", - ))?; - config.shell().note( - "The registry may have a backlog that is delaying making the \ - crate available. The crate should be available soon.", - )?; - break false; - } - - progress.tick_now(elapsed.as_secs() as usize, max, "")?; - std::thread::sleep(sleep_time); - }; - if is_available { - config.shell().status( - "Published", - format!("{short_pkg_description} at {source_description}"), - )?; - } - - Ok(()) -} - /// Returns the `Registry` and `Source` based on command-line and config settings. /// /// * `token_from_cmdline`: The token from the command-line. If not set, uses the token diff --git a/src/cargo/ops/registry/publish.rs b/src/cargo/ops/registry/publish.rs new file mode 100644 index 00000000000..7f4fbbae23d --- /dev/null +++ b/src/cargo/ops/registry/publish.rs @@ -0,0 +1,461 @@ +//! Interacts with the registry [publish API][1]. +//! +//! [1]: https://doc.rust-lang.org/nightly/cargo/reference/registry-web-api.html#publish + +use std::collections::BTreeMap; +use std::collections::HashSet; +use std::fs::File; +use std::time::Duration; + +use anyhow::bail; +use anyhow::Context as _; +use cargo_util::paths; +use crates_io::NewCrate; +use crates_io::NewCrateDependency; +use crates_io::Registry; + +use crate::core::dependency::DepKind; +use crate::core::manifest::ManifestMetadata; +use crate::core::resolver::CliFeatures; +use crate::core::Dependency; +use crate::core::Package; +use crate::core::QueryKind; +use crate::core::SourceId; +use crate::core::Workspace; +use crate::ops; +use crate::ops::PackageOpts; +use crate::ops::Packages; +use crate::sources::SourceConfigMap; +use crate::sources::CRATES_IO_REGISTRY; +use crate::util::auth; +use crate::util::auth::Secret; +use crate::util::config::JobsConfig; +use crate::util::Progress; +use crate::util::ProgressStyle; +use crate::CargoResult; +use crate::Config; + +use super::super::check_dep_has_version; + +pub struct PublishOpts<'cfg> { + pub config: &'cfg Config, + pub token: Option>, + pub index: Option, + pub verify: bool, + pub allow_dirty: bool, + pub jobs: Option, + pub keep_going: bool, + pub to_publish: ops::Packages, + pub targets: Vec, + pub dry_run: bool, + pub registry: Option, + pub cli_features: CliFeatures, +} + +pub fn publish(ws: &Workspace<'_>, opts: &PublishOpts<'_>) -> CargoResult<()> { + let specs = opts.to_publish.to_package_id_specs(ws)?; + if specs.len() > 1 { + bail!("the `-p` argument must be specified to select a single package to publish") + } + if Packages::Default == opts.to_publish && ws.is_virtual() { + bail!("the `-p` argument must be specified in the root of a virtual workspace") + } + let member_ids = ws.members().map(|p| p.package_id()); + // Check that the spec matches exactly one member. + specs[0].query(member_ids)?; + let mut pkgs = ws.members_with_features(&specs, &opts.cli_features)?; + // In `members_with_features_old`, it will add "current" package (determined by the cwd) + // So we need filter + pkgs = pkgs + .into_iter() + .filter(|(m, _)| specs.iter().any(|spec| spec.matches(m.package_id()))) + .collect(); + // Double check. It is safe theoretically, unless logic has updated. + assert_eq!(pkgs.len(), 1); + + let (pkg, cli_features) = pkgs.pop().unwrap(); + + let mut publish_registry = opts.registry.clone(); + if let Some(ref allowed_registries) = *pkg.publish() { + if publish_registry.is_none() && allowed_registries.len() == 1 { + // If there is only one allowed registry, push to that one directly, + // even though there is no registry specified in the command. + let default_registry = &allowed_registries[0]; + if default_registry != CRATES_IO_REGISTRY { + // Don't change the registry for crates.io and don't warn the user. + // crates.io will be defaulted even without this. + opts.config.shell().note(&format!( + "Found `{}` as only allowed registry. Publishing to it automatically.", + default_registry + ))?; + publish_registry = Some(default_registry.clone()); + } + } + + let reg_name = publish_registry + .clone() + .unwrap_or_else(|| CRATES_IO_REGISTRY.to_string()); + if allowed_registries.is_empty() { + bail!( + "`{}` cannot be published.\n\ + `package.publish` is set to `false` or an empty list in Cargo.toml and prevents publishing.", + pkg.name(), + ); + } else if !allowed_registries.contains(®_name) { + bail!( + "`{}` cannot be published.\n\ + The registry `{}` is not listed in the `package.publish` value in Cargo.toml.", + pkg.name(), + reg_name + ); + } + } + // This is only used to confirm that we can create a token before we build the package. + // This causes the credential provider to be called an extra time, but keeps the same order of errors. + let ver = pkg.version().to_string(); + let mutation = auth::Mutation::PrePublish; + + let (mut registry, reg_ids) = super::registry( + opts.config, + opts.token.as_ref().map(Secret::as_deref), + opts.index.as_deref(), + publish_registry.as_deref(), + true, + Some(mutation).filter(|_| !opts.dry_run), + )?; + verify_dependencies(pkg, ®istry, reg_ids.original)?; + + // Prepare a tarball, with a non-suppressible warning if metadata + // is missing since this is being put online. + let tarball = ops::package_one( + ws, + pkg, + &PackageOpts { + config: opts.config, + verify: opts.verify, + list: false, + check_metadata: true, + allow_dirty: opts.allow_dirty, + to_package: Packages::Default, + targets: opts.targets.clone(), + jobs: opts.jobs.clone(), + keep_going: opts.keep_going, + cli_features, + }, + )? + .unwrap(); + + if !opts.dry_run { + let hash = cargo_util::Sha256::new() + .update_file(tarball.file())? + .finish_hex(); + let mutation = Some(auth::Mutation::Publish { + name: pkg.name().as_str(), + vers: &ver, + cksum: &hash, + }); + registry.set_token(Some(auth::auth_token( + &opts.config, + ®_ids.original, + None, + mutation, + )?)); + } + + opts.config + .shell() + .status("Uploading", pkg.package_id().to_string())?; + transmit( + opts.config, + pkg, + tarball.file(), + &mut registry, + reg_ids.original, + opts.dry_run, + )?; + if !opts.dry_run { + const DEFAULT_TIMEOUT: u64 = 60; + let timeout = if opts.config.cli_unstable().publish_timeout { + let timeout: Option = opts.config.get("publish.timeout")?; + timeout.unwrap_or(DEFAULT_TIMEOUT) + } else { + DEFAULT_TIMEOUT + }; + if 0 < timeout { + let timeout = Duration::from_secs(timeout); + wait_for_publish(opts.config, reg_ids.original, pkg, timeout)?; + } + } + + Ok(()) +} + +fn wait_for_publish( + config: &Config, + registry_src: SourceId, + pkg: &Package, + timeout: Duration, +) -> CargoResult<()> { + let version_req = format!("={}", pkg.version()); + let mut source = SourceConfigMap::empty(config)?.load(registry_src, &HashSet::new())?; + // Disable the source's built-in progress bars. Repeatedly showing a bunch + // of independent progress bars can be a little confusing. There is an + // overall progress bar managed here. + source.set_quiet(true); + let source_description = source.source_id().to_string(); + let query = Dependency::parse(pkg.name(), Some(&version_req), registry_src)?; + + let now = std::time::Instant::now(); + let sleep_time = Duration::from_secs(1); + let max = timeout.as_secs() as usize; + // Short does not include the registry name. + let short_pkg_description = format!("{} v{}", pkg.name(), pkg.version()); + config.shell().status( + "Uploaded", + format!("{short_pkg_description} to {source_description}"), + )?; + config.shell().note(format!( + "Waiting for `{short_pkg_description}` to be available at {source_description}.\n\ + You may press ctrl-c to skip waiting; the crate should be available shortly." + ))?; + let mut progress = Progress::with_style("Waiting", ProgressStyle::Ratio, config); + progress.tick_now(0, max, "")?; + let is_available = loop { + { + let _lock = config.acquire_package_cache_lock()?; + // Force re-fetching the source + // + // As pulling from a git source is expensive, we track when we've done it within the + // process to only do it once, but we are one of the rare cases that needs to do it + // multiple times + config + .updated_sources() + .remove(&source.replaced_source_id()); + source.invalidate_cache(); + let summaries = loop { + // Exact to avoid returning all for path/git + match source.query_vec(&query, QueryKind::Exact) { + std::task::Poll::Ready(res) => { + break res?; + } + std::task::Poll::Pending => source.block_until_ready()?, + } + }; + if !summaries.is_empty() { + break true; + } + } + + let elapsed = now.elapsed(); + if timeout < elapsed { + config.shell().warn(format!( + "timed out waiting for `{short_pkg_description}` to be available in {source_description}", + ))?; + config.shell().note( + "The registry may have a backlog that is delaying making the \ + crate available. The crate should be available soon.", + )?; + break false; + } + + progress.tick_now(elapsed.as_secs() as usize, max, "")?; + std::thread::sleep(sleep_time); + }; + if is_available { + config.shell().status( + "Published", + format!("{short_pkg_description} at {source_description}"), + )?; + } + + Ok(()) +} + +fn verify_dependencies( + pkg: &Package, + registry: &Registry, + registry_src: SourceId, +) -> CargoResult<()> { + for dep in pkg.dependencies().iter() { + if check_dep_has_version(dep, true)? { + continue; + } + // TomlManifest::prepare_for_publish will rewrite the dependency + // to be just the `version` field. + if dep.source_id() != registry_src { + if !dep.source_id().is_registry() { + // Consider making SourceId::kind a public type that we can + // exhaustively match on. Using match can help ensure that + // every kind is properly handled. + panic!("unexpected source kind for dependency {:?}", dep); + } + // Block requests to send to crates.io with alt-registry deps. + // This extra hostname check is mostly to assist with testing, + // but also prevents someone using `--index` to specify + // something that points to crates.io. + if registry_src.is_crates_io() || registry.host_is_crates_io() { + bail!("crates cannot be published to crates.io with dependencies sourced from other\n\ + registries. `{}` needs to be published to crates.io before publishing this crate.\n\ + (crate `{}` is pulled from {})", + dep.package_name(), + dep.package_name(), + dep.source_id()); + } + } + } + Ok(()) +} + +fn transmit( + config: &Config, + pkg: &Package, + tarball: &File, + registry: &mut Registry, + registry_id: SourceId, + dry_run: bool, +) -> CargoResult<()> { + let deps = pkg + .dependencies() + .iter() + .filter(|dep| { + // Skip dev-dependency without version. + dep.is_transitive() || dep.specified_req() + }) + .map(|dep| { + // If the dependency is from a different registry, then include the + // registry in the dependency. + let dep_registry_id = match dep.registry_id() { + Some(id) => id, + None => SourceId::crates_io(config)?, + }; + // In the index and Web API, None means "from the same registry" + // whereas in Cargo.toml, it means "from crates.io". + let dep_registry = if dep_registry_id != registry_id { + Some(dep_registry_id.url().to_string()) + } else { + None + }; + + Ok(NewCrateDependency { + optional: dep.is_optional(), + default_features: dep.uses_default_features(), + name: dep.package_name().to_string(), + features: dep.features().iter().map(|s| s.to_string()).collect(), + version_req: dep.version_req().to_string(), + target: dep.platform().map(|s| s.to_string()), + kind: match dep.kind() { + DepKind::Normal => "normal", + DepKind::Build => "build", + DepKind::Development => "dev", + } + .to_string(), + registry: dep_registry, + explicit_name_in_toml: dep.explicit_name_in_toml().map(|s| s.to_string()), + }) + }) + .collect::>>()?; + let manifest = pkg.manifest(); + let ManifestMetadata { + ref authors, + ref description, + ref homepage, + ref documentation, + ref keywords, + ref readme, + ref repository, + ref license, + ref license_file, + ref categories, + ref badges, + ref links, + ref rust_version, + } = *manifest.metadata(); + let readme_content = readme + .as_ref() + .map(|readme| { + paths::read(&pkg.root().join(readme)) + .with_context(|| format!("failed to read `readme` file for package `{}`", pkg)) + }) + .transpose()?; + if let Some(ref file) = *license_file { + if !pkg.root().join(file).exists() { + bail!("the license file `{}` does not exist", file) + } + } + + // Do not upload if performing a dry run + if dry_run { + config.shell().warn("aborting upload due to dry run")?; + return Ok(()); + } + + let string_features = match manifest.original().features() { + Some(features) => features + .iter() + .map(|(feat, values)| { + ( + feat.to_string(), + values.iter().map(|fv| fv.to_string()).collect(), + ) + }) + .collect::>>(), + None => BTreeMap::new(), + }; + + let warnings = registry + .publish( + &NewCrate { + name: pkg.name().to_string(), + vers: pkg.version().to_string(), + deps, + features: string_features, + authors: authors.clone(), + description: description.clone(), + homepage: homepage.clone(), + documentation: documentation.clone(), + keywords: keywords.clone(), + categories: categories.clone(), + readme: readme_content, + readme_file: readme.clone(), + repository: repository.clone(), + license: license.clone(), + license_file: license_file.clone(), + badges: badges.clone(), + links: links.clone(), + rust_version: rust_version.clone(), + }, + tarball, + ) + .with_context(|| format!("failed to publish to registry at {}", registry.host()))?; + + if !warnings.invalid_categories.is_empty() { + let msg = format!( + "the following are not valid category slugs and were \ + ignored: {}. Please see https://crates.io/category_slugs \ + for the list of all category slugs. \ + ", + warnings.invalid_categories.join(", ") + ); + config.shell().warn(&msg)?; + } + + if !warnings.invalid_badges.is_empty() { + let msg = format!( + "the following are not valid badges and were ignored: {}. \ + Either the badge type specified is unknown or a required \ + attribute is missing. Please see \ + https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata \ + for valid badge types and their required attributes.", + warnings.invalid_badges.join(", ") + ); + config.shell().warn(&msg)?; + } + + if !warnings.other.is_empty() { + for msg in warnings.other { + config.shell().warn(&msg)?; + } + } + + Ok(()) +} From d39c6504fa7f364db506a5877d5ceec22eba37e6 Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Tue, 20 Jun 2023 14:52:54 +0100 Subject: [PATCH 05/11] refactor: extract search to its own module --- src/cargo/ops/registry/mod.rs | 88 +---------------------------- src/cargo/ops/registry/search.rs | 95 ++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 85 deletions(-) create mode 100644 src/cargo/ops/registry/search.rs diff --git a/src/cargo/ops/registry/mod.rs b/src/cargo/ops/registry/mod.rs index a28513571be..6e981d42e92 100644 --- a/src/cargo/ops/registry/mod.rs +++ b/src/cargo/ops/registry/mod.rs @@ -3,11 +3,10 @@ //! [1]: https://doc.rust-lang.org/nightly/cargo/reference/registry-web-api.html mod publish; +mod search; -use std::cmp; use std::collections::HashSet; use std::io::{self, BufRead}; -use std::iter::repeat; use std::path::PathBuf; use std::str; use std::task::Poll; @@ -19,9 +18,6 @@ use curl::easy::{Easy, InfoType, SslOpt, SslVersion}; use log::{log, Level}; use pasetors::keys::{AsymmetricKeyPair, Generate}; use pasetors::paserk::FormatAsPaserk; -use termcolor::Color::Green; -use termcolor::ColorSpec; -use url::Url; use crate::core::source::Source; use crate::core::{SourceId, Workspace}; @@ -33,11 +29,12 @@ use crate::util::config::{Config, SslVersionConfig, SslVersionConfigRange}; use crate::util::errors::CargoResult; use crate::util::important_paths::find_root_manifest_for_wd; use crate::util::network; -use crate::util::{truncate_with_ellipsis, IntoUrl}; +use crate::util::IntoUrl; use crate::{drop_print, drop_println, version}; pub use self::publish::publish; pub use self::publish::PublishOpts; +pub use self::search::search; /// Registry settings loaded from config files. /// @@ -722,82 +719,3 @@ struct RegistrySourceIds { /// User-defined source replacement is not applied. replacement: SourceId, } - -pub fn search( - query: &str, - config: &Config, - index: Option, - limit: u32, - reg: Option, -) -> CargoResult<()> { - let (mut registry, source_ids) = - registry(config, None, index.as_deref(), reg.as_deref(), false, None)?; - let (crates, total_crates) = registry.search(query, limit).with_context(|| { - format!( - "failed to retrieve search results from the registry at {}", - registry.host() - ) - })?; - - let names = crates - .iter() - .map(|krate| format!("{} = \"{}\"", krate.name, krate.max_version)) - .collect::>(); - - let description_margin = names.iter().map(|s| s.len() + 4).max().unwrap_or_default(); - - let description_length = cmp::max(80, 128 - description_margin); - - let descriptions = crates.iter().map(|krate| { - krate - .description - .as_ref() - .map(|desc| truncate_with_ellipsis(&desc.replace("\n", " "), description_length)) - }); - - for (name, description) in names.into_iter().zip(descriptions) { - let line = match description { - Some(desc) => { - let space = repeat(' ') - .take(description_margin - name.len()) - .collect::(); - name + &space + "# " + &desc - } - None => name, - }; - let mut fragments = line.split(query).peekable(); - while let Some(fragment) = fragments.next() { - let _ = config.shell().write_stdout(fragment, &ColorSpec::new()); - if fragments.peek().is_some() { - let _ = config - .shell() - .write_stdout(query, &ColorSpec::new().set_bold(true).set_fg(Some(Green))); - } - } - let _ = config.shell().write_stdout("\n", &ColorSpec::new()); - } - - let search_max_limit = 100; - if total_crates > limit && limit < search_max_limit { - let _ = config.shell().write_stdout( - format_args!( - "... and {} crates more (use --limit N to see more)\n", - total_crates - limit - ), - &ColorSpec::new(), - ); - } else if total_crates > limit && limit >= search_max_limit { - let extra = if source_ids.original.is_crates_io() { - let url = Url::parse_with_params("https://crates.io/search", &[("q", query)])?; - format!(" (go to {url} to see more)") - } else { - String::new() - }; - let _ = config.shell().write_stdout( - format_args!("... and {} crates more{}\n", total_crates - limit, extra), - &ColorSpec::new(), - ); - } - - Ok(()) -} diff --git a/src/cargo/ops/registry/search.rs b/src/cargo/ops/registry/search.rs new file mode 100644 index 00000000000..5e01ea98fef --- /dev/null +++ b/src/cargo/ops/registry/search.rs @@ -0,0 +1,95 @@ +//! Interacts with the registry [search API][1]. +//! +//! [1]: https://doc.rust-lang.org/nightly/cargo/reference/registry-web-api.html#search + +use std::cmp; +use std::iter::repeat; + +use anyhow::Context as _; +use termcolor::Color; +use termcolor::ColorSpec; +use url::Url; + +use crate::util::truncate_with_ellipsis; +use crate::CargoResult; +use crate::Config; + +pub fn search( + query: &str, + config: &Config, + index: Option, + limit: u32, + reg: Option, +) -> CargoResult<()> { + let (mut registry, source_ids) = + super::registry(config, None, index.as_deref(), reg.as_deref(), false, None)?; + let (crates, total_crates) = registry.search(query, limit).with_context(|| { + format!( + "failed to retrieve search results from the registry at {}", + registry.host() + ) + })?; + + let names = crates + .iter() + .map(|krate| format!("{} = \"{}\"", krate.name, krate.max_version)) + .collect::>(); + + let description_margin = names.iter().map(|s| s.len() + 4).max().unwrap_or_default(); + + let description_length = cmp::max(80, 128 - description_margin); + + let descriptions = crates.iter().map(|krate| { + krate + .description + .as_ref() + .map(|desc| truncate_with_ellipsis(&desc.replace("\n", " "), description_length)) + }); + + for (name, description) in names.into_iter().zip(descriptions) { + let line = match description { + Some(desc) => { + let space = repeat(' ') + .take(description_margin - name.len()) + .collect::(); + name + &space + "# " + &desc + } + None => name, + }; + let mut fragments = line.split(query).peekable(); + while let Some(fragment) = fragments.next() { + let _ = config.shell().write_stdout(fragment, &ColorSpec::new()); + if fragments.peek().is_some() { + let _ = config.shell().write_stdout( + query, + &ColorSpec::new().set_bold(true).set_fg(Some(Color::Green)), + ); + } + } + let _ = config.shell().write_stdout("\n", &ColorSpec::new()); + } + + let search_max_limit = 100; + if total_crates > limit && limit < search_max_limit { + let _ = config.shell().write_stdout( + format_args!( + "... and {} crates more (use --limit N to see more)\n", + total_crates - limit + ), + &ColorSpec::new(), + ); + } else if total_crates > limit && limit >= search_max_limit { + let extra = if source_ids.original.is_crates_io() { + let url = Url::parse_with_params("https://crates.io/search", &[("q", query)])?; + format!(" (go to {url} to see more)") + } else { + String::new() + }; + let _ = config.shell().write_stdout( + format_args!("... and {} crates more{}\n", total_crates - limit, extra), + &ColorSpec::new(), + ); + } + + Ok(()) +} From 048267934b205974627c8ea806463d1e65fb8206 Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Tue, 20 Jun 2023 15:06:56 +0100 Subject: [PATCH 06/11] refactor: extract login to its own module --- src/cargo/ops/registry/login.rs | 160 ++++++++++++++++++++++++++++++++ src/cargo/ops/registry/mod.rs | 145 +---------------------------- 2 files changed, 165 insertions(+), 140 deletions(-) create mode 100644 src/cargo/ops/registry/login.rs diff --git a/src/cargo/ops/registry/login.rs b/src/cargo/ops/registry/login.rs new file mode 100644 index 00000000000..1e2b3a87b3b --- /dev/null +++ b/src/cargo/ops/registry/login.rs @@ -0,0 +1,160 @@ +//! Interacts with the registry [login API][1]. +//! +//! This doesn't really call any web API at this moment. Instead, it's just an +//! operation for `cargo login`. +//! +//! [1]: https://doc.rust-lang.org/nightly/cargo/reference/registry-web-api.html#login + +use std::io; +use std::io::BufRead; + +use anyhow::anyhow; +use anyhow::bail; +use anyhow::Context as _; +use pasetors::keys::AsymmetricKeyPair; +use pasetors::keys::Generate as _; +use pasetors::paserk::FormatAsPaserk; + +use crate::drop_println; +use crate::ops::RegistryCredentialConfig; +use crate::sources::CRATES_IO_DOMAIN; +use crate::util::auth; +use crate::util::auth::paserk_public_from_paserk_secret; +use crate::util::auth::AuthorizationError; +use crate::util::auth::Secret; +use crate::CargoResult; +use crate::Config; + +use super::get_source_id; + +pub fn registry_login( + config: &Config, + token: Option>, + reg: Option<&str>, + generate_keypair: bool, + secret_key_required: bool, + key_subject: Option<&str>, +) -> CargoResult<()> { + let source_ids = get_source_id(config, None, reg)?; + let reg_cfg = auth::registry_credential_config(config, &source_ids.original)?; + + let login_url = match super::registry(config, token.clone(), None, reg, false, None) { + Ok((registry, _)) => Some(format!("{}/me", registry.host())), + Err(e) if e.is::() => e + .downcast::() + .unwrap() + .login_url + .map(|u| u.to_string()), + Err(e) => return Err(e), + }; + let new_token; + if generate_keypair || secret_key_required || key_subject.is_some() { + if !config.cli_unstable().registry_auth { + let flag = if generate_keypair { + "generate-keypair" + } else if secret_key_required { + "secret-key" + } else if key_subject.is_some() { + "key-subject" + } else { + unreachable!("how did we get here"); + }; + bail!( + "the `{flag}` flag is unstable, pass `-Z registry-auth` to enable it\n\ + See https://github.com/rust-lang/cargo/issues/10519 for more \ + information about the `{flag}` flag." + ); + } + assert!(token.is_none()); + // we are dealing with asymmetric tokens + let (old_secret_key, old_key_subject) = match ®_cfg { + RegistryCredentialConfig::AsymmetricKey((old_secret_key, old_key_subject)) => { + (Some(old_secret_key), old_key_subject.clone()) + } + _ => (None, None), + }; + let secret_key: Secret; + if generate_keypair { + assert!(!secret_key_required); + let kp = AsymmetricKeyPair::::generate().unwrap(); + secret_key = Secret::default().map(|mut key| { + FormatAsPaserk::fmt(&kp.secret, &mut key).unwrap(); + key + }); + } else if secret_key_required { + assert!(!generate_keypair); + drop_println!(config, "please paste the API secret key below"); + secret_key = Secret::default() + .map(|mut line| { + let input = io::stdin(); + input + .lock() + .read_line(&mut line) + .with_context(|| "failed to read stdin") + .map(|_| line.trim().to_string()) + }) + .transpose()?; + } else { + secret_key = old_secret_key + .cloned() + .ok_or_else(|| anyhow!("need a secret_key to set a key_subject"))?; + } + if let Some(p) = paserk_public_from_paserk_secret(secret_key.as_deref()) { + drop_println!(config, "{}", &p); + } else { + bail!("not a validly formatted PASERK secret key"); + } + new_token = RegistryCredentialConfig::AsymmetricKey(( + secret_key, + match key_subject { + Some(key_subject) => Some(key_subject.to_string()), + None => old_key_subject, + }, + )); + } else { + new_token = RegistryCredentialConfig::Token(match token { + Some(token) => token.owned(), + None => { + if let Some(login_url) = login_url { + drop_println!( + config, + "please paste the token found on {} below", + login_url + ) + } else { + drop_println!( + config, + "please paste the token for {} below", + source_ids.original.display_registry_name() + ) + } + + let mut line = String::new(); + let input = io::stdin(); + input + .lock() + .read_line(&mut line) + .with_context(|| "failed to read stdin")?; + // Automatically remove `cargo login` from an inputted token to + // allow direct pastes from `registry.host()`/me. + Secret::from(line.replace("cargo login", "").trim().to_string()) + } + }); + + if let Some(tok) = new_token.as_token() { + crates_io::check_token(tok.as_ref().expose())?; + } + } + if ®_cfg == &new_token { + config.shell().status("Login", "already logged in")?; + return Ok(()); + } + + auth::login(config, &source_ids.original, new_token)?; + + config.shell().status( + "Login", + format!("token for `{}` saved", reg.unwrap_or(CRATES_IO_DOMAIN)), + )?; + Ok(()) +} diff --git a/src/cargo/ops/registry/mod.rs b/src/cargo/ops/registry/mod.rs index 6e981d42e92..3f1d666d8be 100644 --- a/src/cargo/ops/registry/mod.rs +++ b/src/cargo/ops/registry/mod.rs @@ -2,29 +2,25 @@ //! //! [1]: https://doc.rust-lang.org/nightly/cargo/reference/registry-web-api.html +mod login; mod publish; mod search; use std::collections::HashSet; -use std::io::{self, BufRead}; use std::path::PathBuf; use std::str; use std::task::Poll; use std::time::Duration; -use anyhow::{anyhow, bail, format_err, Context as _}; +use anyhow::{bail, format_err, Context as _}; use crates_io::{self, Registry}; use curl::easy::{Easy, InfoType, SslOpt, SslVersion}; use log::{log, Level}; -use pasetors::keys::{AsymmetricKeyPair, Generate}; -use pasetors::paserk::FormatAsPaserk; use crate::core::source::Source; use crate::core::{SourceId, Workspace}; -use crate::sources::{RegistrySource, SourceConfigMap, CRATES_IO_DOMAIN}; -use crate::util::auth::{ - paserk_public_from_paserk_secret, Secret, {self, AuthorizationError}, -}; +use crate::sources::{RegistrySource, SourceConfigMap}; +use crate::util::auth::{self, Secret}; use crate::util::config::{Config, SslVersionConfig, SslVersionConfigRange}; use crate::util::errors::CargoResult; use crate::util::important_paths::find_root_manifest_for_wd; @@ -32,6 +28,7 @@ use crate::util::network; use crate::util::IntoUrl; use crate::{drop_print, drop_println, version}; +pub use self::login::registry_login; pub use self::publish::publish; pub use self::publish::PublishOpts; pub use self::search::search; @@ -353,138 +350,6 @@ impl HttpTimeout { } } -pub fn registry_login( - config: &Config, - token: Option>, - reg: Option<&str>, - generate_keypair: bool, - secret_key_required: bool, - key_subject: Option<&str>, -) -> CargoResult<()> { - let source_ids = get_source_id(config, None, reg)?; - let reg_cfg = auth::registry_credential_config(config, &source_ids.original)?; - - let login_url = match registry(config, token.clone(), None, reg, false, None) { - Ok((registry, _)) => Some(format!("{}/me", registry.host())), - Err(e) if e.is::() => e - .downcast::() - .unwrap() - .login_url - .map(|u| u.to_string()), - Err(e) => return Err(e), - }; - let new_token; - if generate_keypair || secret_key_required || key_subject.is_some() { - if !config.cli_unstable().registry_auth { - let flag = if generate_keypair { - "generate-keypair" - } else if secret_key_required { - "secret-key" - } else if key_subject.is_some() { - "key-subject" - } else { - unreachable!("how did we get here"); - }; - bail!( - "the `{flag}` flag is unstable, pass `-Z registry-auth` to enable it\n\ - See https://github.com/rust-lang/cargo/issues/10519 for more \ - information about the `{flag}` flag." - ); - } - assert!(token.is_none()); - // we are dealing with asymmetric tokens - let (old_secret_key, old_key_subject) = match ®_cfg { - RegistryCredentialConfig::AsymmetricKey((old_secret_key, old_key_subject)) => { - (Some(old_secret_key), old_key_subject.clone()) - } - _ => (None, None), - }; - let secret_key: Secret; - if generate_keypair { - assert!(!secret_key_required); - let kp = AsymmetricKeyPair::::generate().unwrap(); - secret_key = Secret::default().map(|mut key| { - FormatAsPaserk::fmt(&kp.secret, &mut key).unwrap(); - key - }); - } else if secret_key_required { - assert!(!generate_keypair); - drop_println!(config, "please paste the API secret key below"); - secret_key = Secret::default() - .map(|mut line| { - let input = io::stdin(); - input - .lock() - .read_line(&mut line) - .with_context(|| "failed to read stdin") - .map(|_| line.trim().to_string()) - }) - .transpose()?; - } else { - secret_key = old_secret_key - .cloned() - .ok_or_else(|| anyhow!("need a secret_key to set a key_subject"))?; - } - if let Some(p) = paserk_public_from_paserk_secret(secret_key.as_deref()) { - drop_println!(config, "{}", &p); - } else { - bail!("not a validly formatted PASERK secret key"); - } - new_token = RegistryCredentialConfig::AsymmetricKey(( - secret_key, - match key_subject { - Some(key_subject) => Some(key_subject.to_string()), - None => old_key_subject, - }, - )); - } else { - new_token = RegistryCredentialConfig::Token(match token { - Some(token) => token.owned(), - None => { - if let Some(login_url) = login_url { - drop_println!( - config, - "please paste the token found on {} below", - login_url - ) - } else { - drop_println!( - config, - "please paste the token for {} below", - source_ids.original.display_registry_name() - ) - } - - let mut line = String::new(); - let input = io::stdin(); - input - .lock() - .read_line(&mut line) - .with_context(|| "failed to read stdin")?; - // Automatically remove `cargo login` from an inputted token to - // allow direct pastes from `registry.host()`/me. - Secret::from(line.replace("cargo login", "").trim().to_string()) - } - }); - - if let Some(tok) = new_token.as_token() { - crates_io::check_token(tok.as_ref().expose())?; - } - } - if ®_cfg == &new_token { - config.shell().status("Login", "already logged in")?; - return Ok(()); - } - - auth::login(config, &source_ids.original, new_token)?; - - config.shell().status( - "Login", - format!("token for `{}` saved", reg.unwrap_or(CRATES_IO_DOMAIN)), - )?; - Ok(()) -} - pub fn registry_logout(config: &Config, reg: Option<&str>) -> CargoResult<()> { let source_ids = get_source_id(config, None, reg)?; let reg_cfg = auth::registry_credential_config(config, &source_ids.original)?; From 7c0add0ec02966fe517b65c7c4ade5dda1da45c4 Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Tue, 20 Jun 2023 15:13:02 +0100 Subject: [PATCH 07/11] refactor: extract logout to its own module --- src/cargo/ops/registry/logout.rs | 42 ++++++++++++++++++++++++++++++++ src/cargo/ops/registry/mod.rs | 38 ++--------------------------- 2 files changed, 44 insertions(+), 36 deletions(-) create mode 100644 src/cargo/ops/registry/logout.rs diff --git a/src/cargo/ops/registry/logout.rs b/src/cargo/ops/registry/logout.rs new file mode 100644 index 00000000000..59f2d9261d9 --- /dev/null +++ b/src/cargo/ops/registry/logout.rs @@ -0,0 +1,42 @@ +//! Interacts with the registry logout. +//! +//! There is no web API for logout at this moment. Instead, it's just an +//! operation for `cargo logout`. + +use crate::util::auth; +use crate::CargoResult; +use crate::Config; + +use super::get_source_id; + +pub fn registry_logout(config: &Config, reg: Option<&str>) -> CargoResult<()> { + let source_ids = get_source_id(config, None, reg)?; + let reg_cfg = auth::registry_credential_config(config, &source_ids.original)?; + let reg_name = source_ids.original.display_registry_name(); + if reg_cfg.is_none() { + config + .shell() + .status("Logout", format!("not currently logged in to `{reg_name}`"))?; + return Ok(()); + } + auth::logout(config, &source_ids.original)?; + config.shell().status( + "Logout", + format!("token for `{reg_name}` has been removed from local storage"), + )?; + let location = if source_ids.original.is_crates_io() { + "".to_string() + } else { + // The URL for the source requires network access to load the config. + // That could be a fairly heavy operation to perform just to provide a + // help message, so for now this just provides some generic text. + // Perhaps in the future this could have an API to fetch the config if + // it is cached, but avoid network access otherwise? + format!("the `{reg_name}` website") + }; + config.shell().note(format!( + "This does not revoke the token on the registry server.\n \ + If you need to revoke the token, visit {location} and follow the instructions there." + ))?; + Ok(()) +} diff --git a/src/cargo/ops/registry/mod.rs b/src/cargo/ops/registry/mod.rs index 3f1d666d8be..2dca14ceb7e 100644 --- a/src/cargo/ops/registry/mod.rs +++ b/src/cargo/ops/registry/mod.rs @@ -3,6 +3,7 @@ //! [1]: https://doc.rust-lang.org/nightly/cargo/reference/registry-web-api.html mod login; +mod logout; mod publish; mod search; @@ -29,6 +30,7 @@ use crate::util::IntoUrl; use crate::{drop_print, drop_println, version}; pub use self::login::registry_login; +pub use self::logout::registry_logout; pub use self::publish::publish; pub use self::publish::PublishOpts; pub use self::search::search; @@ -350,42 +352,6 @@ impl HttpTimeout { } } -pub fn registry_logout(config: &Config, reg: Option<&str>) -> CargoResult<()> { - let source_ids = get_source_id(config, None, reg)?; - let reg_cfg = auth::registry_credential_config(config, &source_ids.original)?; - let reg_name = source_ids.original.display_registry_name(); - if reg_cfg.is_none() { - config.shell().status( - "Logout", - format!("not currently logged in to `{}`", reg_name), - )?; - return Ok(()); - } - auth::logout(config, &source_ids.original)?; - config.shell().status( - "Logout", - format!( - "token for `{}` has been removed from local storage", - reg_name - ), - )?; - let location = if source_ids.original.is_crates_io() { - "".to_string() - } else { - // The URL for the source requires network access to load the config. - // That could be a fairly heavy operation to perform just to provide a - // help message, so for now this just provides some generic text. - // Perhaps in the future this could have an API to fetch the config if - // it is cached, but avoid network access otherwise? - format!("the `{reg_name}` website") - }; - config.shell().note(format!( - "This does not revoke the token on the registry server.\n \ - If you need to revoke the token, visit {location} and follow the instructions there." - ))?; - Ok(()) -} - pub struct OwnersOptions { pub krate: Option, pub token: Option>, From d263ca6e619c523029486bb789ffaa1c99ddfb16 Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Tue, 20 Jun 2023 15:22:49 +0100 Subject: [PATCH 08/11] refactor: extract yank to its own module --- src/cargo/ops/registry/mod.rs | 64 +--------------------------- src/cargo/ops/registry/yank.rs | 76 ++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 62 deletions(-) create mode 100644 src/cargo/ops/registry/yank.rs diff --git a/src/cargo/ops/registry/mod.rs b/src/cargo/ops/registry/mod.rs index 2dca14ceb7e..453eedbebf0 100644 --- a/src/cargo/ops/registry/mod.rs +++ b/src/cargo/ops/registry/mod.rs @@ -6,6 +6,7 @@ mod login; mod logout; mod publish; mod search; +mod yank; use std::collections::HashSet; use std::path::PathBuf; @@ -34,6 +35,7 @@ pub use self::logout::registry_logout; pub use self::publish::publish; pub use self::publish::PublishOpts; pub use self::search::search; +pub use self::yank::yank; /// Registry settings loaded from config files. /// @@ -431,68 +433,6 @@ pub fn modify_owners(config: &Config, opts: &OwnersOptions) -> CargoResult<()> { Ok(()) } -pub fn yank( - config: &Config, - krate: Option, - version: Option, - token: Option>, - index: Option, - undo: bool, - reg: Option, -) -> CargoResult<()> { - let name = match krate { - Some(name) => name, - None => { - let manifest_path = find_root_manifest_for_wd(config.cwd())?; - let ws = Workspace::new(&manifest_path, config)?; - ws.current()?.package_id().name().to_string() - } - }; - let version = match version { - Some(v) => v, - None => bail!("a version must be specified to yank"), - }; - - let message = if undo { - auth::Mutation::Unyank { - name: &name, - vers: &version, - } - } else { - auth::Mutation::Yank { - name: &name, - vers: &version, - } - }; - - let (mut registry, _) = registry( - config, - token.as_ref().map(Secret::as_deref), - index.as_deref(), - reg.as_deref(), - true, - Some(message), - )?; - - let package_spec = format!("{}@{}", name, version); - if undo { - config.shell().status("Unyank", package_spec)?; - registry.unyank(&name, &version).with_context(|| { - format!( - "failed to undo a yank from the registry at {}", - registry.host() - ) - })?; - } else { - config.shell().status("Yank", package_spec)?; - registry - .yank(&name, &version) - .with_context(|| format!("failed to yank from the registry at {}", registry.host()))?; - } - - Ok(()) -} - /// Gets the SourceId for an index or registry setting. /// /// The `index` and `reg` values are from the command-line or config settings. diff --git a/src/cargo/ops/registry/yank.rs b/src/cargo/ops/registry/yank.rs new file mode 100644 index 00000000000..7f087570a49 --- /dev/null +++ b/src/cargo/ops/registry/yank.rs @@ -0,0 +1,76 @@ +//! Interacts with the registry [yank] and [unyank] API. +//! +//! [yank]: https://doc.rust-lang.org/nightly/cargo/reference/registry-web-api.html#yank +//! [unyank]: https://doc.rust-lang.org/nightly/cargo/reference/registry-web-api.html#unyank + +use anyhow::bail; +use anyhow::Context as _; + +use crate::core::Workspace; +use crate::util::auth; +use crate::util::auth::Secret; +use crate::util::config::Config; +use crate::util::errors::CargoResult; +use crate::util::important_paths::find_root_manifest_for_wd; + +pub fn yank( + config: &Config, + krate: Option, + version: Option, + token: Option>, + index: Option, + undo: bool, + reg: Option, +) -> CargoResult<()> { + let name = match krate { + Some(name) => name, + None => { + let manifest_path = find_root_manifest_for_wd(config.cwd())?; + let ws = Workspace::new(&manifest_path, config)?; + ws.current()?.package_id().name().to_string() + } + }; + let version = match version { + Some(v) => v, + None => bail!("a version must be specified to yank"), + }; + + let message = if undo { + auth::Mutation::Unyank { + name: &name, + vers: &version, + } + } else { + auth::Mutation::Yank { + name: &name, + vers: &version, + } + }; + + let (mut registry, _) = super::registry( + config, + token.as_ref().map(Secret::as_deref), + index.as_deref(), + reg.as_deref(), + true, + Some(message), + )?; + + let package_spec = format!("{}@{}", name, version); + if undo { + config.shell().status("Unyank", package_spec)?; + registry.unyank(&name, &version).with_context(|| { + format!( + "failed to undo a yank from the registry at {}", + registry.host() + ) + })?; + } else { + config.shell().status("Yank", package_spec)?; + registry + .yank(&name, &version) + .with_context(|| format!("failed to yank from the registry at {}", registry.host()))?; + } + + Ok(()) +} From 677cd5ece2008521d1be3afbb3bc6648d9f1cac1 Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Tue, 20 Jun 2023 15:48:22 +0100 Subject: [PATCH 09/11] refactor: extract owner to its own module --- src/cargo/ops/registry/mod.rs | 87 ++---------------------------- src/cargo/ops/registry/owner.rs | 93 +++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 82 deletions(-) create mode 100644 src/cargo/ops/registry/owner.rs diff --git a/src/cargo/ops/registry/mod.rs b/src/cargo/ops/registry/mod.rs index 453eedbebf0..80b432088bf 100644 --- a/src/cargo/ops/registry/mod.rs +++ b/src/cargo/ops/registry/mod.rs @@ -4,6 +4,7 @@ mod login; mod logout; +mod owner; mod publish; mod search; mod yank; @@ -20,18 +21,19 @@ use curl::easy::{Easy, InfoType, SslOpt, SslVersion}; use log::{log, Level}; use crate::core::source::Source; -use crate::core::{SourceId, Workspace}; +use crate::core::SourceId; use crate::sources::{RegistrySource, SourceConfigMap}; use crate::util::auth::{self, Secret}; use crate::util::config::{Config, SslVersionConfig, SslVersionConfigRange}; use crate::util::errors::CargoResult; -use crate::util::important_paths::find_root_manifest_for_wd; use crate::util::network; use crate::util::IntoUrl; -use crate::{drop_print, drop_println, version}; +use crate::version; pub use self::login::registry_login; pub use self::logout::registry_logout; +pub use self::owner::modify_owners; +pub use self::owner::OwnersOptions; pub use self::publish::publish; pub use self::publish::PublishOpts; pub use self::search::search; @@ -354,85 +356,6 @@ impl HttpTimeout { } } -pub struct OwnersOptions { - pub krate: Option, - pub token: Option>, - pub index: Option, - pub to_add: Option>, - pub to_remove: Option>, - pub list: bool, - pub registry: Option, -} - -pub fn modify_owners(config: &Config, opts: &OwnersOptions) -> CargoResult<()> { - let name = match opts.krate { - Some(ref name) => name.clone(), - None => { - let manifest_path = find_root_manifest_for_wd(config.cwd())?; - let ws = Workspace::new(&manifest_path, config)?; - ws.current()?.package_id().name().to_string() - } - }; - - let mutation = auth::Mutation::Owners { name: &name }; - - let (mut registry, _) = registry( - config, - opts.token.as_ref().map(Secret::as_deref), - opts.index.as_deref(), - opts.registry.as_deref(), - true, - Some(mutation), - )?; - - if let Some(ref v) = opts.to_add { - let v = v.iter().map(|s| &s[..]).collect::>(); - let msg = registry.add_owners(&name, &v).with_context(|| { - format!( - "failed to invite owners to crate `{}` on registry at {}", - name, - registry.host() - ) - })?; - - config.shell().status("Owner", msg)?; - } - - if let Some(ref v) = opts.to_remove { - let v = v.iter().map(|s| &s[..]).collect::>(); - config - .shell() - .status("Owner", format!("removing {:?} from crate {}", v, name))?; - registry.remove_owners(&name, &v).with_context(|| { - format!( - "failed to remove owners from crate `{}` on registry at {}", - name, - registry.host() - ) - })?; - } - - if opts.list { - let owners = registry.list_owners(&name).with_context(|| { - format!( - "failed to list owners of crate `{}` on registry at {}", - name, - registry.host() - ) - })?; - for owner in owners.iter() { - drop_print!(config, "{}", owner.login); - match (owner.name.as_ref(), owner.email.as_ref()) { - (Some(name), Some(email)) => drop_println!(config, " ({} <{}>)", name, email), - (Some(s), None) | (None, Some(s)) => drop_println!(config, " ({})", s), - (None, None) => drop_println!(config), - } - } - } - - Ok(()) -} - /// Gets the SourceId for an index or registry setting. /// /// The `index` and `reg` values are from the command-line or config settings. diff --git a/src/cargo/ops/registry/owner.rs b/src/cargo/ops/registry/owner.rs new file mode 100644 index 00000000000..e53e07cb820 --- /dev/null +++ b/src/cargo/ops/registry/owner.rs @@ -0,0 +1,93 @@ +//! Interacts with the registry [owners API][1]. +//! +//! [1]: https://doc.rust-lang.org/nightly/cargo/reference/registry-web-api.html#owners + +use anyhow::Context as _; + +use crate::core::Workspace; +use crate::drop_print; +use crate::drop_println; +use crate::util::auth; +use crate::util::auth::Secret; +use crate::util::important_paths::find_root_manifest_for_wd; +use crate::CargoResult; +use crate::Config; + +pub struct OwnersOptions { + pub krate: Option, + pub token: Option>, + pub index: Option, + pub to_add: Option>, + pub to_remove: Option>, + pub list: bool, + pub registry: Option, +} + +pub fn modify_owners(config: &Config, opts: &OwnersOptions) -> CargoResult<()> { + let name = match opts.krate { + Some(ref name) => name.clone(), + None => { + let manifest_path = find_root_manifest_for_wd(config.cwd())?; + let ws = Workspace::new(&manifest_path, config)?; + ws.current()?.package_id().name().to_string() + } + }; + + let mutation = auth::Mutation::Owners { name: &name }; + + let (mut registry, _) = super::registry( + config, + opts.token.as_ref().map(Secret::as_deref), + opts.index.as_deref(), + opts.registry.as_deref(), + true, + Some(mutation), + )?; + + if let Some(ref v) = opts.to_add { + let v = v.iter().map(|s| &s[..]).collect::>(); + let msg = registry.add_owners(&name, &v).with_context(|| { + format!( + "failed to invite owners to crate `{}` on registry at {}", + name, + registry.host() + ) + })?; + + config.shell().status("Owner", msg)?; + } + + if let Some(ref v) = opts.to_remove { + let v = v.iter().map(|s| &s[..]).collect::>(); + config + .shell() + .status("Owner", format!("removing {:?} from crate {}", v, name))?; + registry.remove_owners(&name, &v).with_context(|| { + format!( + "failed to remove owners from crate `{}` on registry at {}", + name, + registry.host() + ) + })?; + } + + if opts.list { + let owners = registry.list_owners(&name).with_context(|| { + format!( + "failed to list owners of crate `{}` on registry at {}", + name, + registry.host() + ) + })?; + for owner in owners.iter() { + drop_print!(config, "{}", owner.login); + match (owner.name.as_ref(), owner.email.as_ref()) { + (Some(name), Some(email)) => drop_println!(config, " ({} <{}>)", name, email), + (Some(s), None) | (None, Some(s)) => drop_println!(config, " ({})", s), + (None, None) => drop_println!(config), + } + } + } + + Ok(()) +} From b465ed9f4b11c06b4eba87b0b4d445c93e65a27f Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Tue, 20 Jun 2023 16:37:40 +0100 Subject: [PATCH 10/11] refactor: move http handle constructor to its own module --- src/bin/cargo/main.rs | 6 +- src/cargo/core/package.rs | 9 +- src/cargo/ops/mod.rs | 14 +- src/cargo/ops/registry/mod.rs | 206 +-------------------- src/cargo/sources/git/oxide.rs | 2 +- src/cargo/sources/registry/http_remote.rs | 4 +- src/cargo/util/config/mod.rs | 8 +- src/cargo/util/network/http.rs | 216 ++++++++++++++++++++++ src/cargo/util/network/mod.rs | 1 + 9 files changed, 245 insertions(+), 221 deletions(-) create mode 100644 src/cargo/util/network/http.rs diff --git a/src/bin/cargo/main.rs b/src/bin/cargo/main.rs index 9fb6635ea77..462332fb7ed 100644 --- a/src/bin/cargo/main.rs +++ b/src/bin/cargo/main.rs @@ -2,6 +2,8 @@ #![allow(clippy::all)] #![warn(clippy::disallowed_methods)] +use cargo::util::network::http::http_handle; +use cargo::util::network::http::needs_custom_http_transport; use cargo::util::toml::StringOrVec; use cargo::util::CliError; use cargo::util::{self, closest_msg, command_prelude, CargoResult, CliResult, Config}; @@ -293,12 +295,12 @@ fn init_git(config: &Config) { /// configured to use libcurl instead of the built-in networking support so /// that those configuration settings can be used. fn init_git_transports(config: &Config) { - match cargo::ops::needs_custom_http_transport(config) { + match needs_custom_http_transport(config) { Ok(true) => {} _ => return, } - let handle = match cargo::ops::http_handle(config) { + let handle = match http_handle(config) { Ok(handle) => handle, Err(..) => return, }; diff --git a/src/cargo/core/package.rs b/src/cargo/core/package.rs index 44e2ccbfd39..f4ab448d28d 100644 --- a/src/cargo/core/package.rs +++ b/src/cargo/core/package.rs @@ -24,10 +24,11 @@ use crate::core::resolver::{HasDevUnits, Resolve}; use crate::core::source::MaybePackage; use crate::core::{Dependency, Manifest, PackageId, SourceId, Target}; use crate::core::{SourceMap, Summary, Workspace}; -use crate::ops; use crate::util::config::PackageCacheLock; use crate::util::errors::{CargoResult, HttpNotSuccessful, DEBUG_HEADERS}; use crate::util::interning::InternedString; +use crate::util::network::http::http_handle_and_timeout; +use crate::util::network::http::HttpTimeout; use crate::util::network::retry::{Retry, RetryResult}; use crate::util::network::sleep::SleepTracker; use crate::util::{self, internal, Config, Progress, ProgressStyle}; @@ -348,7 +349,7 @@ pub struct Downloads<'a, 'cfg> { /// Note that timeout management is done manually here instead of in libcurl /// because we want to apply timeouts to an entire batch of operations, not /// any one particular single operation. - timeout: ops::HttpTimeout, + timeout: HttpTimeout, /// Last time bytes were received. updated_at: Cell, /// This is a slow-speed check. It is reset to `now + timeout_duration` @@ -441,7 +442,7 @@ impl<'cfg> PackageSet<'cfg> { pub fn enable_download<'a>(&'a self) -> CargoResult> { assert!(!self.downloading.replace(true)); - let timeout = ops::HttpTimeout::new(self.config)?; + let timeout = HttpTimeout::new(self.config)?; Ok(Downloads { start: Instant::now(), set: self, @@ -713,7 +714,7 @@ impl<'a, 'cfg> Downloads<'a, 'cfg> { debug!("downloading {} as {}", id, token); assert!(self.pending_ids.insert(id)); - let (mut handle, _timeout) = ops::http_handle_and_timeout(self.set.config)?; + let (mut handle, _timeout) = http_handle_and_timeout(self.set.config)?; handle.get(true)?; handle.url(&url)?; handle.follow_location(true)?; // follow redirects diff --git a/src/cargo/ops/mod.rs b/src/cargo/ops/mod.rs index 4b6aea991bd..d4ec442dd91 100644 --- a/src/cargo/ops/mod.rs +++ b/src/cargo/ops/mod.rs @@ -21,11 +21,15 @@ pub use self::cargo_test::{run_benches, run_tests, TestOptions}; pub use self::cargo_uninstall::uninstall; pub use self::fix::{fix, fix_exec_rustc, fix_get_proxy_lock_addr, FixOptions}; pub use self::lockfile::{load_pkg_lockfile, resolve_to_string, write_pkg_lockfile}; -pub use self::registry::HttpTimeout; -pub use self::registry::{configure_http_handle, http_handle, http_handle_and_timeout}; -pub use self::registry::{modify_owners, yank, OwnersOptions, PublishOpts}; -pub use self::registry::{needs_custom_http_transport, registry_login, registry_logout, search}; -pub use self::registry::{publish, RegistryCredentialConfig}; +pub use self::registry::modify_owners; +pub use self::registry::publish; +pub use self::registry::registry_login; +pub use self::registry::registry_logout; +pub use self::registry::search; +pub use self::registry::yank; +pub use self::registry::OwnersOptions; +pub use self::registry::PublishOpts; +pub use self::registry::RegistryCredentialConfig; pub use self::resolve::{ add_overrides, get_resolved_packages, resolve_with_previous, resolve_ws, resolve_ws_with_opts, WorkspaceResolve, diff --git a/src/cargo/ops/registry/mod.rs b/src/cargo/ops/registry/mod.rs index 80b432088bf..ecb610ddda1 100644 --- a/src/cargo/ops/registry/mod.rs +++ b/src/cargo/ops/registry/mod.rs @@ -13,22 +13,18 @@ use std::collections::HashSet; use std::path::PathBuf; use std::str; use std::task::Poll; -use std::time::Duration; use anyhow::{bail, format_err, Context as _}; use crates_io::{self, Registry}; -use curl::easy::{Easy, InfoType, SslOpt, SslVersion}; -use log::{log, Level}; use crate::core::source::Source; use crate::core::SourceId; use crate::sources::{RegistrySource, SourceConfigMap}; use crate::util::auth::{self, Secret}; -use crate::util::config::{Config, SslVersionConfig, SslVersionConfigRange}; +use crate::util::config::Config; use crate::util::errors::CargoResult; -use crate::util::network; +use crate::util::network::http::http_handle; use crate::util::IntoUrl; -use crate::version; pub use self::login::registry_login; pub use self::logout::registry_logout; @@ -158,204 +154,6 @@ fn registry( )) } -/// Creates a new HTTP handle with appropriate global configuration for cargo. -pub fn http_handle(config: &Config) -> CargoResult { - let (mut handle, timeout) = http_handle_and_timeout(config)?; - timeout.configure(&mut handle)?; - Ok(handle) -} - -pub fn http_handle_and_timeout(config: &Config) -> CargoResult<(Easy, HttpTimeout)> { - if config.frozen() { - bail!( - "attempting to make an HTTP request, but --frozen was \ - specified" - ) - } - if config.offline() { - bail!( - "attempting to make an HTTP request, but --offline was \ - specified" - ) - } - - // The timeout option for libcurl by default times out the entire transfer, - // but we probably don't want this. Instead we only set timeouts for the - // connect phase as well as a "low speed" timeout so if we don't receive - // many bytes in a large-ish period of time then we time out. - let mut handle = Easy::new(); - let timeout = configure_http_handle(config, &mut handle)?; - Ok((handle, timeout)) -} - -// Only use a custom transport if any HTTP options are specified, -// such as proxies or custom certificate authorities. -// -// The custom transport, however, is not as well battle-tested. -pub fn needs_custom_http_transport(config: &Config) -> CargoResult { - Ok( - network::proxy::http_proxy_exists(config.http_config()?, config) - || *config.http_config()? != Default::default() - || config.get_env_os("HTTP_TIMEOUT").is_some(), - ) -} - -/// Configure a libcurl http handle with the defaults options for Cargo -pub fn configure_http_handle(config: &Config, handle: &mut Easy) -> CargoResult { - let http = config.http_config()?; - if let Some(proxy) = network::proxy::http_proxy(http) { - handle.proxy(&proxy)?; - } - if let Some(cainfo) = &http.cainfo { - let cainfo = cainfo.resolve_path(config); - handle.cainfo(&cainfo)?; - } - if let Some(check) = http.check_revoke { - handle.ssl_options(SslOpt::new().no_revoke(!check))?; - } - - if let Some(user_agent) = &http.user_agent { - handle.useragent(user_agent)?; - } else { - handle.useragent(&format!("cargo {}", version()))?; - } - - fn to_ssl_version(s: &str) -> CargoResult { - let version = match s { - "default" => SslVersion::Default, - "tlsv1" => SslVersion::Tlsv1, - "tlsv1.0" => SslVersion::Tlsv10, - "tlsv1.1" => SslVersion::Tlsv11, - "tlsv1.2" => SslVersion::Tlsv12, - "tlsv1.3" => SslVersion::Tlsv13, - _ => bail!( - "Invalid ssl version `{s}`,\ - choose from 'default', 'tlsv1', 'tlsv1.0', 'tlsv1.1', 'tlsv1.2', 'tlsv1.3'." - ), - }; - Ok(version) - } - - // Empty string accept encoding expands to the encodings supported by the current libcurl. - handle.accept_encoding("")?; - if let Some(ssl_version) = &http.ssl_version { - match ssl_version { - SslVersionConfig::Single(s) => { - let version = to_ssl_version(s.as_str())?; - handle.ssl_version(version)?; - } - SslVersionConfig::Range(SslVersionConfigRange { min, max }) => { - let min_version = min - .as_ref() - .map_or(Ok(SslVersion::Default), |s| to_ssl_version(s))?; - let max_version = max - .as_ref() - .map_or(Ok(SslVersion::Default), |s| to_ssl_version(s))?; - handle.ssl_min_max_version(min_version, max_version)?; - } - } - } else if cfg!(windows) { - // This is a temporary workaround for some bugs with libcurl and - // schannel and TLS 1.3. - // - // Our libcurl on Windows is usually built with schannel. - // On Windows 11 (or Windows Server 2022), libcurl recently (late - // 2022) gained support for TLS 1.3 with schannel, and it now defaults - // to 1.3. Unfortunately there have been some bugs with this. - // https://github.com/curl/curl/issues/9431 is the most recent. Once - // that has been fixed, and some time has passed where we can be more - // confident that the 1.3 support won't cause issues, this can be - // removed. - // - // Windows 10 is unaffected. libcurl does not support TLS 1.3 on - // Windows 10. (Windows 10 sorta had support, but it required enabling - // an advanced option in the registry which was buggy, and libcurl - // does runtime checks to prevent it.) - handle.ssl_min_max_version(SslVersion::Default, SslVersion::Tlsv12)?; - } - - if let Some(true) = http.debug { - handle.verbose(true)?; - log::debug!("{:#?}", curl::Version::get()); - handle.debug_function(|kind, data| { - let (prefix, level) = match kind { - InfoType::Text => ("*", Level::Debug), - InfoType::HeaderIn => ("<", Level::Debug), - InfoType::HeaderOut => (">", Level::Debug), - InfoType::DataIn => ("{", Level::Trace), - InfoType::DataOut => ("}", Level::Trace), - InfoType::SslDataIn | InfoType::SslDataOut => return, - _ => return, - }; - let starts_with_ignore_case = |line: &str, text: &str| -> bool { - line[..line.len().min(text.len())].eq_ignore_ascii_case(text) - }; - match str::from_utf8(data) { - Ok(s) => { - for mut line in s.lines() { - if starts_with_ignore_case(line, "authorization:") { - line = "Authorization: [REDACTED]"; - } else if starts_with_ignore_case(line, "h2h3 [authorization:") { - line = "h2h3 [Authorization: [REDACTED]]"; - } else if starts_with_ignore_case(line, "set-cookie") { - line = "set-cookie: [REDACTED]"; - } - log!(level, "http-debug: {} {}", prefix, line); - } - } - Err(_) => { - log!( - level, - "http-debug: {} ({} bytes of data)", - prefix, - data.len() - ); - } - } - })?; - } - - HttpTimeout::new(config) -} - -#[must_use] -pub struct HttpTimeout { - pub dur: Duration, - pub low_speed_limit: u32, -} - -impl HttpTimeout { - pub fn new(config: &Config) -> CargoResult { - let http_config = config.http_config()?; - let low_speed_limit = http_config.low_speed_limit.unwrap_or(10); - let seconds = http_config - .timeout - .or_else(|| { - config - .get_env("HTTP_TIMEOUT") - .ok() - .and_then(|s| s.parse().ok()) - }) - .unwrap_or(30); - Ok(HttpTimeout { - dur: Duration::new(seconds, 0), - low_speed_limit, - }) - } - - pub fn configure(&self, handle: &mut Easy) -> CargoResult<()> { - // The timeout option for libcurl by default times out the entire - // transfer, but we probably don't want this. Instead we only set - // timeouts for the connect phase as well as a "low speed" timeout so - // if we don't receive many bytes in a large-ish period of time then we - // time out. - handle.connect_timeout(self.dur)?; - handle.low_speed_time(self.dur)?; - handle.low_speed_limit(self.low_speed_limit)?; - Ok(()) - } -} - /// Gets the SourceId for an index or registry setting. /// /// The `index` and `reg` values are from the command-line or config settings. diff --git a/src/cargo/sources/git/oxide.rs b/src/cargo/sources/git/oxide.rs index 7f5298ef846..e86c63e8ee9 100644 --- a/src/cargo/sources/git/oxide.rs +++ b/src/cargo/sources/git/oxide.rs @@ -1,7 +1,7 @@ //! This module contains all code sporting `gitoxide` for operations on `git` repositories and it mirrors //! `utils` closely for now. One day it can be renamed into `utils` once `git2` isn't required anymore. -use crate::ops::HttpTimeout; +use crate::util::network::http::HttpTimeout; use crate::util::{human_readable_bytes, network, MetricsCounter, Progress}; use crate::{CargoResult, Config}; use cargo_util::paths; diff --git a/src/cargo/sources/registry/http_remote.rs b/src/cargo/sources/registry/http_remote.rs index 96e8dd2d7b9..c69ef8f9bed 100644 --- a/src/cargo/sources/registry/http_remote.rs +++ b/src/cargo/sources/registry/http_remote.rs @@ -1,11 +1,11 @@ //! Access to a HTTP-based crate registry. See [`HttpRegistry`] for details. use crate::core::{PackageId, SourceId}; -use crate::ops; use crate::sources::registry::download; use crate::sources::registry::MaybeLock; use crate::sources::registry::{LoadResponse, RegistryConfig, RegistryData}; use crate::util::errors::{CargoResult, HttpNotSuccessful, DEBUG_HEADERS}; +use crate::util::network::http::http_handle; use crate::util::network::retry::{Retry, RetryResult}; use crate::util::network::sleep::SleepTracker; use crate::util::{auth, Config, Filesystem, IntoUrl, Progress, ProgressStyle}; @@ -610,7 +610,7 @@ impl<'cfg> RegistryData for HttpRegistry<'cfg> { // Looks like we're going to have to do a network request. self.start_fetch()?; - let mut handle = ops::http_handle(self.config)?; + let mut handle = http_handle(self.config)?; let full_url = self.full_url(path); debug!("fetch {}", full_url); handle.get(true)?; diff --git a/src/cargo/util/config/mod.rs b/src/cargo/util/config/mod.rs index dfd0bb19879..68f6f2139eb 100644 --- a/src/cargo/util/config/mod.rs +++ b/src/cargo/util/config/mod.rs @@ -69,9 +69,11 @@ use self::ConfigValue as CV; use crate::core::compiler::rustdoc::RustdocExternMap; use crate::core::shell::Verbosity; use crate::core::{features, CliUnstable, Shell, SourceId, Workspace, WorkspaceRootConfig}; -use crate::ops::{self, RegistryCredentialConfig}; +use crate::ops::RegistryCredentialConfig; use crate::util::auth::Secret; use crate::util::errors::CargoResult; +use crate::util::network::http::configure_http_handle; +use crate::util::network::http::http_handle; use crate::util::CanonicalUrl; use crate::util::{internal, toml as cargo_toml}; use crate::util::{try_canonicalize, validate_package_name}; @@ -1706,11 +1708,11 @@ impl Config { pub fn http(&self) -> CargoResult<&RefCell> { let http = self .easy - .try_borrow_with(|| ops::http_handle(self).map(RefCell::new))?; + .try_borrow_with(|| http_handle(self).map(RefCell::new))?; { let mut http = http.borrow_mut(); http.reset(); - let timeout = ops::configure_http_handle(self, &mut http)?; + let timeout = configure_http_handle(self, &mut http)?; timeout.configure(&mut http)?; } Ok(http) diff --git a/src/cargo/util/network/http.rs b/src/cargo/util/network/http.rs new file mode 100644 index 00000000000..f077ce2b640 --- /dev/null +++ b/src/cargo/util/network/http.rs @@ -0,0 +1,216 @@ +//! Configures libcurl's http handles. + +use std::str; +use std::time::Duration; + +use anyhow::bail; +use curl::easy::Easy; +use curl::easy::InfoType; +use curl::easy::SslOpt; +use curl::easy::SslVersion; +use log::log; +use log::Level; + +use crate::util::config::SslVersionConfig; +use crate::util::config::SslVersionConfigRange; +use crate::version; +use crate::CargoResult; +use crate::Config; + +/// Creates a new HTTP handle with appropriate global configuration for cargo. +pub fn http_handle(config: &Config) -> CargoResult { + let (mut handle, timeout) = http_handle_and_timeout(config)?; + timeout.configure(&mut handle)?; + Ok(handle) +} + +pub fn http_handle_and_timeout(config: &Config) -> CargoResult<(Easy, HttpTimeout)> { + if config.frozen() { + bail!( + "attempting to make an HTTP request, but --frozen was \ + specified" + ) + } + if config.offline() { + bail!( + "attempting to make an HTTP request, but --offline was \ + specified" + ) + } + + // The timeout option for libcurl by default times out the entire transfer, + // but we probably don't want this. Instead we only set timeouts for the + // connect phase as well as a "low speed" timeout so if we don't receive + // many bytes in a large-ish period of time then we time out. + let mut handle = Easy::new(); + let timeout = configure_http_handle(config, &mut handle)?; + Ok((handle, timeout)) +} + +// Only use a custom transport if any HTTP options are specified, +// such as proxies or custom certificate authorities. +// +// The custom transport, however, is not as well battle-tested. +pub fn needs_custom_http_transport(config: &Config) -> CargoResult { + Ok( + super::proxy::http_proxy_exists(config.http_config()?, config) + || *config.http_config()? != Default::default() + || config.get_env_os("HTTP_TIMEOUT").is_some(), + ) +} + +/// Configure a libcurl http handle with the defaults options for Cargo +pub fn configure_http_handle(config: &Config, handle: &mut Easy) -> CargoResult { + let http = config.http_config()?; + if let Some(proxy) = super::proxy::http_proxy(http) { + handle.proxy(&proxy)?; + } + if let Some(cainfo) = &http.cainfo { + let cainfo = cainfo.resolve_path(config); + handle.cainfo(&cainfo)?; + } + if let Some(check) = http.check_revoke { + handle.ssl_options(SslOpt::new().no_revoke(!check))?; + } + + if let Some(user_agent) = &http.user_agent { + handle.useragent(user_agent)?; + } else { + handle.useragent(&format!("cargo {}", version()))?; + } + + fn to_ssl_version(s: &str) -> CargoResult { + let version = match s { + "default" => SslVersion::Default, + "tlsv1" => SslVersion::Tlsv1, + "tlsv1.0" => SslVersion::Tlsv10, + "tlsv1.1" => SslVersion::Tlsv11, + "tlsv1.2" => SslVersion::Tlsv12, + "tlsv1.3" => SslVersion::Tlsv13, + _ => bail!( + "Invalid ssl version `{s}`,\ + choose from 'default', 'tlsv1', 'tlsv1.0', 'tlsv1.1', 'tlsv1.2', 'tlsv1.3'." + ), + }; + Ok(version) + } + + // Empty string accept encoding expands to the encodings supported by the current libcurl. + handle.accept_encoding("")?; + if let Some(ssl_version) = &http.ssl_version { + match ssl_version { + SslVersionConfig::Single(s) => { + let version = to_ssl_version(s.as_str())?; + handle.ssl_version(version)?; + } + SslVersionConfig::Range(SslVersionConfigRange { min, max }) => { + let min_version = min + .as_ref() + .map_or(Ok(SslVersion::Default), |s| to_ssl_version(s))?; + let max_version = max + .as_ref() + .map_or(Ok(SslVersion::Default), |s| to_ssl_version(s))?; + handle.ssl_min_max_version(min_version, max_version)?; + } + } + } else if cfg!(windows) { + // This is a temporary workaround for some bugs with libcurl and + // schannel and TLS 1.3. + // + // Our libcurl on Windows is usually built with schannel. + // On Windows 11 (or Windows Server 2022), libcurl recently (late + // 2022) gained support for TLS 1.3 with schannel, and it now defaults + // to 1.3. Unfortunately there have been some bugs with this. + // https://github.com/curl/curl/issues/9431 is the most recent. Once + // that has been fixed, and some time has passed where we can be more + // confident that the 1.3 support won't cause issues, this can be + // removed. + // + // Windows 10 is unaffected. libcurl does not support TLS 1.3 on + // Windows 10. (Windows 10 sorta had support, but it required enabling + // an advanced option in the registry which was buggy, and libcurl + // does runtime checks to prevent it.) + handle.ssl_min_max_version(SslVersion::Default, SslVersion::Tlsv12)?; + } + + if let Some(true) = http.debug { + handle.verbose(true)?; + log::debug!("{:#?}", curl::Version::get()); + handle.debug_function(|kind, data| { + let (prefix, level) = match kind { + InfoType::Text => ("*", Level::Debug), + InfoType::HeaderIn => ("<", Level::Debug), + InfoType::HeaderOut => (">", Level::Debug), + InfoType::DataIn => ("{", Level::Trace), + InfoType::DataOut => ("}", Level::Trace), + InfoType::SslDataIn | InfoType::SslDataOut => return, + _ => return, + }; + let starts_with_ignore_case = |line: &str, text: &str| -> bool { + line[..line.len().min(text.len())].eq_ignore_ascii_case(text) + }; + match str::from_utf8(data) { + Ok(s) => { + for mut line in s.lines() { + if starts_with_ignore_case(line, "authorization:") { + line = "Authorization: [REDACTED]"; + } else if starts_with_ignore_case(line, "h2h3 [authorization:") { + line = "h2h3 [Authorization: [REDACTED]]"; + } else if starts_with_ignore_case(line, "set-cookie") { + line = "set-cookie: [REDACTED]"; + } + log!(level, "http-debug: {} {}", prefix, line); + } + } + Err(_) => { + log!( + level, + "http-debug: {} ({} bytes of data)", + prefix, + data.len() + ); + } + } + })?; + } + + HttpTimeout::new(config) +} + +#[must_use] +pub struct HttpTimeout { + pub dur: Duration, + pub low_speed_limit: u32, +} + +impl HttpTimeout { + pub fn new(config: &Config) -> CargoResult { + let http_config = config.http_config()?; + let low_speed_limit = http_config.low_speed_limit.unwrap_or(10); + let seconds = http_config + .timeout + .or_else(|| { + config + .get_env("HTTP_TIMEOUT") + .ok() + .and_then(|s| s.parse().ok()) + }) + .unwrap_or(30); + Ok(HttpTimeout { + dur: Duration::new(seconds, 0), + low_speed_limit, + }) + } + + pub fn configure(&self, handle: &mut Easy) -> CargoResult<()> { + // The timeout option for libcurl by default times out the entire + // transfer, but we probably don't want this. Instead we only set + // timeouts for the connect phase as well as a "low speed" timeout so + // if we don't receive many bytes in a large-ish period of time then we + // time out. + handle.connect_timeout(self.dur)?; + handle.low_speed_time(self.dur)?; + handle.low_speed_limit(self.low_speed_limit)?; + Ok(()) + } +} diff --git a/src/cargo/util/network/mod.rs b/src/cargo/util/network/mod.rs index d03a32fede7..b078fa3527d 100644 --- a/src/cargo/util/network/mod.rs +++ b/src/cargo/util/network/mod.rs @@ -2,6 +2,7 @@ use std::task::Poll; +pub mod http; pub mod proxy; pub mod retry; pub mod sleep; From aaf02211c658bf43fcb40fcc5bb2839fcb4c3039 Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Tue, 20 Jun 2023 17:01:38 +0100 Subject: [PATCH 11/11] chore: update auto-label trigger files --- triagebot.toml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/triagebot.toml b/triagebot.toml index 00a98e00806..6d07f438fbd 100644 --- a/triagebot.toml +++ b/triagebot.toml @@ -137,7 +137,7 @@ trigger_files = [ ] [autolabel."A-interacts-with-crates.io"] -trigger_files = ["crates/crates-io/", "src/cargo/ops/registry.rs"] +trigger_files = ["crates/crates-io/", "src/cargo/ops/registry/"] [autolabel."A-layout"] trigger_files = [ @@ -180,7 +180,7 @@ trigger_files = ["src/cargo/core/compiler/fingerprint/"] trigger_files = ["src/cargo/sources/registry/", "src/cargo/core/registry.rs"] [autolabel."A-registry-authentication"] -trigger_files = ["src/cargo/util/auth.rs", "credential/"] +trigger_files = ["src/cargo/util/auth/", "credential/"] [autolabel."A-semver"] trigger_files = [ @@ -266,10 +266,10 @@ trigger_files = ["src/bin/cargo/commands/install.rs", "src/cargo/ops/cargo_insta trigger_files = ["src/bin/cargo/commands/locate_project.rs"] [autolabel."Command-login"] -trigger_files = ["src/bin/cargo/commands/login.rs"] +trigger_files = ["src/bin/cargo/commands/login.rs", "src/cargo/ops/registry/login.rs"] [autolabel."Command-logout"] -trigger_files = ["src/bin/cargo/commands/logout.rs"] +trigger_files = ["src/bin/cargo/commands/logout.rs", "src/cargo/ops/registry/logout.rs"] [autolabel."Command-metadata"] trigger_files = ["src/bin/cargo/commands/metadata.rs", "src/cargo/ops/cargo_output_metadata.rs"] @@ -278,7 +278,7 @@ trigger_files = ["src/bin/cargo/commands/metadata.rs", "src/cargo/ops/cargo_outp trigger_files = ["src/bin/cargo/commands/new.rs", "src/cargo/ops/cargo_new.rs"] [autolabel."Command-owner"] -trigger_files = ["src/bin/cargo/commands/owner.rs"] +trigger_files = ["src/bin/cargo/commands/owner.rs", "src/cargo/ops/registry/owner.rs"] [autolabel."Command-package"] trigger_files = ["src/bin/cargo/commands/package.rs", "src/cargo/ops/cargo_package.rs"] @@ -287,7 +287,7 @@ trigger_files = ["src/bin/cargo/commands/package.rs", "src/cargo/ops/cargo_packa trigger_files = ["src/bin/cargo/commands/pkgid.rs", "src/cargo/ops/cargo_pkgid.rs"] [autolabel."Command-publish"] -trigger_files = ["src/bin/cargo/commands/publish.rs"] +trigger_files = ["src/bin/cargo/commands/publish.rs", "src/cargo/ops/registry/publish.rs"] [autolabel."Command-read-manifest"] trigger_files = ["src/bin/cargo/commands/read_manifest.rs", "src/cargo/ops/cargo_read_manifest.rs"] @@ -308,7 +308,7 @@ trigger_files = ["src/bin/cargo/commands/rustc.rs"] trigger_files = ["src/bin/cargo/commands/rustdoc.rs"] [autolabel."Command-search"] -trigger_files = ["src/bin/cargo/commands/search.rs"] +trigger_files = ["src/bin/cargo/commands/search.rs", "src/cargo/ops/registry/search.rs"] [autolabel."Command-test"] trigger_files = ["src/bin/cargo/commands/test.rs", "src/cargo/ops/cargo_test.rs"] @@ -332,4 +332,4 @@ trigger_files = ["src/bin/cargo/commands/verify_project.rs"] trigger_files = ["src/bin/cargo/commands/version.rs"] [autolabel."Command-yank"] -trigger_files = ["src/bin/cargo/commands/yank.rs"] +trigger_files = ["src/bin/cargo/commands/yank.rs", "src/cargo/ops/registry/yank.rs"]