summaryrefslogtreecommitdiffstats
path: root/crates/cargo-test-support/src/registry.rs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--crates/cargo-test-support/src/registry.rs1581
1 files changed, 1581 insertions, 0 deletions
diff --git a/crates/cargo-test-support/src/registry.rs b/crates/cargo-test-support/src/registry.rs
new file mode 100644
index 0000000..7b1dc54
--- /dev/null
+++ b/crates/cargo-test-support/src/registry.rs
@@ -0,0 +1,1581 @@
+use crate::git::repo;
+use crate::paths;
+use crate::publish::{create_index_line, write_to_index};
+use cargo_util::paths::append;
+use cargo_util::Sha256;
+use flate2::write::GzEncoder;
+use flate2::Compression;
+use pasetors::keys::{AsymmetricPublicKey, AsymmetricSecretKey};
+use pasetors::paserk::FormatAsPaserk;
+use pasetors::token::UntrustedToken;
+use std::collections::{BTreeMap, HashMap};
+use std::fmt;
+use std::fs::{self, File};
+use std::io::{BufRead, BufReader, Read, Write};
+use std::net::{SocketAddr, TcpListener, TcpStream};
+use std::path::PathBuf;
+use std::thread::{self, JoinHandle};
+use tar::{Builder, Header};
+use time::format_description::well_known::Rfc3339;
+use time::{Duration, OffsetDateTime};
+use url::Url;
+
+/// Gets the path to the local index pretending to be crates.io. This is a Git repo
+/// initialized with a `config.json` file pointing to `dl_path` for downloads
+/// and `api_path` for uploads.
+pub fn registry_path() -> PathBuf {
+ generate_path("registry")
+}
+/// Gets the path for local web API uploads. Cargo will place the contents of a web API
+/// request here. For example, `api/v1/crates/new` is the result of publishing a crate.
+pub fn api_path() -> PathBuf {
+ generate_path("api")
+}
+/// Gets the path where crates can be downloaded using the web API endpoint. Crates
+/// should be organized as `{name}/{version}/download` to match the web API
+/// endpoint. This is rarely used and must be manually set up.
+fn dl_path() -> PathBuf {
+ generate_path("dl")
+}
+/// Gets the alternative-registry version of `registry_path`.
+fn alt_registry_path() -> PathBuf {
+ generate_path("alternative-registry")
+}
+/// Gets the alternative-registry version of `registry_url`.
+fn alt_registry_url() -> Url {
+ generate_url("alternative-registry")
+}
+/// Gets the alternative-registry version of `dl_path`.
+pub fn alt_dl_path() -> PathBuf {
+ generate_path("alternative-dl")
+}
+/// Gets the alternative-registry version of `api_path`.
+pub fn alt_api_path() -> PathBuf {
+ generate_path("alternative-api")
+}
+fn generate_path(name: &str) -> PathBuf {
+ paths::root().join(name)
+}
+fn generate_url(name: &str) -> Url {
+ Url::from_file_path(generate_path(name)).ok().unwrap()
+}
+
+#[derive(Clone)]
+pub enum Token {
+ Plaintext(String),
+ Keys(String, Option<String>),
+}
+
+impl Token {
+ /// This is a valid PASETO secret key.
+ /// This one is already publicly available as part of the text of the RFC so is safe to use for tests.
+ pub fn rfc_key() -> Token {
+ Token::Keys(
+ "k3.secret.fNYVuMvBgOlljt9TDohnaYLblghqaHoQquVZwgR6X12cBFHZLFsaU3q7X3k1Zn36"
+ .to_string(),
+ Some("sub".to_string()),
+ )
+ }
+}
+
+/// A builder for initializing registries.
+pub struct RegistryBuilder {
+ /// If set, configures an alternate registry with the given name.
+ alternative: Option<String>,
+ /// The authorization token for the registry.
+ token: Option<Token>,
+ /// If set, the registry requires authorization for all operations.
+ auth_required: bool,
+ /// If set, serves the index over http.
+ http_index: bool,
+ /// If set, serves the API over http.
+ http_api: bool,
+ /// If set, config.json includes 'api'
+ api: bool,
+ /// Write the token in the configuration.
+ configure_token: bool,
+ /// Write the registry in configuration.
+ configure_registry: bool,
+ /// API responders.
+ custom_responders: HashMap<&'static str, Box<dyn Send + Fn(&Request, &HttpServer) -> Response>>,
+}
+
+pub struct TestRegistry {
+ server: Option<HttpServerHandle>,
+ index_url: Url,
+ path: PathBuf,
+ api_url: Url,
+ dl_url: Url,
+ token: Token,
+}
+
+impl TestRegistry {
+ pub fn index_url(&self) -> &Url {
+ &self.index_url
+ }
+
+ pub fn api_url(&self) -> &Url {
+ &self.api_url
+ }
+
+ pub fn token(&self) -> &str {
+ match &self.token {
+ Token::Plaintext(s) => s,
+ Token::Keys(_, _) => panic!("registry was not configured with a plaintext token"),
+ }
+ }
+
+ pub fn key(&self) -> &str {
+ match &self.token {
+ Token::Plaintext(_) => panic!("registry was not configured with a secret key"),
+ Token::Keys(s, _) => s,
+ }
+ }
+
+ /// Shutdown the server thread and wait for it to stop.
+ /// `Drop` automatically stops the server, but this additionally
+ /// waits for the thread to stop.
+ pub fn join(self) {
+ if let Some(mut server) = self.server {
+ server.stop();
+ let handle = server.handle.take().unwrap();
+ handle.join().unwrap();
+ }
+ }
+}
+
+impl RegistryBuilder {
+ #[must_use]
+ pub fn new() -> RegistryBuilder {
+ RegistryBuilder {
+ alternative: None,
+ token: None,
+ auth_required: false,
+ http_api: false,
+ http_index: false,
+ api: true,
+ configure_registry: true,
+ configure_token: true,
+ custom_responders: HashMap::new(),
+ }
+ }
+
+ /// Adds a custom HTTP response for a specific url
+ #[must_use]
+ pub fn add_responder<R: 'static + Send + Fn(&Request, &HttpServer) -> Response>(
+ mut self,
+ url: &'static str,
+ responder: R,
+ ) -> Self {
+ self.custom_responders.insert(url, Box::new(responder));
+ self
+ }
+
+ /// Sets whether or not to initialize as an alternative registry.
+ #[must_use]
+ pub fn alternative_named(mut self, alt: &str) -> Self {
+ self.alternative = Some(alt.to_string());
+ self
+ }
+
+ /// Sets whether or not to initialize as an alternative registry.
+ #[must_use]
+ pub fn alternative(self) -> Self {
+ self.alternative_named("alternative")
+ }
+
+ /// Prevents placing a token in the configuration
+ #[must_use]
+ pub fn no_configure_token(mut self) -> Self {
+ self.configure_token = false;
+ self
+ }
+
+ /// Prevents adding the registry to the configuration.
+ #[must_use]
+ pub fn no_configure_registry(mut self) -> Self {
+ self.configure_registry = false;
+ self
+ }
+
+ /// Sets the token value
+ #[must_use]
+ pub fn token(mut self, token: Token) -> Self {
+ self.token = Some(token);
+ self
+ }
+
+ /// Sets this registry to require the authentication token for
+ /// all operations.
+ #[must_use]
+ pub fn auth_required(mut self) -> Self {
+ self.auth_required = true;
+ self
+ }
+
+ /// Operate the index over http
+ #[must_use]
+ pub fn http_index(mut self) -> Self {
+ self.http_index = true;
+ self
+ }
+
+ /// Operate the api over http
+ #[must_use]
+ pub fn http_api(mut self) -> Self {
+ self.http_api = true;
+ self
+ }
+
+ /// The registry has no api.
+ #[must_use]
+ pub fn no_api(mut self) -> Self {
+ self.api = false;
+ self
+ }
+
+ /// Initializes the registry.
+ #[must_use]
+ pub fn build(self) -> TestRegistry {
+ let config_path = paths::home().join(".cargo/config");
+ t!(fs::create_dir_all(config_path.parent().unwrap()));
+ let prefix = if let Some(alternative) = &self.alternative {
+ format!("{alternative}-")
+ } else {
+ String::new()
+ };
+ let registry_path = generate_path(&format!("{prefix}registry"));
+ let index_url = generate_url(&format!("{prefix}registry"));
+ let api_url = generate_url(&format!("{prefix}api"));
+ let dl_url = generate_url(&format!("{prefix}dl"));
+ let dl_path = generate_path(&format!("{prefix}dl"));
+ let api_path = generate_path(&format!("{prefix}api"));
+ let token = self
+ .token
+ .unwrap_or_else(|| Token::Plaintext(format!("{prefix}sekrit")));
+
+ let (server, index_url, api_url, dl_url) = if !self.http_index && !self.http_api {
+ // No need to start the HTTP server.
+ (None, index_url, api_url, dl_url)
+ } else {
+ let server = HttpServer::new(
+ registry_path.clone(),
+ dl_path,
+ api_path.clone(),
+ token.clone(),
+ self.auth_required,
+ self.custom_responders,
+ );
+ let index_url = if self.http_index {
+ server.index_url()
+ } else {
+ index_url
+ };
+ let api_url = if self.http_api {
+ server.api_url()
+ } else {
+ api_url
+ };
+ let dl_url = server.dl_url();
+ (Some(server), index_url, api_url, dl_url)
+ };
+
+ let registry = TestRegistry {
+ api_url,
+ index_url,
+ server,
+ dl_url,
+ path: registry_path,
+ token,
+ };
+
+ if self.configure_registry {
+ if let Some(alternative) = &self.alternative {
+ append(
+ &config_path,
+ format!(
+ "
+ [registries.{alternative}]
+ index = '{}'",
+ registry.index_url
+ )
+ .as_bytes(),
+ )
+ .unwrap();
+ } else {
+ append(
+ &config_path,
+ format!(
+ "
+ [source.crates-io]
+ replace-with = 'dummy-registry'
+
+ [registries.dummy-registry]
+ index = '{}'",
+ registry.index_url
+ )
+ .as_bytes(),
+ )
+ .unwrap();
+ }
+ }
+
+ if self.configure_token {
+ let credentials = paths::home().join(".cargo/credentials.toml");
+ match &registry.token {
+ Token::Plaintext(token) => {
+ if let Some(alternative) = &self.alternative {
+ append(
+ &credentials,
+ format!(
+ r#"
+ [registries.{alternative}]
+ token = "{token}"
+ "#
+ )
+ .as_bytes(),
+ )
+ .unwrap();
+ } else {
+ append(
+ &credentials,
+ format!(
+ r#"
+ [registry]
+ token = "{token}"
+ "#
+ )
+ .as_bytes(),
+ )
+ .unwrap();
+ }
+ }
+ Token::Keys(key, subject) => {
+ let mut out = if let Some(alternative) = &self.alternative {
+ format!("\n[registries.{alternative}]\n")
+ } else {
+ format!("\n[registry]\n")
+ };
+ out += &format!("secret-key = \"{key}\"\n");
+ if let Some(subject) = subject {
+ out += &format!("secret-key-subject = \"{subject}\"\n");
+ }
+
+ append(&credentials, out.as_bytes()).unwrap();
+ }
+ }
+ }
+
+ let auth = if self.auth_required {
+ r#","auth-required":true"#
+ } else {
+ ""
+ };
+ let api = if self.api {
+ format!(r#","api":"{}""#, registry.api_url)
+ } else {
+ String::new()
+ };
+ // Initialize a new registry.
+ repo(&registry.path)
+ .file(
+ "config.json",
+ &format!(r#"{{"dl":"{}"{api}{auth}}}"#, registry.dl_url),
+ )
+ .build();
+ fs::create_dir_all(api_path.join("api/v1/crates")).unwrap();
+
+ registry
+ }
+}
+
+/// A builder for creating a new package in a registry.
+///
+/// This uses "source replacement" using an automatically generated
+/// `.cargo/config` file to ensure that dependencies will use these packages
+/// instead of contacting crates.io. See `source-replacement.md` for more
+/// details on how source replacement works.
+///
+/// Call `publish` to finalize and create the package.
+///
+/// If no files are specified, an empty `lib.rs` file is automatically created.
+///
+/// The `Cargo.toml` file is automatically generated based on the methods
+/// called on `Package` (for example, calling `dep()` will add to the
+/// `[dependencies]` automatically). You may also specify a `Cargo.toml` file
+/// to override the generated one.
+///
+/// This supports different registry types:
+/// - Regular source replacement that replaces `crates.io` (the default).
+/// - A "local registry" which is a subset for vendoring (see
+/// `Package::local`).
+/// - An "alternative registry" which requires specifying the registry name
+/// (see `Package::alternative`).
+///
+/// This does not support "directory sources". See `directory.rs` for
+/// `VendorPackage` which implements directory sources.
+///
+/// # Example
+/// ```
+/// // Publish package "a" depending on "b".
+/// Package::new("a", "1.0.0")
+/// .dep("b", "1.0.0")
+/// .file("src/lib.rs", r#"
+/// extern crate b;
+/// pub fn f() -> i32 { b::f() * 2 }
+/// "#)
+/// .publish();
+///
+/// // Publish package "b".
+/// Package::new("b", "1.0.0")
+/// .file("src/lib.rs", r#"
+/// pub fn f() -> i32 { 12 }
+/// "#)
+/// .publish();
+///
+/// // Create a project that uses package "a".
+/// let p = project()
+/// .file("Cargo.toml", r#"
+/// [package]
+/// name = "foo"
+/// version = "0.0.1"
+///
+/// [dependencies]
+/// a = "1.0"
+/// "#)
+/// .file("src/main.rs", r#"
+/// extern crate a;
+/// fn main() { println!("{}", a::f()); }
+/// "#)
+/// .build();
+///
+/// p.cargo("run").with_stdout("24").run();
+/// ```
+#[must_use]
+pub struct Package {
+ name: String,
+ vers: String,
+ deps: Vec<Dependency>,
+ files: Vec<PackageFile>,
+ yanked: bool,
+ features: FeatureMap,
+ local: bool,
+ alternative: bool,
+ invalid_json: bool,
+ proc_macro: bool,
+ links: Option<String>,
+ rust_version: Option<String>,
+ cargo_features: Vec<String>,
+ v: Option<u32>,
+}
+
+pub(crate) type FeatureMap = BTreeMap<String, Vec<String>>;
+
+#[derive(Clone)]
+pub struct Dependency {
+ name: String,
+ vers: String,
+ kind: String,
+ artifact: Option<(String, Option<String>)>,
+ target: Option<String>,
+ features: Vec<String>,
+ registry: Option<String>,
+ package: Option<String>,
+ optional: bool,
+}
+
+/// Entry with data that corresponds to [`tar::EntryType`].
+#[non_exhaustive]
+enum EntryData {
+ Regular(String),
+ Symlink(PathBuf),
+}
+
+/// A file to be created in a package.
+struct PackageFile {
+ path: String,
+ contents: EntryData,
+ /// The Unix mode for the file. Note that when extracted on Windows, this
+ /// is mostly ignored since it doesn't have the same style of permissions.
+ mode: u32,
+ /// If `true`, the file is created in the root of the tarfile, used for
+ /// testing invalid packages.
+ extra: bool,
+}
+
+const DEFAULT_MODE: u32 = 0o644;
+
+/// Initializes the on-disk registry and sets up the config so that crates.io
+/// is replaced with the one on disk.
+pub fn init() -> TestRegistry {
+ RegistryBuilder::new().build()
+}
+
+/// Variant of `init` that initializes the "alternative" registry and crates.io
+/// replacement.
+pub fn alt_init() -> TestRegistry {
+ init();
+ RegistryBuilder::new().alternative().build()
+}
+
+pub struct HttpServerHandle {
+ addr: SocketAddr,
+ handle: Option<JoinHandle<()>>,
+}
+
+impl HttpServerHandle {
+ pub fn index_url(&self) -> Url {
+ Url::parse(&format!("sparse+http://{}/index/", self.addr.to_string())).unwrap()
+ }
+
+ pub fn api_url(&self) -> Url {
+ Url::parse(&format!("http://{}/", self.addr.to_string())).unwrap()
+ }
+
+ pub fn dl_url(&self) -> Url {
+ Url::parse(&format!("http://{}/dl", self.addr.to_string())).unwrap()
+ }
+
+ fn stop(&self) {
+ if let Ok(mut stream) = TcpStream::connect(self.addr) {
+ // shutdown the server
+ let _ = stream.write_all(b"stop");
+ let _ = stream.flush();
+ }
+ }
+}
+
+impl Drop for HttpServerHandle {
+ fn drop(&mut self) {
+ self.stop();
+ }
+}
+
+/// Request to the test http server
+#[derive(Clone)]
+pub struct Request {
+ pub url: Url,
+ pub method: String,
+ pub body: Option<Vec<u8>>,
+ pub authorization: Option<String>,
+ pub if_modified_since: Option<String>,
+ pub if_none_match: Option<String>,
+}
+
+impl fmt::Debug for Request {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ // body is not included as it can produce long debug outputs
+ f.debug_struct("Request")
+ .field("url", &self.url)
+ .field("method", &self.method)
+ .field("authorization", &self.authorization)
+ .field("if_modified_since", &self.if_modified_since)
+ .field("if_none_match", &self.if_none_match)
+ .finish()
+ }
+}
+
+/// Response from the test http server
+pub struct Response {
+ pub code: u32,
+ pub headers: Vec<String>,
+ pub body: Vec<u8>,
+}
+
+pub struct HttpServer {
+ listener: TcpListener,
+ registry_path: PathBuf,
+ dl_path: PathBuf,
+ api_path: PathBuf,
+ addr: SocketAddr,
+ token: Token,
+ auth_required: bool,
+ custom_responders: HashMap<&'static str, Box<dyn Send + Fn(&Request, &HttpServer) -> Response>>,
+}
+
+/// A helper struct that collects the arguments for [HttpServer::check_authorized].
+/// Based on looking at the request, these are the fields that the authentication header should attest to.
+pub struct Mutation<'a> {
+ pub mutation: &'a str,
+ pub name: Option<&'a str>,
+ pub vers: Option<&'a str>,
+ pub cksum: Option<&'a str>,
+}
+
+impl HttpServer {
+ pub fn new(
+ registry_path: PathBuf,
+ dl_path: PathBuf,
+ api_path: PathBuf,
+ token: Token,
+ auth_required: bool,
+ api_responders: HashMap<
+ &'static str,
+ Box<dyn Send + Fn(&Request, &HttpServer) -> Response>,
+ >,
+ ) -> HttpServerHandle {
+ let listener = TcpListener::bind("127.0.0.1:0").unwrap();
+ let addr = listener.local_addr().unwrap();
+ let server = HttpServer {
+ listener,
+ registry_path,
+ dl_path,
+ api_path,
+ addr,
+ token,
+ auth_required,
+ custom_responders: api_responders,
+ };
+ let handle = Some(thread::spawn(move || server.start()));
+ HttpServerHandle { addr, handle }
+ }
+
+ fn start(&self) {
+ let mut line = String::new();
+ 'server: loop {
+ let (socket, _) = self.listener.accept().unwrap();
+ let mut buf = BufReader::new(socket);
+ line.clear();
+ if buf.read_line(&mut line).unwrap() == 0 {
+ // Connection terminated.
+ continue;
+ }
+ // Read the "GET path HTTP/1.1" line.
+ let mut parts = line.split_ascii_whitespace();
+ let method = parts.next().unwrap().to_ascii_lowercase();
+ if method == "stop" {
+ // Shutdown the server.
+ return;
+ }
+ let addr = self.listener.local_addr().unwrap();
+ let url = format!(
+ "http://{}/{}",
+ addr,
+ parts.next().unwrap().trim_start_matches('/')
+ );
+ let url = Url::parse(&url).unwrap();
+
+ // Grab headers we care about.
+ let mut if_modified_since = None;
+ let mut if_none_match = None;
+ let mut authorization = None;
+ let mut content_len = None;
+ loop {
+ line.clear();
+ if buf.read_line(&mut line).unwrap() == 0 {
+ continue 'server;
+ }
+ if line == "\r\n" {
+ // End of headers.
+ line.clear();
+ break;
+ }
+ let (name, value) = line.split_once(':').unwrap();
+ let name = name.trim().to_ascii_lowercase();
+ let value = value.trim().to_string();
+ match name.as_str() {
+ "if-modified-since" => if_modified_since = Some(value),
+ "if-none-match" => if_none_match = Some(value),
+ "authorization" => authorization = Some(value),
+ "content-length" => content_len = Some(value),
+ _ => {}
+ }
+ }
+
+ let mut body = None;
+ if let Some(con_len) = content_len {
+ let len = con_len.parse::<u64>().unwrap();
+ let mut content = vec![0u8; len as usize];
+ buf.read_exact(&mut content).unwrap();
+ body = Some(content)
+ }
+
+ let req = Request {
+ authorization,
+ if_modified_since,
+ if_none_match,
+ method,
+ url,
+ body,
+ };
+ println!("req: {:#?}", req);
+ let response = self.route(&req);
+ let buf = buf.get_mut();
+ write!(buf, "HTTP/1.1 {}\r\n", response.code).unwrap();
+ write!(buf, "Content-Length: {}\r\n", response.body.len()).unwrap();
+ for header in response.headers {
+ write!(buf, "{}\r\n", header).unwrap();
+ }
+ write!(buf, "\r\n").unwrap();
+ buf.write_all(&response.body).unwrap();
+ buf.flush().unwrap();
+ }
+ }
+
+ fn check_authorized(&self, req: &Request, mutation: Option<Mutation>) -> bool {
+ let (private_key, private_key_subject) = if mutation.is_some() || self.auth_required {
+ match &self.token {
+ Token::Plaintext(token) => return Some(token) == req.authorization.as_ref(),
+ Token::Keys(private_key, private_key_subject) => {
+ (private_key.as_str(), private_key_subject)
+ }
+ }
+ } else {
+ assert!(req.authorization.is_none(), "unexpected token");
+ return true;
+ };
+
+ macro_rules! t {
+ ($e:expr) => {
+ match $e {
+ Some(e) => e,
+ None => return false,
+ }
+ };
+ }
+
+ let secret: AsymmetricSecretKey<pasetors::version3::V3> = private_key.try_into().unwrap();
+ let public: AsymmetricPublicKey<pasetors::version3::V3> = (&secret).try_into().unwrap();
+ let pub_key_id: pasetors::paserk::Id = (&public).into();
+ let mut paserk_pub_key_id = String::new();
+ FormatAsPaserk::fmt(&pub_key_id, &mut paserk_pub_key_id).unwrap();
+ // https://github.com/rust-lang/rfcs/blob/master/text/3231-cargo-asymmetric-tokens.md#how-the-registry-server-will-validate-an-asymmetric-token
+
+ // - The PASETO is in v3.public format.
+ let authorization = t!(&req.authorization);
+ let untrusted_token = t!(
+ UntrustedToken::<pasetors::Public, pasetors::version3::V3>::try_from(authorization)
+ .ok()
+ );
+
+ // - The PASETO validates using the public key it looked up based on the key ID.
+ #[derive(serde::Deserialize, Debug)]
+ struct Footer<'a> {
+ url: &'a str,
+ kip: &'a str,
+ }
+ let footer: Footer = t!(serde_json::from_slice(untrusted_token.untrusted_footer()).ok());
+ if footer.kip != paserk_pub_key_id {
+ return false;
+ }
+ let trusted_token =
+ t!(
+ pasetors::version3::PublicToken::verify(&public, &untrusted_token, None, None,)
+ .ok()
+ );
+
+ // - The URL matches the registry base URL
+ if footer.url != "https://github.com/rust-lang/crates.io-index"
+ && footer.url != &format!("sparse+http://{}/index/", self.addr.to_string())
+ {
+ dbg!(footer.url);
+ return false;
+ }
+
+ // - The PASETO is still within its valid time period.
+ #[derive(serde::Deserialize)]
+ struct Message<'a> {
+ iat: &'a str,
+ sub: Option<&'a str>,
+ mutation: Option<&'a str>,
+ name: Option<&'a str>,
+ vers: Option<&'a str>,
+ cksum: Option<&'a str>,
+ _challenge: Option<&'a str>, // todo: PASETO with challenges
+ v: Option<u8>,
+ }
+ let message: Message = t!(serde_json::from_str(trusted_token.payload()).ok());
+ let token_time = t!(OffsetDateTime::parse(message.iat, &Rfc3339).ok());
+ let now = OffsetDateTime::now_utc();
+ if (now - token_time) > Duration::MINUTE {
+ return false;
+ }
+ if private_key_subject.as_deref() != message.sub {
+ dbg!(message.sub);
+ return false;
+ }
+ // - If the claim v is set, that it has the value of 1.
+ if let Some(v) = message.v {
+ if v != 1 {
+ dbg!(message.v);
+ return false;
+ }
+ }
+ // - If the server issues challenges, that the challenge has not yet been answered.
+ // todo: PASETO with challenges
+ // - If the operation is a mutation:
+ if let Some(mutation) = mutation {
+ // - That the operation matches the mutation field and is one of publish, yank, or unyank.
+ if message.mutation != Some(mutation.mutation) {
+ dbg!(message.mutation);
+ return false;
+ }
+ // - That the package, and version match the request.
+ if message.name != mutation.name {
+ dbg!(message.name);
+ return false;
+ }
+ if message.vers != mutation.vers {
+ dbg!(message.vers);
+ return false;
+ }
+ // - If the mutation is publish, that the version has not already been published, and that the hash matches the request.
+ if mutation.mutation == "publish" {
+ if message.cksum != mutation.cksum {
+ dbg!(message.cksum);
+ return false;
+ }
+ }
+ } else {
+ // - If the operation is a read, that the mutation field is not set.
+ if message.mutation.is_some()
+ || message.name.is_some()
+ || message.vers.is_some()
+ || message.cksum.is_some()
+ {
+ return false;
+ }
+ }
+ true
+ }
+
+ /// Route the request
+ fn route(&self, req: &Request) -> Response {
+ // Check for custom responder
+ if let Some(responder) = self.custom_responders.get(req.url.path()) {
+ return responder(&req, self);
+ }
+ let path: Vec<_> = req.url.path()[1..].split('/').collect();
+ match (req.method.as_str(), path.as_slice()) {
+ ("get", ["index", ..]) => {
+ if !self.check_authorized(req, None) {
+ self.unauthorized(req)
+ } else {
+ self.index(&req)
+ }
+ }
+ ("get", ["dl", ..]) => {
+ if !self.check_authorized(req, None) {
+ self.unauthorized(req)
+ } else {
+ self.dl(&req)
+ }
+ }
+ // publish
+ ("put", ["api", "v1", "crates", "new"]) => self.check_authorized_publish(req),
+ // The remainder of the operators in the test framework do nothing other than responding 'ok'.
+ //
+ // Note: We don't need to support anything real here because there are no tests that
+ // currently require anything other than publishing via the http api.
+
+ // yank / unyank
+ ("delete" | "put", ["api", "v1", "crates", crate_name, version, mutation]) => {
+ if !self.check_authorized(
+ req,
+ Some(Mutation {
+ mutation,
+ name: Some(crate_name),
+ vers: Some(version),
+ cksum: None,
+ }),
+ ) {
+ self.unauthorized(req)
+ } else {
+ self.ok(&req)
+ }
+ }
+ // owners
+ ("get" | "put" | "delete", ["api", "v1", "crates", crate_name, "owners"]) => {
+ if !self.check_authorized(
+ req,
+ Some(Mutation {
+ mutation: "owners",
+ name: Some(crate_name),
+ vers: None,
+ cksum: None,
+ }),
+ ) {
+ self.unauthorized(req)
+ } else {
+ self.ok(&req)
+ }
+ }
+ _ => self.not_found(&req),
+ }
+ }
+
+ /// Unauthorized response
+ pub fn unauthorized(&self, _req: &Request) -> Response {
+ Response {
+ code: 401,
+ headers: vec![
+ r#"WWW-Authenticate: Cargo login_url="https://test-registry-login/me""#.to_string(),
+ ],
+ body: b"Unauthorized message from server.".to_vec(),
+ }
+ }
+
+ /// Not found response
+ pub fn not_found(&self, _req: &Request) -> Response {
+ Response {
+ code: 404,
+ headers: vec![],
+ body: b"not found".to_vec(),
+ }
+ }
+
+ /// Respond OK without doing anything
+ pub fn ok(&self, _req: &Request) -> Response {
+ Response {
+ code: 200,
+ headers: vec![],
+ body: br#"{"ok": true, "msg": "completed!"}"#.to_vec(),
+ }
+ }
+
+ /// Return an internal server error (HTTP 500)
+ pub fn internal_server_error(&self, _req: &Request) -> Response {
+ Response {
+ code: 500,
+ headers: vec![],
+ body: br#"internal server error"#.to_vec(),
+ }
+ }
+
+ /// Serve the download endpoint
+ pub fn dl(&self, req: &Request) -> Response {
+ let file = self
+ .dl_path
+ .join(req.url.path().strip_prefix("/dl/").unwrap());
+ println!("{}", file.display());
+ if !file.exists() {
+ return self.not_found(req);
+ }
+ return Response {
+ body: fs::read(&file).unwrap(),
+ code: 200,
+ headers: vec![],
+ };
+ }
+
+ /// Serve the registry index
+ pub fn index(&self, req: &Request) -> Response {
+ let file = self
+ .registry_path
+ .join(req.url.path().strip_prefix("/index/").unwrap());
+ if !file.exists() {
+ return self.not_found(req);
+ } else {
+ // Now grab info about the file.
+ let data = fs::read(&file).unwrap();
+ let etag = Sha256::new().update(&data).finish_hex();
+ let last_modified = format!("{:?}", file.metadata().unwrap().modified().unwrap());
+
+ // Start to construct our response:
+ let mut any_match = false;
+ let mut all_match = true;
+ if let Some(expected) = &req.if_none_match {
+ if &etag != expected {
+ all_match = false;
+ } else {
+ any_match = true;
+ }
+ }
+ if let Some(expected) = &req.if_modified_since {
+ // NOTE: Equality comparison is good enough for tests.
+ if &last_modified != expected {
+ all_match = false;
+ } else {
+ any_match = true;
+ }
+ }
+
+ if any_match && all_match {
+ return Response {
+ body: Vec::new(),
+ code: 304,
+ headers: vec![],
+ };
+ } else {
+ return Response {
+ body: data,
+ code: 200,
+ headers: vec![
+ format!("ETag: \"{}\"", etag),
+ format!("Last-Modified: {}", last_modified),
+ ],
+ };
+ }
+ }
+ }
+
+ pub fn check_authorized_publish(&self, req: &Request) -> Response {
+ if let Some(body) = &req.body {
+ // Mimic the publish behavior for local registries by writing out the request
+ // so tests can verify publishes made to either registry type.
+ let path = self.api_path.join("api/v1/crates/new");
+ t!(fs::create_dir_all(path.parent().unwrap()));
+ t!(fs::write(&path, body));
+
+ // Get the metadata of the package
+ let (len, remaining) = body.split_at(4);
+ let json_len = u32::from_le_bytes(len.try_into().unwrap());
+ let (json, remaining) = remaining.split_at(json_len as usize);
+ let new_crate = serde_json::from_slice::<crates_io::NewCrate>(json).unwrap();
+ // Get the `.crate` file
+ let (len, remaining) = remaining.split_at(4);
+ let file_len = u32::from_le_bytes(len.try_into().unwrap());
+ let (file, _remaining) = remaining.split_at(file_len as usize);
+ let file_cksum = cksum(&file);
+
+ if !self.check_authorized(
+ req,
+ Some(Mutation {
+ mutation: "publish",
+ name: Some(&new_crate.name),
+ vers: Some(&new_crate.vers),
+ cksum: Some(&file_cksum),
+ }),
+ ) {
+ return self.unauthorized(req);
+ }
+
+ // Write the `.crate`
+ let dst = self
+ .dl_path
+ .join(&new_crate.name)
+ .join(&new_crate.vers)
+ .join("download");
+ t!(fs::create_dir_all(dst.parent().unwrap()));
+ t!(fs::write(&dst, file));
+
+ let deps = new_crate
+ .deps
+ .iter()
+ .map(|dep| {
+ let (name, package) = match &dep.explicit_name_in_toml {
+ Some(explicit) => (explicit.to_string(), Some(dep.name.to_string())),
+ None => (dep.name.to_string(), None),
+ };
+ serde_json::json!({
+ "name": name,
+ "req": dep.version_req,
+ "features": dep.features,
+ "default_features": true,
+ "target": dep.target,
+ "optional": dep.optional,
+ "kind": dep.kind,
+ "registry": dep.registry,
+ "package": package,
+ })
+ })
+ .collect::<Vec<_>>();
+
+ let line = create_index_line(
+ serde_json::json!(new_crate.name),
+ &new_crate.vers,
+ deps,
+ &file_cksum,
+ new_crate.features,
+ false,
+ new_crate.links,
+ None,
+ );
+
+ write_to_index(&self.registry_path, &new_crate.name, line, false);
+
+ self.ok(&req)
+ } else {
+ Response {
+ code: 400,
+ headers: vec![],
+ body: b"The request was missing a body".to_vec(),
+ }
+ }
+ }
+}
+
+impl Package {
+ /// Creates a new package builder.
+ /// Call `publish()` to finalize and build the package.
+ pub fn new(name: &str, vers: &str) -> Package {
+ let config = paths::home().join(".cargo/config");
+ if !config.exists() {
+ init();
+ }
+ Package {
+ name: name.to_string(),
+ vers: vers.to_string(),
+ deps: Vec::new(),
+ files: Vec::new(),
+ yanked: false,
+ features: BTreeMap::new(),
+ local: false,
+ alternative: false,
+ invalid_json: false,
+ proc_macro: false,
+ links: None,
+ rust_version: None,
+ cargo_features: Vec::new(),
+ v: None,
+ }
+ }
+
+ /// Call with `true` to publish in a "local registry".
+ ///
+ /// See `source-replacement.html#local-registry-sources` for more details
+ /// on local registries. See `local_registry.rs` for the tests that use
+ /// this.
+ pub fn local(&mut self, local: bool) -> &mut Package {
+ self.local = local;
+ self
+ }
+
+ /// Call with `true` to publish in an "alternative registry".
+ ///
+ /// The name of the alternative registry is called "alternative".
+ ///
+ /// See `src/doc/src/reference/registries.md` for more details on
+ /// alternative registries. See `alt_registry.rs` for the tests that use
+ /// this.
+ pub fn alternative(&mut self, alternative: bool) -> &mut Package {
+ self.alternative = alternative;
+ self
+ }
+
+ /// Adds a file to the package.
+ pub fn file(&mut self, name: &str, contents: &str) -> &mut Package {
+ self.file_with_mode(name, DEFAULT_MODE, contents)
+ }
+
+ /// Adds a file with a specific Unix mode.
+ pub fn file_with_mode(&mut self, path: &str, mode: u32, contents: &str) -> &mut Package {
+ self.files.push(PackageFile {
+ path: path.to_string(),
+ contents: EntryData::Regular(contents.into()),
+ mode,
+ extra: false,
+ });
+ self
+ }
+
+ /// Adds a symlink to a path to the package.
+ pub fn symlink(&mut self, dst: &str, src: &str) -> &mut Package {
+ self.files.push(PackageFile {
+ path: dst.to_string(),
+ contents: EntryData::Symlink(src.into()),
+ mode: DEFAULT_MODE,
+ extra: false,
+ });
+ self
+ }
+
+ /// Adds an "extra" file that is not rooted within the package.
+ ///
+ /// Normal files are automatically placed within a directory named
+ /// `$PACKAGE-$VERSION`. This allows you to override that behavior,
+ /// typically for testing invalid behavior.
+ pub fn extra_file(&mut self, path: &str, contents: &str) -> &mut Package {
+ self.files.push(PackageFile {
+ path: path.to_string(),
+ contents: EntryData::Regular(contents.to_string()),
+ mode: DEFAULT_MODE,
+ extra: true,
+ });
+ self
+ }
+
+ /// Adds a normal dependency. Example:
+ /// ```
+ /// [dependencies]
+ /// foo = {version = "1.0"}
+ /// ```
+ pub fn dep(&mut self, name: &str, vers: &str) -> &mut Package {
+ self.add_dep(&Dependency::new(name, vers))
+ }
+
+ /// Adds a dependency with the given feature. Example:
+ /// ```
+ /// [dependencies]
+ /// foo = {version = "1.0", "features": ["feat1", "feat2"]}
+ /// ```
+ pub fn feature_dep(&mut self, name: &str, vers: &str, features: &[&str]) -> &mut Package {
+ self.add_dep(Dependency::new(name, vers).enable_features(features))
+ }
+
+ /// Adds a platform-specific dependency. Example:
+ /// ```
+ /// [target.'cfg(windows)'.dependencies]
+ /// foo = {version = "1.0"}
+ /// ```
+ pub fn target_dep(&mut self, name: &str, vers: &str, target: &str) -> &mut Package {
+ self.add_dep(Dependency::new(name, vers).target(target))
+ }
+
+ /// Adds a dependency to the alternative registry.
+ pub fn registry_dep(&mut self, name: &str, vers: &str) -> &mut Package {
+ self.add_dep(Dependency::new(name, vers).registry("alternative"))
+ }
+
+ /// Adds a dev-dependency. Example:
+ /// ```
+ /// [dev-dependencies]
+ /// foo = {version = "1.0"}
+ /// ```
+ pub fn dev_dep(&mut self, name: &str, vers: &str) -> &mut Package {
+ self.add_dep(Dependency::new(name, vers).dev())
+ }
+
+ /// Adds a build-dependency. Example:
+ /// ```
+ /// [build-dependencies]
+ /// foo = {version = "1.0"}
+ /// ```
+ pub fn build_dep(&mut self, name: &str, vers: &str) -> &mut Package {
+ self.add_dep(Dependency::new(name, vers).build())
+ }
+
+ pub fn add_dep(&mut self, dep: &Dependency) -> &mut Package {
+ self.deps.push(dep.clone());
+ self
+ }
+
+ /// Specifies whether or not the package is "yanked".
+ pub fn yanked(&mut self, yanked: bool) -> &mut Package {
+ self.yanked = yanked;
+ self
+ }
+
+ /// Specifies whether or not this is a proc macro.
+ pub fn proc_macro(&mut self, proc_macro: bool) -> &mut Package {
+ self.proc_macro = proc_macro;
+ self
+ }
+
+ /// Adds an entry in the `[features]` section.
+ pub fn feature(&mut self, name: &str, deps: &[&str]) -> &mut Package {
+ let deps = deps.iter().map(|s| s.to_string()).collect();
+ self.features.insert(name.to_string(), deps);
+ self
+ }
+
+ /// Specify a minimal Rust version.
+ pub fn rust_version(&mut self, rust_version: &str) -> &mut Package {
+ self.rust_version = Some(rust_version.into());
+ self
+ }
+
+ /// Causes the JSON line emitted in the index to be invalid, presumably
+ /// causing Cargo to skip over this version.
+ pub fn invalid_json(&mut self, invalid: bool) -> &mut Package {
+ self.invalid_json = invalid;
+ self
+ }
+
+ pub fn links(&mut self, links: &str) -> &mut Package {
+ self.links = Some(links.to_string());
+ self
+ }
+
+ pub fn cargo_feature(&mut self, feature: &str) -> &mut Package {
+ self.cargo_features.push(feature.to_owned());
+ self
+ }
+
+ /// Sets the index schema version for this package.
+ ///
+ /// See `cargo::sources::registry::RegistryPackage` for more information.
+ pub fn schema_version(&mut self, version: u32) -> &mut Package {
+ self.v = Some(version);
+ self
+ }
+
+ /// Creates the package and place it in the registry.
+ ///
+ /// This does not actually use Cargo's publishing system, but instead
+ /// manually creates the entry in the registry on the filesystem.
+ ///
+ /// Returns the checksum for the package.
+ pub fn publish(&self) -> String {
+ self.make_archive();
+
+ // Figure out what we're going to write into the index.
+ let deps = self
+ .deps
+ .iter()
+ .map(|dep| {
+ // In the index, the `registry` is null if it is from the same registry.
+ // In Cargo.toml, it is None if it is from crates.io.
+ let registry_url = match (self.alternative, dep.registry.as_deref()) {
+ (false, None) => None,
+ (false, Some("alternative")) => Some(alt_registry_url().to_string()),
+ (true, None) => {
+ Some("https://github.com/rust-lang/crates.io-index".to_string())
+ }
+ (true, Some("alternative")) => None,
+ _ => panic!("registry_dep currently only supports `alternative`"),
+ };
+ serde_json::json!({
+ "name": dep.name,
+ "req": dep.vers,
+ "features": dep.features,
+ "default_features": true,
+ "target": dep.target,
+ "artifact": dep.artifact,
+ "optional": dep.optional,
+ "kind": dep.kind,
+ "registry": registry_url,
+ "package": dep.package,
+ })
+ })
+ .collect::<Vec<_>>();
+ let cksum = {
+ let c = t!(fs::read(&self.archive_dst()));
+ cksum(&c)
+ };
+ let name = if self.invalid_json {
+ serde_json::json!(1)
+ } else {
+ serde_json::json!(self.name)
+ };
+ let line = create_index_line(
+ name,
+ &self.vers,
+ deps,
+ &cksum,
+ self.features.clone(),
+ self.yanked,
+ self.links.clone(),
+ self.v,
+ );
+
+ let registry_path = if self.alternative {
+ alt_registry_path()
+ } else {
+ registry_path()
+ };
+
+ write_to_index(&registry_path, &self.name, line, self.local);
+
+ cksum
+ }
+
+ fn make_archive(&self) {
+ let dst = self.archive_dst();
+ t!(fs::create_dir_all(dst.parent().unwrap()));
+ let f = t!(File::create(&dst));
+ let mut a = Builder::new(GzEncoder::new(f, Compression::default()));
+
+ if !self
+ .files
+ .iter()
+ .any(|PackageFile { path, .. }| path == "Cargo.toml")
+ {
+ self.append_manifest(&mut a);
+ }
+ if self.files.is_empty() {
+ self.append(
+ &mut a,
+ "src/lib.rs",
+ DEFAULT_MODE,
+ &EntryData::Regular("".into()),
+ );
+ } else {
+ for PackageFile {
+ path,
+ contents,
+ mode,
+ extra,
+ } in &self.files
+ {
+ if *extra {
+ self.append_raw(&mut a, path, *mode, contents);
+ } else {
+ self.append(&mut a, path, *mode, contents);
+ }
+ }
+ }
+ }
+
+ fn append_manifest<W: Write>(&self, ar: &mut Builder<W>) {
+ let mut manifest = String::new();
+
+ if !self.cargo_features.is_empty() {
+ let mut features = String::new();
+ serde::Serialize::serialize(
+ &self.cargo_features,
+ toml::ser::ValueSerializer::new(&mut features),
+ )
+ .unwrap();
+ manifest.push_str(&format!("cargo-features = {}\n\n", features));
+ }
+
+ manifest.push_str(&format!(
+ r#"
+ [package]
+ name = "{}"
+ version = "{}"
+ authors = []
+ "#,
+ self.name, self.vers
+ ));
+
+ if let Some(version) = &self.rust_version {
+ manifest.push_str(&format!("rust-version = \"{}\"", version));
+ }
+
+ for dep in self.deps.iter() {
+ let target = match dep.target {
+ None => String::new(),
+ Some(ref s) => format!("target.'{}'.", s),
+ };
+ let kind = match &dep.kind[..] {
+ "build" => "build-",
+ "dev" => "dev-",
+ _ => "",
+ };
+ manifest.push_str(&format!(
+ r#"
+ [{}{}dependencies.{}]
+ version = "{}"
+ "#,
+ target, kind, dep.name, dep.vers
+ ));
+ if let Some((artifact, target)) = &dep.artifact {
+ manifest.push_str(&format!("artifact = \"{}\"\n", artifact));
+ if let Some(target) = &target {
+ manifest.push_str(&format!("target = \"{}\"\n", target))
+ }
+ }
+ if let Some(registry) = &dep.registry {
+ assert_eq!(registry, "alternative");
+ manifest.push_str(&format!("registry-index = \"{}\"", alt_registry_url()));
+ }
+ }
+ if self.proc_macro {
+ manifest.push_str("[lib]\nproc-macro = true\n");
+ }
+
+ self.append(
+ ar,
+ "Cargo.toml",
+ DEFAULT_MODE,
+ &EntryData::Regular(manifest.into()),
+ );
+ }
+
+ fn append<W: Write>(&self, ar: &mut Builder<W>, file: &str, mode: u32, contents: &EntryData) {
+ self.append_raw(
+ ar,
+ &format!("{}-{}/{}", self.name, self.vers, file),
+ mode,
+ contents,
+ );
+ }
+
+ fn append_raw<W: Write>(
+ &self,
+ ar: &mut Builder<W>,
+ path: &str,
+ mode: u32,
+ contents: &EntryData,
+ ) {
+ let mut header = Header::new_ustar();
+ let contents = match contents {
+ EntryData::Regular(contents) => contents.as_str(),
+ EntryData::Symlink(src) => {
+ header.set_entry_type(tar::EntryType::Symlink);
+ t!(header.set_link_name(src));
+ "" // Symlink has no contents.
+ }
+ };
+ header.set_size(contents.len() as u64);
+ t!(header.set_path(path));
+ header.set_mode(mode);
+ header.set_cksum();
+ t!(ar.append(&header, contents.as_bytes()));
+ }
+
+ /// Returns the path to the compressed package file.
+ pub fn archive_dst(&self) -> PathBuf {
+ if self.local {
+ registry_path().join(format!("{}-{}.crate", self.name, self.vers))
+ } else if self.alternative {
+ alt_dl_path()
+ .join(&self.name)
+ .join(&self.vers)
+ .join("download")
+ } else {
+ dl_path().join(&self.name).join(&self.vers).join("download")
+ }
+ }
+}
+
+pub fn cksum(s: &[u8]) -> String {
+ Sha256::new().update(s).finish_hex()
+}
+
+impl Dependency {
+ pub fn new(name: &str, vers: &str) -> Dependency {
+ Dependency {
+ name: name.to_string(),
+ vers: vers.to_string(),
+ kind: "normal".to_string(),
+ artifact: None,
+ target: None,
+ features: Vec::new(),
+ package: None,
+ optional: false,
+ registry: None,
+ }
+ }
+
+ /// Changes this to `[build-dependencies]`.
+ pub fn build(&mut self) -> &mut Self {
+ self.kind = "build".to_string();
+ self
+ }
+
+ /// Changes this to `[dev-dependencies]`.
+ pub fn dev(&mut self) -> &mut Self {
+ self.kind = "dev".to_string();
+ self
+ }
+
+ /// Changes this to `[target.$target.dependencies]`.
+ pub fn target(&mut self, target: &str) -> &mut Self {
+ self.target = Some(target.to_string());
+ self
+ }
+
+ /// Change the artifact to be of the given kind, like "bin", or "staticlib",
+ /// along with a specific target triple if provided.
+ pub fn artifact(&mut self, kind: &str, target: Option<String>) -> &mut Self {
+ self.artifact = Some((kind.to_string(), target));
+ self
+ }
+
+ /// Adds `registry = $registry` to this dependency.
+ pub fn registry(&mut self, registry: &str) -> &mut Self {
+ self.registry = Some(registry.to_string());
+ self
+ }
+
+ /// Adds `features = [ ... ]` to this dependency.
+ pub fn enable_features(&mut self, features: &[&str]) -> &mut Self {
+ self.features.extend(features.iter().map(|s| s.to_string()));
+ self
+ }
+
+ /// Adds `package = ...` to this dependency.
+ pub fn package(&mut self, pkg: &str) -> &mut Self {
+ self.package = Some(pkg.to_string());
+ self
+ }
+
+ /// Changes this to an optional dependency.
+ pub fn optional(&mut self, optional: bool) -> &mut Self {
+ self.optional = optional;
+ self
+ }
+}