From 0195b4d65167a22ebdd8fcd94ae0935abc6fcf17 Mon Sep 17 00:00:00 2001 From: Andres Mejia Sanchez Date: Tue, 6 Jan 2026 23:59:33 -0500 Subject: [PATCH 01/15] Support adding `Authorization` HTTP headers through the `RUSTUP_AUTHORIZATION_HEADER` environment variable. This partially addresses https://github.com/rust-lang/rustup/issues/1343 by supporting reverse proxies which require authentication that function as mirrors to https://static.rust-lang.org . --- doc/user-guide/src/environment-variables.md | 6 + src/download/mod.rs | 117 ++++++++++++++---- src/download/tests.rs | 10 ++ .../ferron-rustup-dist-server.kdl | 38 ++++++ .../logs/.gitignore | 1 + 5 files changed, 145 insertions(+), 27 deletions(-) create mode 100644 tests/test-dist-server-resources/ferron-rustup-dist-server.kdl create mode 100644 tests/test-dist-server-resources/logs/.gitignore diff --git a/doc/user-guide/src/environment-variables.md b/doc/user-guide/src/environment-variables.md index bb9dc3d512..e40f1cc875 100644 --- a/doc/user-guide/src/environment-variables.md +++ b/doc/user-guide/src/environment-variables.md @@ -28,6 +28,12 @@ - `RUSTUP_VERSION` (default: none). Overrides the rustup version (e.g. `1.27.1`) to be downloaded when executing `rustup-init.sh` or `rustup self update`. +- `RUSTUP_AUTHORIZATION_HEADER` (default: none). The value to an `Authorization` HTTP + header that should be added to all requests made by rustup. This is meant for use when + using an alternate rustup distribution server (through the `RUSTUP_DIST_SERVER` + environment variable) which requires authentication such as basic username:password + credentials or a bearer token. + - `RUSTUP_IO_THREADS` *unstable* (default: reported cpu count, max 8). Sets the number of threads to perform close IO in. Set to `1` to force single-threaded IO for troubleshooting, or an arbitrary number to override diff --git a/src/download/mod.rs b/src/download/mod.rs index 37ae9b32d4..3770e0c0a0 100644 --- a/src/download/mod.rs +++ b/src/download/mod.rs @@ -207,7 +207,14 @@ async fn download_file_( }; let res = backend - .download_to_path(url, path, resume_from_partial, Some(callback), timeout) + .download_to_path( + url, + path, + resume_from_partial, + Some(callback), + timeout, + process, + ) .await; // The notification should only be sent if the download was successful (i.e. didn't timeout) @@ -253,9 +260,10 @@ impl Backend { resume_from_partial: bool, callback: Option>, timeout: Duration, + process: &Process, ) -> anyhow::Result<()> { let Err(err) = self - .download_impl(url, path, resume_from_partial, callback, timeout) + .download_impl(url, path, resume_from_partial, callback, timeout, process) .await else { return Ok(()); @@ -278,6 +286,7 @@ impl Backend { resume_from_partial: bool, callback: Option>, timeout: Duration, + process: &Process, ) -> anyhow::Result<()> { use std::cell::RefCell; use std::fs::OpenOptions; @@ -337,17 +346,23 @@ impl Backend { let file = RefCell::new(file); // TODO: the sync callback will stall the async runtime if IO calls block, which is OS dependent. Rearrange. - self.download(url, resume_from, timeout, &|event| { - if let Event::DownloadDataReceived(data) = event { - file.borrow_mut() - .write_all(data) - .context("unable to write download to disk")?; - } - match callback { - Some(cb) => cb(event), - None => Ok(()), - } - }) + self.download( + url, + resume_from, + timeout, + &|event| { + if let Event::DownloadDataReceived(data) = event { + file.borrow_mut() + .write_all(data) + .context("unable to write download to disk")?; + } + match callback { + Some(cb) => cb(event), + None => Ok(()), + } + }, + process, + ) .await?; file.borrow_mut() @@ -371,12 +386,16 @@ impl Backend { resume_from: u64, timeout: Duration, callback: DownloadCallback<'_>, + process: &Process, ) -> anyhow::Result<()> { match self { #[cfg(feature = "curl-backend")] - Self::Curl => curl::download(url, resume_from, callback, timeout), + Self::Curl => curl::download(url, resume_from, callback, timeout, process), #[cfg(any(feature = "reqwest-rustls-tls", feature = "reqwest-native-tls"))] - Self::Reqwest(tls) => tls.download(url, resume_from, callback, timeout).await, + Self::Reqwest(tls) => { + tls.download(url, resume_from, callback, timeout, process) + .await + } } } } @@ -398,12 +417,13 @@ impl TlsBackend { resume_from: u64, callback: DownloadCallback<'_>, timeout: Duration, + process: &Process, ) -> anyhow::Result<()> { let client = match self { #[cfg(feature = "reqwest-rustls-tls")] - Self::Rustls => reqwest_be::rustls_client(timeout)?, + Self::Rustls => reqwest_be::rustls_client(timeout, process)?, #[cfg(feature = "reqwest-native-tls")] - Self::NativeTls => reqwest_be::native_tls_client(timeout)?, + Self::NativeTls => reqwest_be::native_tls_client(timeout, process)?, }; reqwest_be::download(url, resume_from, callback, client).await @@ -430,16 +450,18 @@ mod curl { use std::time::Duration; use anyhow::{Context, Result}; - use curl::easy::Easy; + use curl::easy::{Easy, List}; + use tracing::debug; use url::Url; - use super::{DownloadError, Event}; + use super::{DownloadError, Event, Process}; pub(super) fn download( url: &Url, resume_from: u64, callback: &dyn Fn(Event<'_>) -> Result<()>, timeout: Duration, + process: &Process, ) -> Result<()> { // Fetch either a cached libcurl handle (which will preserve open // connections) or create a new one if it isn't listed. @@ -453,6 +475,20 @@ mod curl { handle.url(url.as_ref())?; handle.follow_location(true)?; handle.useragent(super::CURL_USER_AGENT)?; + if let Some(rustup_authorization_header_value) = process.var_opt("RUSTUP_AUTHORIZATION_HEADER").map_err(|error| { + anyhow::anyhow!("Internal error getting `RUSTUP_AUTHORIZATION_HEADER` environment variable: {}", anyhow::format_err!(error)) + })? { + let mut list = List::new(); + list.append(format!("Authorization: {rustup_authorization_header_value}").as_str()).map_err(|_| { + // The error could contain sensitive data so give a generic error instead. + anyhow::anyhow!("Failed to add `Authorization` HTTP header.") + })?; + handle.http_headers(list).map_err(|_| { + // The error could contain sensitive data so give a generic error instead. + anyhow::anyhow!("Failed to add headers to curl handle.") + })?; + debug!("Added `Authorization` header."); + } if resume_from > 0 { handle.resume_from(resume_from)?; @@ -557,7 +593,7 @@ mod reqwest_be { use tokio_stream::StreamExt; use url::Url; - use super::{DownloadError, Event}; + use super::{DownloadError, Event, Process, debug}; pub(super) async fn download( url: &Url, @@ -592,18 +628,42 @@ mod reqwest_be { Ok(()) } - fn client_generic() -> ClientBuilder { - Client::builder() + fn client_generic(process: &Process) -> Result { + let mut client_builder = Client::builder() // HACK: set `pool_max_idle_per_host` to `0` to avoid an issue in the underlying // `hyper` library that causes the `reqwest` client to hang in some cases. // See for more details. .pool_max_idle_per_host(0) .gzip(false) - .proxy(Proxy::custom(env_proxy)) + .proxy(Proxy::custom(env_proxy)); + if let Some(rustup_authorization_header_value) = process + .var_opt("RUSTUP_AUTHORIZATION_HEADER") + .map_err(|_| { + // The error could contain sensitive data so give a generic error instead. + DownloadError::Message( + "Internal error getting `RUSTUP_AUTHORIZATION_HEADER` environment variable" + .to_string(), + ) + })? + { + let mut headers = header::HeaderMap::new(); + let mut auth_value = header::HeaderValue::from_str(&rustup_authorization_header_value).map_err(|_| { + // The error could contain sensitive data so give a generic error instead. + DownloadError::Message("The `RUSTUP_AUTHORIZATION_HEADER` environment variable set to an invalid HTTP header value.".to_string()) + })?; + auth_value.set_sensitive(true); + headers.insert(header::AUTHORIZATION, auth_value); + client_builder = client_builder.default_headers(headers); + debug!("Added `Authorization` header."); + } + Ok(client_builder) } #[cfg(feature = "reqwest-rustls-tls")] - pub(super) fn rustls_client(timeout: Duration) -> Result<&'static Client, DownloadError> { + pub(super) fn rustls_client( + timeout: Duration, + process: &Process, + ) -> Result<&'static Client, DownloadError> { // If the client is already initialized, the passed timeout is ignored. if let Some(client) = CLIENT_RUSTLS_TLS.get() { return Ok(client); @@ -627,7 +687,7 @@ mod reqwest_be { .with_no_client_auth(); tls_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; - let client = client_generic() + let client = client_generic(process)? .read_timeout(timeout) .use_preconfigured_tls(tls_config) .user_agent(super::REQWEST_RUSTLS_TLS_USER_AGENT) @@ -644,13 +704,16 @@ mod reqwest_be { static CLIENT_RUSTLS_TLS: OnceLock = OnceLock::new(); #[cfg(feature = "reqwest-native-tls")] - pub(super) fn native_tls_client(timeout: Duration) -> Result<&'static Client, DownloadError> { + pub(super) fn native_tls_client( + timeout: Duration, + process: &Process, + ) -> Result<&'static Client, DownloadError> { // If the client is already initialized, the passed timeout is ignored. if let Some(client) = CLIENT_NATIVE_TLS.get() { return Ok(client); } - let client = client_generic() + let client = client_generic(process)? .read_timeout(timeout) .user_agent(super::REQWEST_DEFAULT_TLS_USER_AGENT) .build() diff --git a/src/download/tests.rs b/src/download/tests.rs index dd95c67f02..a9df8de3fc 100644 --- a/src/download/tests.rs +++ b/src/download/tests.rs @@ -26,6 +26,9 @@ mod curl { use super::{scrub_env, serve_file, tmp_dir, write_file}; use crate::download::{Backend, Event}; + #[cfg(feature = "test")] + use crate::process::TestProcess; + #[tokio::test] async fn partially_downloaded_file_gets_resumed_from_byte_offset() { let tmpdir = tmp_dir(); @@ -43,6 +46,7 @@ mod curl { true, None, Duration::from_secs(180), + &TestProcess::default().process, ) .await .expect("Test download failed"); @@ -91,6 +95,7 @@ mod curl { Ok(()) }), Duration::from_secs(180), + &TestProcess::default().process, ) .await .expect("Test download failed"); @@ -120,6 +125,9 @@ mod reqwest { use super::{scrub_env, serve_file, tmp_dir, write_file}; use crate::download::{Backend, Event, TlsBackend}; + #[cfg(feature = "test")] + use crate::process::TestProcess; + // Tests for correctly retrieving the proxy (host, port) tuple from $https_proxy #[tokio::test] async fn read_basic_proxy_params() { @@ -199,6 +207,7 @@ mod reqwest { true, None, Duration::from_secs(180), + &TestProcess::default().process, ) .await .expect("Test download failed"); @@ -247,6 +256,7 @@ mod reqwest { Ok(()) }), Duration::from_secs(180), + &TestProcess::default().process, ) .await .expect("Test download failed"); diff --git a/tests/test-dist-server-resources/ferron-rustup-dist-server.kdl b/tests/test-dist-server-resources/ferron-rustup-dist-server.kdl new file mode 100644 index 0000000000..f22114b211 --- /dev/null +++ b/tests/test-dist-server-resources/ferron-rustup-dist-server.kdl @@ -0,0 +1,38 @@ +// This is for starting a simple proxy that requires basic auth. The only valid credentials will be 'test:123?45>6'. The +// docker command to run is the following (assuming the current working directory is the top-level source directory for rustup). +// +// $ docker run --detach --name ferron-rustup-dist-server 8080:8080 --volume "$(pwd):/mnt/cwd" ferronserver/ferron:2 ferron --config /mnt/cwd/tests/test-dist-server-resources/ferron-rustup-dist-server.kdl +// +// For powershell users, the equivalent command is the following. +// +// > docker run --detach --name ferron-rustup-dist-server --publish 8080:8080 --volume "$(Get-Location):/mnt/cwd" ferronserver/ferron:2 ferron --config /mnt/cwd/tests/test-dist-server-resources/ferron-rustup-dist-server.kdl +// +// Once the server is started, have rustup use it by setting the necessary environment variables. +// +// $ export RUSTUP_DIST_SERVER="http://localhost:8080" +// $ export RUSTUP_UPDATE_ROOT="${RUSTUP_DIST_SERVER}/rustup" +// $ export RUSTUP_AUTHORIZATION_HEADER="Basic $(printf 'test:123?45>6' | base64)" +// +// In powershell, the equivalent commmands are... +// +// > $env:RUSTUP_DIST_SERVER = "http://localhost:8080" +// > $env:RUSTUP_UPDATE_ROOT = "${env:RUSTUP_DIST_SERVER}/rustup" +// > $env:RUSTUP_AUTHORIZATION_HEADER = "Basic $([Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes('test:123?45>6')))" +// +// The 'RUSTUP_LOG' environment variable should also be set so you can see what server is being used for web requests. Once +// testing is done, the server can be stopped using the following command. +// +// $ docker stop ferron-rustup-dist-server && docker rm ferron-rustup-dist-server + +* { + log_date_format "%d/%b/%Y:%H:%M:%S %z" + log_format "{client_ip} - {auth_user} [{timestamp}] \"{method} {path_and_query} {version}\" {status_code} {content_length} \"{header:Referer}\" \"{header:User-Agent}\"" +} + +:8080 { + log "/mnt/cwd/tests/test-dist-server-resources/logs/access.log" + error_log "/mnt/cwd/tests/test-dist-server-resources/logs/error.log" + status 401 regex=".*" users="test" + user "test" "$argon2id$v=19$m=19456,t=2,p=1$emTillHaS3OqFuvITdXxzg$G00heP8QSXk5H/ruTiLt302Xk3uETfU5QO8hBIwUq08" + proxy "https://static.rust-lang.org/" +} diff --git a/tests/test-dist-server-resources/logs/.gitignore b/tests/test-dist-server-resources/logs/.gitignore new file mode 100644 index 0000000000..397b4a7624 --- /dev/null +++ b/tests/test-dist-server-resources/logs/.gitignore @@ -0,0 +1 @@ +*.log From c7abe42d5f198c07f256025ed6dc11aa87bdd74d Mon Sep 17 00:00:00 2001 From: Andres Mejia Sanchez Date: Wed, 7 Jan 2026 22:54:45 -0500 Subject: [PATCH 02/15] Support adding `Proxy-Authorization` headers. --- doc/user-guide/src/environment-variables.md | 4 + src/download/mod.rs | 108 ++++++++++++------ .../ferron-forward-proxy.kdl | 47 ++++++++ .../ferron-rustup-dist-server.kdl | 15 ++- 4 files changed, 132 insertions(+), 42 deletions(-) create mode 100644 tests/test-dist-server-resources/ferron-forward-proxy.kdl diff --git a/doc/user-guide/src/environment-variables.md b/doc/user-guide/src/environment-variables.md index e40f1cc875..b29cc79562 100644 --- a/doc/user-guide/src/environment-variables.md +++ b/doc/user-guide/src/environment-variables.md @@ -34,6 +34,10 @@ environment variable) which requires authentication such as basic username:password credentials or a bearer token. +- `RUSTUP_PROXY_AUTHORIZATION_HEADER` (default: none). This is like the `RUSTUP_AUTHORIZATION_HEADER` except + this will add a `Proxy-Authorization` HTTP header. This is for authenticating to forward + proxies (via the `HTTP_PROXY` or `HTTPS_PROXY`) environment variables. + - `RUSTUP_IO_THREADS` *unstable* (default: reported cpu count, max 8). Sets the number of threads to perform close IO in. Set to `1` to force single-threaded IO for troubleshooting, or an arbitrary number to override diff --git a/src/download/mod.rs b/src/download/mod.rs index 3770e0c0a0..006af852ad 100644 --- a/src/download/mod.rs +++ b/src/download/mod.rs @@ -456,6 +456,30 @@ mod curl { use super::{DownloadError, Event, Process}; + macro_rules! add_header_for_curl_easy_handle { + ($handle:ident, $process:ident, $env_var:literal, $header_name:literal) => { + if let Some(rustup_header_value) = $process.var_opt($env_var).map_err(|error| { + anyhow::anyhow!( + "Internal error getting `{}` environment variable: {}", + $env_var, + anyhow::format_err!(error) + ) + })? { + let mut list = List::new(); + list.append(format!("{}: {}", $header_name, rustup_header_value).as_str()) + .map_err(|_| { + // The error could contain sensitive data so give a generic error instead. + anyhow::anyhow!("Failed to add `{}` HTTP header.", $header_name) + })?; + $handle.http_headers(list).map_err(|_| { + // The error could contain sensitive data so give a generic error instead. + anyhow::anyhow!("Failed to add headers to curl easy handle.") + })?; + debug!("Added `{}` header.", $header_name); + } + }; + } + pub(super) fn download( url: &Url, resume_from: u64, @@ -475,20 +499,18 @@ mod curl { handle.url(url.as_ref())?; handle.follow_location(true)?; handle.useragent(super::CURL_USER_AGENT)?; - if let Some(rustup_authorization_header_value) = process.var_opt("RUSTUP_AUTHORIZATION_HEADER").map_err(|error| { - anyhow::anyhow!("Internal error getting `RUSTUP_AUTHORIZATION_HEADER` environment variable: {}", anyhow::format_err!(error)) - })? { - let mut list = List::new(); - list.append(format!("Authorization: {rustup_authorization_header_value}").as_str()).map_err(|_| { - // The error could contain sensitive data so give a generic error instead. - anyhow::anyhow!("Failed to add `Authorization` HTTP header.") - })?; - handle.http_headers(list).map_err(|_| { - // The error could contain sensitive data so give a generic error instead. - anyhow::anyhow!("Failed to add headers to curl handle.") - })?; - debug!("Added `Authorization` header."); - } + add_header_for_curl_easy_handle!( + handle, + process, + "RUSTUP_AUTHORIZATION_HEADER", + "Authorization" + ); + add_header_for_curl_easy_handle!( + handle, + process, + "RUSTUP_PROXY_AUTHORIZATION_HEADER", + "Proxy-Authorization" + ); if resume_from > 0 { handle.resume_from(resume_from)?; @@ -595,6 +617,32 @@ mod reqwest_be { use super::{DownloadError, Event, Process, debug}; + macro_rules! add_header_for_client_builder { + ($client_builder:ident, $process:ident, $env_var:literal, $header_name:path) => { + if let Some(rustup_header_value) = $process.var_opt($env_var).map_err(|_| { + // The error could contain sensitive data so give a generic error instead. + DownloadError::Message(format!( + "Internal error getting `{}` environment variable", + $env_var + )) + })? { + let mut headers = header::HeaderMap::new(); + let mut auth_value = + header::HeaderValue::from_str(&rustup_header_value).map_err(|_| { + // The error could contain sensitive data so give a generic error instead. + DownloadError::Message(format!( + "The `{}` environment variable set to an invalid HTTP header value.", + $env_var + )) + })?; + auth_value.set_sensitive(true); + headers.insert($header_name, auth_value); + $client_builder = $client_builder.default_headers(headers); + debug!("Added `{}` header.", $header_name); + } + }; + } + pub(super) async fn download( url: &Url, resume_from: u64, @@ -636,26 +684,18 @@ mod reqwest_be { .pool_max_idle_per_host(0) .gzip(false) .proxy(Proxy::custom(env_proxy)); - if let Some(rustup_authorization_header_value) = process - .var_opt("RUSTUP_AUTHORIZATION_HEADER") - .map_err(|_| { - // The error could contain sensitive data so give a generic error instead. - DownloadError::Message( - "Internal error getting `RUSTUP_AUTHORIZATION_HEADER` environment variable" - .to_string(), - ) - })? - { - let mut headers = header::HeaderMap::new(); - let mut auth_value = header::HeaderValue::from_str(&rustup_authorization_header_value).map_err(|_| { - // The error could contain sensitive data so give a generic error instead. - DownloadError::Message("The `RUSTUP_AUTHORIZATION_HEADER` environment variable set to an invalid HTTP header value.".to_string()) - })?; - auth_value.set_sensitive(true); - headers.insert(header::AUTHORIZATION, auth_value); - client_builder = client_builder.default_headers(headers); - debug!("Added `Authorization` header."); - } + add_header_for_client_builder!( + client_builder, + process, + "RUSTUP_AUTHORIZATION_HEADER", + header::AUTHORIZATION + ); + add_header_for_client_builder!( + client_builder, + process, + "RUSTUP_PROXY_AUTHORIZATION_HEADER", + header::PROXY_AUTHORIZATION + ); Ok(client_builder) } diff --git a/tests/test-dist-server-resources/ferron-forward-proxy.kdl b/tests/test-dist-server-resources/ferron-forward-proxy.kdl new file mode 100644 index 0000000000..0622567a1a --- /dev/null +++ b/tests/test-dist-server-resources/ferron-forward-proxy.kdl @@ -0,0 +1,47 @@ +// This is for starting a simple forward proxy that requires basic auth. The only valid credentials will be 'test:123?45>6'. The +// docker command to run is the following (assuming the current working directory is the top-level source directory for rustup). +// +// $ docker run --detach --name ferron-forward-proxy --publish 9080:9080 --volume "$(pwd):/mnt/cwd" ferronserver/ferron:2 ferron --config /mnt/cwd/tests/test-dist-server-resources/ferron-forward-proxy.kdl +// +// For powershell users, the equivalent command is the following. +// +// > docker run --detach --name ferron-forward-proxy --publish 9080:9080 --volume "$(Get-Location):/mnt/cwd" ferronserver/ferron:2 ferron --config /mnt/cwd/tests/test-dist-server-resources/ferron-forward-proxy.kdl +// +// Once the server is started, have rustup use it by setting the necessary environment variables. +// +// $ export ALL_PROXY="http://localhost:9080" +// $ export RUSTUP_PROXY_AUTHORIZATION_HEADER="Basic $(printf 'test:123?45>6' | base64)" +// +// In powershell, the equivalent commmands are... +// +// > $env:ALL_PROXY = "http://localhost:9080" +// > $env:RUSTUP_PROXY_AUTHORIZATION_HEADER = "Basic $([Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes('test:123?45>6')))" +// +// The 'RUSTUP_LOG' environment variable should also be set so you can see what server is being used for web requests. Once +// testing is done, the server can be stopped using the following command. +// +// $ docker stop ferron-forward-proxy && docker rm ferron-forward-proxy + +:9080 { + log_date_format "%d/%b/%Y:%H:%M:%S %z" + log_format "{client_ip} - {auth_user} [{timestamp}] \"{method} {path_and_query} {version}\" {status_code} {content_length} \"{header:Referer}\" \"{header:User-Agent}\"" + log "/mnt/cwd/tests/test-dist-server-resources/logs/forward-proxy-access.log" + error_log "/mnt/cwd/tests/test-dist-server-resources/logs/forward-proxy-error.log" + status 401 users="test" + user "test" "$argon2id$v=19$m=19456,t=2,p=1$emTillHaS3OqFuvITdXxzg$G00heP8QSXk5H/ruTiLt302Xk3uETfU5QO8hBIwUq08" + forward_proxy +} + +// The reverse proxy configuration is added here so that when the RUSTUP_DIST_SERVER is set to point to the test ferron dist server which uses +// 127.0.0.1 or localhost, it will work within the container. +:8080 { + log_date_format "%d/%b/%Y:%H:%M:%S %z" + log_format "{client_ip} - {auth_user} [{timestamp}] \"{method} {path_and_query} {version}\" {status_code} {content_length} \"{header:Referer}\" \"{header:User-Agent}\"" + log "/mnt/cwd/tests/test-dist-server-resources/logs/dist-server-access.log" + error_log "/mnt/cwd/tests/test-dist-server-resources/logs/dist-server-error.log" + status 401 users="test" + user "test" "$argon2id$v=19$m=19456,t=2,p=1$emTillHaS3OqFuvITdXxzg$G00heP8QSXk5H/ruTiLt302Xk3uETfU5QO8hBIwUq08" + proxy "https://static.rust-lang.org/" + proxy_request_header_remove "Authorization" + proxy_request_header_remove "Proxy-Authorization" +} diff --git a/tests/test-dist-server-resources/ferron-rustup-dist-server.kdl b/tests/test-dist-server-resources/ferron-rustup-dist-server.kdl index f22114b211..3055ea919d 100644 --- a/tests/test-dist-server-resources/ferron-rustup-dist-server.kdl +++ b/tests/test-dist-server-resources/ferron-rustup-dist-server.kdl @@ -1,7 +1,7 @@ // This is for starting a simple proxy that requires basic auth. The only valid credentials will be 'test:123?45>6'. The // docker command to run is the following (assuming the current working directory is the top-level source directory for rustup). // -// $ docker run --detach --name ferron-rustup-dist-server 8080:8080 --volume "$(pwd):/mnt/cwd" ferronserver/ferron:2 ferron --config /mnt/cwd/tests/test-dist-server-resources/ferron-rustup-dist-server.kdl +// $ docker run --detach --name ferron-rustup-dist-server --publish 8080:8080 --volume "$(pwd):/mnt/cwd" ferronserver/ferron:2 ferron --config /mnt/cwd/tests/test-dist-server-resources/ferron-rustup-dist-server.kdl // // For powershell users, the equivalent command is the following. // @@ -24,15 +24,14 @@ // // $ docker stop ferron-rustup-dist-server && docker rm ferron-rustup-dist-server -* { +:8080 { log_date_format "%d/%b/%Y:%H:%M:%S %z" log_format "{client_ip} - {auth_user} [{timestamp}] \"{method} {path_and_query} {version}\" {status_code} {content_length} \"{header:Referer}\" \"{header:User-Agent}\"" -} - -:8080 { - log "/mnt/cwd/tests/test-dist-server-resources/logs/access.log" - error_log "/mnt/cwd/tests/test-dist-server-resources/logs/error.log" - status 401 regex=".*" users="test" + log "/mnt/cwd/tests/test-dist-server-resources/logs/dist-server-access.log" + error_log "/mnt/cwd/tests/test-dist-server-resources/logs/dist-server-error.log" + status 401 users="test" user "test" "$argon2id$v=19$m=19456,t=2,p=1$emTillHaS3OqFuvITdXxzg$G00heP8QSXk5H/ruTiLt302Xk3uETfU5QO8hBIwUq08" proxy "https://static.rust-lang.org/" + proxy_request_header_remove "Authorization" + proxy_request_header_remove "Proxy-Authorization" } From 7383220a8de37b1f11f547c7389b49dba4aa76dc Mon Sep 17 00:00:00 2001 From: Andres Mejia Sanchez Date: Thu, 8 Jan 2026 21:43:17 -0500 Subject: [PATCH 03/15] Adjust the ferron config file to document how to properly setup test proxies. --- .../ferron-forward-proxy.kdl | 47 -------------- .../ferron-rustup-dist-server.kdl | 37 ----------- .../rustup-test-dist-server.kdl | 64 +++++++++++++++++++ 3 files changed, 64 insertions(+), 84 deletions(-) delete mode 100644 tests/test-dist-server-resources/ferron-forward-proxy.kdl delete mode 100644 tests/test-dist-server-resources/ferron-rustup-dist-server.kdl create mode 100644 tests/test-dist-server-resources/rustup-test-dist-server.kdl diff --git a/tests/test-dist-server-resources/ferron-forward-proxy.kdl b/tests/test-dist-server-resources/ferron-forward-proxy.kdl deleted file mode 100644 index 0622567a1a..0000000000 --- a/tests/test-dist-server-resources/ferron-forward-proxy.kdl +++ /dev/null @@ -1,47 +0,0 @@ -// This is for starting a simple forward proxy that requires basic auth. The only valid credentials will be 'test:123?45>6'. The -// docker command to run is the following (assuming the current working directory is the top-level source directory for rustup). -// -// $ docker run --detach --name ferron-forward-proxy --publish 9080:9080 --volume "$(pwd):/mnt/cwd" ferronserver/ferron:2 ferron --config /mnt/cwd/tests/test-dist-server-resources/ferron-forward-proxy.kdl -// -// For powershell users, the equivalent command is the following. -// -// > docker run --detach --name ferron-forward-proxy --publish 9080:9080 --volume "$(Get-Location):/mnt/cwd" ferronserver/ferron:2 ferron --config /mnt/cwd/tests/test-dist-server-resources/ferron-forward-proxy.kdl -// -// Once the server is started, have rustup use it by setting the necessary environment variables. -// -// $ export ALL_PROXY="http://localhost:9080" -// $ export RUSTUP_PROXY_AUTHORIZATION_HEADER="Basic $(printf 'test:123?45>6' | base64)" -// -// In powershell, the equivalent commmands are... -// -// > $env:ALL_PROXY = "http://localhost:9080" -// > $env:RUSTUP_PROXY_AUTHORIZATION_HEADER = "Basic $([Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes('test:123?45>6')))" -// -// The 'RUSTUP_LOG' environment variable should also be set so you can see what server is being used for web requests. Once -// testing is done, the server can be stopped using the following command. -// -// $ docker stop ferron-forward-proxy && docker rm ferron-forward-proxy - -:9080 { - log_date_format "%d/%b/%Y:%H:%M:%S %z" - log_format "{client_ip} - {auth_user} [{timestamp}] \"{method} {path_and_query} {version}\" {status_code} {content_length} \"{header:Referer}\" \"{header:User-Agent}\"" - log "/mnt/cwd/tests/test-dist-server-resources/logs/forward-proxy-access.log" - error_log "/mnt/cwd/tests/test-dist-server-resources/logs/forward-proxy-error.log" - status 401 users="test" - user "test" "$argon2id$v=19$m=19456,t=2,p=1$emTillHaS3OqFuvITdXxzg$G00heP8QSXk5H/ruTiLt302Xk3uETfU5QO8hBIwUq08" - forward_proxy -} - -// The reverse proxy configuration is added here so that when the RUSTUP_DIST_SERVER is set to point to the test ferron dist server which uses -// 127.0.0.1 or localhost, it will work within the container. -:8080 { - log_date_format "%d/%b/%Y:%H:%M:%S %z" - log_format "{client_ip} - {auth_user} [{timestamp}] \"{method} {path_and_query} {version}\" {status_code} {content_length} \"{header:Referer}\" \"{header:User-Agent}\"" - log "/mnt/cwd/tests/test-dist-server-resources/logs/dist-server-access.log" - error_log "/mnt/cwd/tests/test-dist-server-resources/logs/dist-server-error.log" - status 401 users="test" - user "test" "$argon2id$v=19$m=19456,t=2,p=1$emTillHaS3OqFuvITdXxzg$G00heP8QSXk5H/ruTiLt302Xk3uETfU5QO8hBIwUq08" - proxy "https://static.rust-lang.org/" - proxy_request_header_remove "Authorization" - proxy_request_header_remove "Proxy-Authorization" -} diff --git a/tests/test-dist-server-resources/ferron-rustup-dist-server.kdl b/tests/test-dist-server-resources/ferron-rustup-dist-server.kdl deleted file mode 100644 index 3055ea919d..0000000000 --- a/tests/test-dist-server-resources/ferron-rustup-dist-server.kdl +++ /dev/null @@ -1,37 +0,0 @@ -// This is for starting a simple proxy that requires basic auth. The only valid credentials will be 'test:123?45>6'. The -// docker command to run is the following (assuming the current working directory is the top-level source directory for rustup). -// -// $ docker run --detach --name ferron-rustup-dist-server --publish 8080:8080 --volume "$(pwd):/mnt/cwd" ferronserver/ferron:2 ferron --config /mnt/cwd/tests/test-dist-server-resources/ferron-rustup-dist-server.kdl -// -// For powershell users, the equivalent command is the following. -// -// > docker run --detach --name ferron-rustup-dist-server --publish 8080:8080 --volume "$(Get-Location):/mnt/cwd" ferronserver/ferron:2 ferron --config /mnt/cwd/tests/test-dist-server-resources/ferron-rustup-dist-server.kdl -// -// Once the server is started, have rustup use it by setting the necessary environment variables. -// -// $ export RUSTUP_DIST_SERVER="http://localhost:8080" -// $ export RUSTUP_UPDATE_ROOT="${RUSTUP_DIST_SERVER}/rustup" -// $ export RUSTUP_AUTHORIZATION_HEADER="Basic $(printf 'test:123?45>6' | base64)" -// -// In powershell, the equivalent commmands are... -// -// > $env:RUSTUP_DIST_SERVER = "http://localhost:8080" -// > $env:RUSTUP_UPDATE_ROOT = "${env:RUSTUP_DIST_SERVER}/rustup" -// > $env:RUSTUP_AUTHORIZATION_HEADER = "Basic $([Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes('test:123?45>6')))" -// -// The 'RUSTUP_LOG' environment variable should also be set so you can see what server is being used for web requests. Once -// testing is done, the server can be stopped using the following command. -// -// $ docker stop ferron-rustup-dist-server && docker rm ferron-rustup-dist-server - -:8080 { - log_date_format "%d/%b/%Y:%H:%M:%S %z" - log_format "{client_ip} - {auth_user} [{timestamp}] \"{method} {path_and_query} {version}\" {status_code} {content_length} \"{header:Referer}\" \"{header:User-Agent}\"" - log "/mnt/cwd/tests/test-dist-server-resources/logs/dist-server-access.log" - error_log "/mnt/cwd/tests/test-dist-server-resources/logs/dist-server-error.log" - status 401 users="test" - user "test" "$argon2id$v=19$m=19456,t=2,p=1$emTillHaS3OqFuvITdXxzg$G00heP8QSXk5H/ruTiLt302Xk3uETfU5QO8hBIwUq08" - proxy "https://static.rust-lang.org/" - proxy_request_header_remove "Authorization" - proxy_request_header_remove "Proxy-Authorization" -} diff --git a/tests/test-dist-server-resources/rustup-test-dist-server.kdl b/tests/test-dist-server-resources/rustup-test-dist-server.kdl new file mode 100644 index 0000000000..3d5a968842 --- /dev/null +++ b/tests/test-dist-server-resources/rustup-test-dist-server.kdl @@ -0,0 +1,64 @@ +// This is for starting a simple test distribution server that requires basic auth. The only valid credentials will be 'test:123?45>6'. The +// docker command to run is the following (assuming the current working directory is the top-level source directory for rustup). +// +// First, create a test network. +// +// $ docker network create rustup-test-network +// +// Now create the test distribution server. +// +// $ docker run --detach --name rustup-test-dist-server --net rustup-test-network --publish 8080:8080 --volume "$(pwd):/mnt/cwd" ferronserver/ferron:2 ferron --config /mnt/cwd/tests/test-dist-server-resources/rustup-test-dist-server.kdl +// +// For powershell users, the equivalent command is the following. +// +// > docker run --detach --name rustup-test-dist-server --net rustup-test-network --publish 8080:8080 --volume "$(Get-Location):/mnt/cwd" ferronserver/ferron:2 ferron --config /mnt/cwd/tests/test-dist-server-resources/rustup-test-dist-server.kdl +// +// Once the server is started, have rustup use it by setting the necessary environment variables. +// +// $ export RUSTUP_DIST_SERVER="http://localhost:8080" +// $ export RUSTUP_UPDATE_ROOT="${RUSTUP_DIST_SERVER}/rustup" +// $ export RUSTUP_AUTHORIZATION_HEADER="Basic $(printf 'test:123?45>6' | base64)" +// +// In powershell, the equivalent commmands are... +// +// > $env:RUSTUP_DIST_SERVER = "http://localhost:8080" +// > $env:RUSTUP_UPDATE_ROOT = "${env:RUSTUP_DIST_SERVER}/rustup" +// > $env:RUSTUP_AUTHORIZATION_HEADER = "Basic $([Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes('test:123?45>6')))" +// +// The 'RUSTUP_LOG' environment variable should also be set so you can see what server is being used for web requests. Once +// testing is done, the server can be stopped using the following command. +// +// $ docker stop rustup-test-dist-server && docker rm rustup-test-dist-server +// +// For testing support to set the `Proxy-Authorization` header, ensure the test distribution server is running using the same docker command +// as before. Then set these environment variables. +// +// $ export ALL_PROXY="http://127.0.0.1:9080" +// $ export RUSTUP_DIST_SERVER="http://rustup-test-dist-server:8080" +// $ export RUSTUP_UPDATE_ROOT="${RUSTUP_DIST_SERVER}/rustup" +// +// In powershell, equivalent commands are... +// +// > $env:ALL_PROXY = "http://127.0.0.1:9080" +// > $env:RUSTUP_DIST_SERVER = "http://rustup-test-dist-server:8080" +// > $env:RUSTUP_UPDATE_ROOT = "${env:RUSTUP_DIST_SERVER}/rustup" +// +// Finally, start the test forward proxy as follows. +// +// $ docker run --detach --name rustup-test-forward-proxy --net rustup-test-network --interactive --tty --publish 9080:9080 mitmproxy/mitmproxy mitmdump --mode regular@0.0.0.0:9080 --proxyauth 'test:123?45>6' +// +// To stop and remove the test forward proxy container, run the following. +// +// $ docker stop rustup-test-forward-proxy && docker rm rustup-test-forward-proxy + +:8080 { + log_date_format "%d/%b/%Y:%H:%M:%S %z" + log_format "{client_ip} - {auth_user} [{timestamp}] \"{method} {path_and_query} {version}\" {status_code} {content_length} \"{header:Referer}\" \"{header:User-Agent}\"" + log "/mnt/cwd/tests/test-dist-server-resources/logs/dist-server-access.log" + error_log "/mnt/cwd/tests/test-dist-server-resources/logs/dist-server-error.log" + status 401 users="test" + user "test" "$argon2id$v=19$m=19456,t=2,p=1$emTillHaS3OqFuvITdXxzg$G00heP8QSXk5H/ruTiLt302Xk3uETfU5QO8hBIwUq08" + proxy "https://static.rust-lang.org/" + proxy_request_header_remove "Authorization" + proxy_request_header_remove "Proxy-Authorization" +} From cdbb975cc18f95e6b6f830dcdc03255b7da7e5d9 Mon Sep 17 00:00:00 2001 From: Andres Mejia Sanchez Date: Sat, 10 Jan 2026 17:59:19 -0500 Subject: [PATCH 04/15] Rename directory with test resources. --- .../logs/.gitignore | 0 .../rustup-test-dist-server.kdl | 8 ++++---- 2 files changed, 4 insertions(+), 4 deletions(-) rename tests/{test-dist-server-resources => resources}/logs/.gitignore (100%) rename tests/{test-dist-server-resources => resources}/rustup-test-dist-server.kdl (89%) diff --git a/tests/test-dist-server-resources/logs/.gitignore b/tests/resources/logs/.gitignore similarity index 100% rename from tests/test-dist-server-resources/logs/.gitignore rename to tests/resources/logs/.gitignore diff --git a/tests/test-dist-server-resources/rustup-test-dist-server.kdl b/tests/resources/rustup-test-dist-server.kdl similarity index 89% rename from tests/test-dist-server-resources/rustup-test-dist-server.kdl rename to tests/resources/rustup-test-dist-server.kdl index 3d5a968842..40fb84d5da 100644 --- a/tests/test-dist-server-resources/rustup-test-dist-server.kdl +++ b/tests/resources/rustup-test-dist-server.kdl @@ -7,11 +7,11 @@ // // Now create the test distribution server. // -// $ docker run --detach --name rustup-test-dist-server --net rustup-test-network --publish 8080:8080 --volume "$(pwd):/mnt/cwd" ferronserver/ferron:2 ferron --config /mnt/cwd/tests/test-dist-server-resources/rustup-test-dist-server.kdl +// $ docker run --detach --name rustup-test-dist-server --net rustup-test-network --publish 8080:8080 --volume "$(pwd):/mnt/cwd" ferronserver/ferron:2 ferron --config /mnt/cwd/tests/resources/rustup-test-dist-server.kdl // // For powershell users, the equivalent command is the following. // -// > docker run --detach --name rustup-test-dist-server --net rustup-test-network --publish 8080:8080 --volume "$(Get-Location):/mnt/cwd" ferronserver/ferron:2 ferron --config /mnt/cwd/tests/test-dist-server-resources/rustup-test-dist-server.kdl +// > docker run --detach --name rustup-test-dist-server --net rustup-test-network --publish 8080:8080 --volume "$(Get-Location):/mnt/cwd" ferronserver/ferron:2 ferron --config /mnt/cwd/tests/resources/rustup-test-dist-server.kdl // // Once the server is started, have rustup use it by setting the necessary environment variables. // @@ -54,8 +54,8 @@ :8080 { log_date_format "%d/%b/%Y:%H:%M:%S %z" log_format "{client_ip} - {auth_user} [{timestamp}] \"{method} {path_and_query} {version}\" {status_code} {content_length} \"{header:Referer}\" \"{header:User-Agent}\"" - log "/mnt/cwd/tests/test-dist-server-resources/logs/dist-server-access.log" - error_log "/mnt/cwd/tests/test-dist-server-resources/logs/dist-server-error.log" + log "/mnt/cwd/tests/resources/logs/dist-server-access.log" + error_log "/mnt/cwd/tests/resources/logs/dist-server-error.log" status 401 users="test" user "test" "$argon2id$v=19$m=19456,t=2,p=1$emTillHaS3OqFuvITdXxzg$G00heP8QSXk5H/ruTiLt302Xk3uETfU5QO8hBIwUq08" proxy "https://static.rust-lang.org/" From 95106f2397a1840d21eb1a19a9d350d04803aa81 Mon Sep 17 00:00:00 2001 From: Andres Mejia Sanchez Date: Sat, 10 Jan 2026 18:48:46 -0500 Subject: [PATCH 05/15] Add environment variable needed to set the 'Proxy-Authorization' HTTP header. --- tests/resources/rustup-test-dist-server.kdl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/resources/rustup-test-dist-server.kdl b/tests/resources/rustup-test-dist-server.kdl index 40fb84d5da..e2890837bc 100644 --- a/tests/resources/rustup-test-dist-server.kdl +++ b/tests/resources/rustup-test-dist-server.kdl @@ -34,12 +34,14 @@ // as before. Then set these environment variables. // // $ export ALL_PROXY="http://127.0.0.1:9080" +// $ export RUSTUP_PROXY_AUTHORIZATION_HEADER="Basic $(printf 'test:123?45>6' | base64)" // $ export RUSTUP_DIST_SERVER="http://rustup-test-dist-server:8080" // $ export RUSTUP_UPDATE_ROOT="${RUSTUP_DIST_SERVER}/rustup" // // In powershell, equivalent commands are... // // > $env:ALL_PROXY = "http://127.0.0.1:9080" +// > $env:RUSTUP_PROXY_AUTHORIZATION_HEADER = "Basic $([Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes('test:123?45>6')))" // > $env:RUSTUP_DIST_SERVER = "http://rustup-test-dist-server:8080" // > $env:RUSTUP_UPDATE_ROOT = "${env:RUSTUP_DIST_SERVER}/rustup" // From 8ee988b766cef310e7374057bf42b71fe8d75e43 Mon Sep 17 00:00:00 2001 From: Andres Mejia Sanchez Date: Sun, 11 Jan 2026 13:01:15 -0500 Subject: [PATCH 06/15] Change logs to simply be output to stderr. --- tests/resources/logs/.gitignore | 1 - tests/resources/rustup-test-dist-server.kdl | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) delete mode 100644 tests/resources/logs/.gitignore diff --git a/tests/resources/logs/.gitignore b/tests/resources/logs/.gitignore deleted file mode 100644 index 397b4a7624..0000000000 --- a/tests/resources/logs/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.log diff --git a/tests/resources/rustup-test-dist-server.kdl b/tests/resources/rustup-test-dist-server.kdl index e2890837bc..fe725291e7 100644 --- a/tests/resources/rustup-test-dist-server.kdl +++ b/tests/resources/rustup-test-dist-server.kdl @@ -56,8 +56,8 @@ :8080 { log_date_format "%d/%b/%Y:%H:%M:%S %z" log_format "{client_ip} - {auth_user} [{timestamp}] \"{method} {path_and_query} {version}\" {status_code} {content_length} \"{header:Referer}\" \"{header:User-Agent}\"" - log "/mnt/cwd/tests/resources/logs/dist-server-access.log" - error_log "/mnt/cwd/tests/resources/logs/dist-server-error.log" + log "/dev/stderr" + error_log "/dev/stderr" status 401 users="test" user "test" "$argon2id$v=19$m=19456,t=2,p=1$emTillHaS3OqFuvITdXxzg$G00heP8QSXk5H/ruTiLt302Xk3uETfU5QO8hBIwUq08" proxy "https://static.rust-lang.org/" From a30052f98502f3d0235d09792ff78ac2a165fa10 Mon Sep 17 00:00:00 2001 From: Andres Mejia Sanchez Date: Sun, 11 Jan 2026 22:44:44 -0500 Subject: [PATCH 07/15] Write integration tests for the Authorization and Proxy-Authorization HTTP headers support. --- Cargo.toml | 3 + src/test.rs | 2 +- src/test/clitools.rs | 277 ++++++++++++++++++++ tests/README.md | 85 ++++++ tests/resources/rustup-test-dist-server.kdl | 58 +--- tests/suite/cli_test_with_containers.rs | 273 +++++++++++++++++++ tests/suite/mod.rs | 1 + 7 files changed, 643 insertions(+), 56 deletions(-) create mode 100644 tests/README.md create mode 100644 tests/suite/cli_test_with_containers.rs diff --git a/Cargo.toml b/Cargo.toml index 4efa1efee1..5082a5ebaa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,9 @@ otel = [ # Exports code dependent on private interfaces for the integration test suite test = ["dep:snapbox", "dep:walkdir", "clap-cargo/testing_colors"] +# Run the tests that require containers. +test-with-containers = ["test"] + # Sorted by alphabetic order [dependencies] anstream = "0.6.20" diff --git a/src/test.rs b/src/test.rs index 99e57e275b..e1ff847d92 100644 --- a/src/test.rs +++ b/src/test.rs @@ -29,7 +29,7 @@ pub use crate::cli::self_update::{RegistryGuard, RegistryValueId, USER_PATH, get mod clitools; pub use clitools::{ Assert, CliTestContext, Config, SanitizedOutput, Scenario, SelfUpdateTestContext, - output_release_file, print_command, print_indented, + TestContainer, TestContainerContext, output_release_file, print_command, print_indented, }; pub(crate) mod dist; pub use dist::DistContext; diff --git a/src/test/clitools.rs b/src/test/clitools.rs index 17bcf923b8..bfb11671ac 100644 --- a/src/test/clitools.rs +++ b/src/test/clitools.rs @@ -1253,3 +1253,280 @@ where } inner(original.as_ref(), link.as_ref()) } + +const DEFAULT_RUSTUP_TEST_NETWORK_NAME: &str = "rustup-test-network"; + +const RUSTUP_TEST_DIST_SERVER_CONFIG: &str = + include_str!("../../tests/resources/rustup-test-dist-server.kdl"); + +// This is used for ensuring one thread ensures the test network is created. +static DOCKER_NETWORK_CREATE_LOCK: LazyLock> = LazyLock::new(|| RwLock::new(())); + +static DEFAULT_RUSTUP_TEST_DOCKER_PROGRAM: LazyLock = + LazyLock::new(|| format!("docker{EXE_SUFFIX}")); + +#[derive(Debug)] +pub enum TestContainer { + DistServer, + ForwardProxy, +} + +impl std::fmt::Display for TestContainer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TestContainer::DistServer => { + write!(f, "rustup-test-dist-server") + } + TestContainer::ForwardProxy => { + write!(f, "rustup-test-forward-proxy") + } + } + } +} + +pub struct TestContainerContext { + container: TestContainer, + docker_program: std::ffi::OsString, + leave_container_running: bool, + cli_test_context: tokio::sync::Mutex>, +} + +impl Drop for TestContainerContext { + fn drop(&mut self) { + self.cleanup_container().unwrap(); + } +} + +impl TestContainerContext { + pub fn new(process: &process::Process, container: TestContainer) -> Self { + let docker_program = process + .var_os("RUSTUP_TEST_DOCKER_PROGRAM") + .unwrap_or((*DEFAULT_RUSTUP_TEST_DOCKER_PROGRAM).as_str().into()); + let leave_container_running = process + .var_opt("RUSTUP_TEST_LEAVE_CONTAINERS_RUNNING") + .unwrap() + .map(|var| var.eq("true")) + .unwrap_or_default(); + let cli_test_context = tokio::sync::Mutex::new(None); + Self { + container, + docker_program, + leave_container_running, + cli_test_context, + } + } + + async fn ensure_initialized(&mut self) { + let mut cli_test_context_guard = self.cli_test_context.lock().await; + if cli_test_context_guard.is_none() { + let test_version = "2.0.0"; + let mut cli_test_context = CliTestContext::new(Scenario::SimpleV2).await; + let _dist_guard = cli_test_context.with_update_server(test_version); + if self.leave_container_running { + cli_test_context.config.test_dist_dir.disable_cleanup(true); + } + cli_test_context_guard.replace(cli_test_context); + } + } + + pub async fn run(&mut self) -> anyhow::Result<()> { + if !self.is_running() { + self.ensure_network_created().unwrap(); + self.ensure_initialized().await; + let cli_test_context_guard = self.cli_test_context.lock().await; + let cli_test_context = cli_test_context_guard.as_ref().unwrap(); + match self.container { + TestContainer::DistServer => { + let temp_dir_string = cli_test_context + .config + .test_dist_dir + .path() + .to_string_lossy() + .into_owned(); + let re = regex::Regex::new(r"(?sm)\n// BEGIN VARIABLE CONFIG SECTION\n.*\n// END VARIABLE CONFIG SECTION\n").unwrap(); + let config = re + .replace_all( + RUSTUP_TEST_DIST_SERVER_CONFIG, + "\n root \"/mnt/rustup-test-temp-dir\"\n", + ) + .to_string(); + tokio::fs::write( + cli_test_context + .config + .test_dist_dir + .path() + .join("rustup-test-dist-server.kdl"), + config, + ) + .await + .unwrap(); + let mut command = Command::new(&self.docker_program); + let exit_status = command + .args([ + "run", + "--detach", + "--name", + format!("{}", self.container).as_str(), + "--net", + DEFAULT_RUSTUP_TEST_NETWORK_NAME, + "--publish", + "8080:8080", + "--volume", + format!("{temp_dir_string}:/mnt/rustup-test-temp-dir").as_str(), + "ferronserver/ferron:2-debian", + "ferron", + "--config", + "/mnt/rustup-test-temp-dir/rustup-test-dist-server.kdl", + ]) + .spawn() + .unwrap() + .wait() + .unwrap(); + if !exit_status.success() { + let msg = format!( + "A problem occurred attempting to start the {} container.", + self.container + ); + tracing::error!("{msg}"); + Err(anyhow::anyhow!(msg)) + } else { + Ok(()) + } + } + TestContainer::ForwardProxy => { + let mut command = Command::new(&self.docker_program); + let exit_status = command + .args([ + "run", + "--detach", + "--name", + format!("{}", self.container).as_str(), + "--net", + DEFAULT_RUSTUP_TEST_NETWORK_NAME, + "--publish", + "9080:9080", + "mitmproxy/mitmproxy", + "mitmdump", + "--mode", + "regular@0.0.0.0:9080", + "--proxyauth", + "test:123?45>6", + ]) + .spawn() + .unwrap() + .wait() + .unwrap(); + if !exit_status.success() { + let msg = format!( + "A problem occurred attempting to start the {} container.", + self.container + ); + tracing::error!("{msg}"); + Err(anyhow::anyhow!(msg)) + } else { + Ok(()) + } + } + } + } else { + Ok(()) + } + } + + fn ensure_network_created(&self) -> anyhow::Result<()> { + let _guard = (*DOCKER_NETWORK_CREATE_LOCK).write().unwrap(); + let mut command = Command::new(&self.docker_program); + let output = command + .stdout(std::process::Stdio::piped()) + .args([ + "network", + "ls", + "--format", + "json", + "--filter", + format!("name={DEFAULT_RUSTUP_TEST_NETWORK_NAME}").as_str(), + ]) + .output() + .unwrap(); + if output.stdout.is_empty() { + let mut command = Command::new(&self.docker_program); + let exit_status = command + .args(["network", "create", DEFAULT_RUSTUP_TEST_NETWORK_NAME]) + .spawn() + .unwrap() + .wait() + .unwrap(); + if !exit_status.success() { + let msg = format!( + "A problem occurred attempting to ensure '{DEFAULT_RUSTUP_TEST_NETWORK_NAME}' network is created." + ); + tracing::error!("{msg}"); + Err(anyhow::anyhow!(msg)) + } else { + Ok(()) + } + } else { + Ok(()) + } + } + + fn is_running(&self) -> bool { + let mut command = Command::new(&self.docker_program); + let output = command + .stdout(std::process::Stdio::piped()) + .args([ + "ps", + "--format", + "json", + "--filter", + format!("name={}", self.container).as_str(), + ]) + .output() + .unwrap(); + !output.stdout.is_empty() + } + + fn cleanup_container(&self) -> anyhow::Result<()> { + if !self.leave_container_running { + if self.is_running() { + let mut command = Command::new(&self.docker_program); + let exit_status = command + .args(["stop", format!("{}", self.container).as_str()]) + .spawn() + .unwrap() + .wait() + .unwrap(); + if !exit_status.success() { + let msg = format!( + "A problem occurred attempting to stop the {} container.", + self.container + ); + tracing::error!("{msg}"); + Err(anyhow::anyhow!(msg)) + } else { + let mut command = Command::new(&self.docker_program); + let exit_status = command + .args(["rm", format!("{}", self.container).as_str()]) + .spawn() + .unwrap() + .wait() + .unwrap(); + if !exit_status.success() { + let msg = format!( + "A problem occurred attempting to remove the {} container.", + self.container + ); + tracing::error!("{msg}"); + Err(anyhow::anyhow!(msg)) + } else { + Ok(()) + } + } + } else { + Ok(()) + } + } else { + Ok(()) + } + } +} diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000000..8eae1013b4 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,85 @@ +# Test containers + +Test containers are for validating functionality when needing to use a distribution server and/or forward proxy that require +authentication. For any of the test servers, the only valid credentials will be the username:password creds 'test:123?45>6'. +The tests requiring containers will be ran when the 'test-with-containers' feature is enabled. By default, the containers are +started with the 'docker' program on non-Windows machines and 'docker.exe' on Windows machines. The program can be changed +to a different program or a full path to a program using the `RUSTUP_TEST_DOCKER_PROGRAM` environment variable. As an example, +if you need to use podman, run the following. + +```sh +export RUSTUP_TEST_DOCKER_PROGRAM="podman" +``` + +## Manual startup of test containers. + +The test containers can be started and left running by running any of the container tests and setting the +`RUSTUP_TEST_LEAVE_CONTAINERS_RUNNING` environment variable to the case-sensitive string `true`. You can also +run the following to quickly start the containers. + +```sh +export RUSTUP_TEST_LEAVE_CONTAINERS_RUNNING="true" +cargo test --features test-with-containers -- suite::cli_test_with_containers::tests::test_start_containers +``` + +Or in powershell... + +```powershell +$env:RUSTUP_TEST_LEAVE_CONTAINERS_RUNNING = "true" +cargo test --features test-with-containers -- suite::cli_test_with_containers::tests::test_start_containers +``` + +Use the docker command to determine what host port is being used for each of the containers. For example... + +```sh +docker ps --format json --filter "name=rustup-test-dist-server" | jq '.["Ports"]' +docker ps --format json --filter "name=rustup-test-forward-proxy" | jq '.["Ports"]' +``` + +When using the test containers, set the following environment variables. + +```sh +export RUSTUP_DIST_SERVER="http://localhost:8080" +export RUSTUP_UPDATE_ROOT="${RUSTUP_DIST_SERVER}/rustup" +export RUSTUP_AUTHORIZATION_HEADER="Basic $(printf 'test:123?45>6' | base64)" +export RUSTUP_HOME="${HOME}/test-rustup-init" +export CARGO_HOME="${RUSTUP_HOME}" +``` + +In powershell, the equivalent commmands are... + +```powershell +$env:RUSTUP_DIST_SERVER = "http://localhost:8080" +$env:RUSTUP_UPDATE_ROOT = "${env:RUSTUP_DIST_SERVER}/rustup" +$env:RUSTUP_AUTHORIZATION_HEADER = "Basic $([Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes('test:123?45>6')))" +$env:RUSTUP_HOME = "${env:USERPROFILE}/test-rustup-init" +$env:CARGO_HOME = "${env:RUSTUP_HOME}" +``` + +The 'RUSTUP_LOG' environment variable should also be set so you can see what server is being used for web requests. + +For testing support to set the `Proxy-Authorization` header, set these environment variables. + +```sh +export ALL_PROXY="http://127.0.0.1:9080" +export RUSTUP_PROXY_AUTHORIZATION_HEADER="Basic $(printf 'test:123?45>6' | base64)" +export RUSTUP_DIST_SERVER="http://rustup-test-dist-server:8080" +export RUSTUP_UPDATE_ROOT="${RUSTUP_DIST_SERVER}/rustup" +``` + +In powershell, equivalent commands are... + +```powershell +$env:ALL_PROXY = "http://127.0.0.1:9080" +$env:RUSTUP_PROXY_AUTHORIZATION_HEADER = "Basic $([Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes('test:123?45>6')))" +$env:RUSTUP_DIST_SERVER = "http://rustup-test-dist-server:8080" +$env:RUSTUP_UPDATE_ROOT = "${env:RUSTUP_DIST_SERVER}/rustup" +``` + +To stop and remove the test containers, run the following. + +```sh +docker stop rustup-test-dist-server && docker rm rustup-test-dist-server +docker stop rustup-test-forward-proxy && docker rm rustup-test-forward-proxy +``` + diff --git a/tests/resources/rustup-test-dist-server.kdl b/tests/resources/rustup-test-dist-server.kdl index fe725291e7..8122750745 100644 --- a/tests/resources/rustup-test-dist-server.kdl +++ b/tests/resources/rustup-test-dist-server.kdl @@ -1,58 +1,4 @@ -// This is for starting a simple test distribution server that requires basic auth. The only valid credentials will be 'test:123?45>6'. The -// docker command to run is the following (assuming the current working directory is the top-level source directory for rustup). -// -// First, create a test network. -// -// $ docker network create rustup-test-network -// -// Now create the test distribution server. -// -// $ docker run --detach --name rustup-test-dist-server --net rustup-test-network --publish 8080:8080 --volume "$(pwd):/mnt/cwd" ferronserver/ferron:2 ferron --config /mnt/cwd/tests/resources/rustup-test-dist-server.kdl -// -// For powershell users, the equivalent command is the following. -// -// > docker run --detach --name rustup-test-dist-server --net rustup-test-network --publish 8080:8080 --volume "$(Get-Location):/mnt/cwd" ferronserver/ferron:2 ferron --config /mnt/cwd/tests/resources/rustup-test-dist-server.kdl -// -// Once the server is started, have rustup use it by setting the necessary environment variables. -// -// $ export RUSTUP_DIST_SERVER="http://localhost:8080" -// $ export RUSTUP_UPDATE_ROOT="${RUSTUP_DIST_SERVER}/rustup" -// $ export RUSTUP_AUTHORIZATION_HEADER="Basic $(printf 'test:123?45>6' | base64)" -// -// In powershell, the equivalent commmands are... -// -// > $env:RUSTUP_DIST_SERVER = "http://localhost:8080" -// > $env:RUSTUP_UPDATE_ROOT = "${env:RUSTUP_DIST_SERVER}/rustup" -// > $env:RUSTUP_AUTHORIZATION_HEADER = "Basic $([Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes('test:123?45>6')))" -// -// The 'RUSTUP_LOG' environment variable should also be set so you can see what server is being used for web requests. Once -// testing is done, the server can be stopped using the following command. -// -// $ docker stop rustup-test-dist-server && docker rm rustup-test-dist-server -// -// For testing support to set the `Proxy-Authorization` header, ensure the test distribution server is running using the same docker command -// as before. Then set these environment variables. -// -// $ export ALL_PROXY="http://127.0.0.1:9080" -// $ export RUSTUP_PROXY_AUTHORIZATION_HEADER="Basic $(printf 'test:123?45>6' | base64)" -// $ export RUSTUP_DIST_SERVER="http://rustup-test-dist-server:8080" -// $ export RUSTUP_UPDATE_ROOT="${RUSTUP_DIST_SERVER}/rustup" -// -// In powershell, equivalent commands are... -// -// > $env:ALL_PROXY = "http://127.0.0.1:9080" -// > $env:RUSTUP_PROXY_AUTHORIZATION_HEADER = "Basic $([Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes('test:123?45>6')))" -// > $env:RUSTUP_DIST_SERVER = "http://rustup-test-dist-server:8080" -// > $env:RUSTUP_UPDATE_ROOT = "${env:RUSTUP_DIST_SERVER}/rustup" -// -// Finally, start the test forward proxy as follows. -// -// $ docker run --detach --name rustup-test-forward-proxy --net rustup-test-network --interactive --tty --publish 9080:9080 mitmproxy/mitmproxy mitmdump --mode regular@0.0.0.0:9080 --proxyauth 'test:123?45>6' -// -// To stop and remove the test forward proxy container, run the following. -// -// $ docker stop rustup-test-forward-proxy && docker rm rustup-test-forward-proxy - +// Ferron configuration to start simple test distribution server. See the README.md for details about this file. :8080 { log_date_format "%d/%b/%Y:%H:%M:%S %z" log_format "{client_ip} - {auth_user} [{timestamp}] \"{method} {path_and_query} {version}\" {status_code} {content_length} \"{header:Referer}\" \"{header:User-Agent}\"" @@ -60,7 +6,9 @@ error_log "/dev/stderr" status 401 users="test" user "test" "$argon2id$v=19$m=19456,t=2,p=1$emTillHaS3OqFuvITdXxzg$G00heP8QSXk5H/ruTiLt302Xk3uETfU5QO8hBIwUq08" +// BEGIN VARIABLE CONFIG SECTION proxy "https://static.rust-lang.org/" proxy_request_header_remove "Authorization" proxy_request_header_remove "Proxy-Authorization" +// END VARIABLE CONFIG SECTION } diff --git a/tests/suite/cli_test_with_containers.rs b/tests/suite/cli_test_with_containers.rs new file mode 100644 index 0000000000..bdd9c1ab44 --- /dev/null +++ b/tests/suite/cli_test_with_containers.rs @@ -0,0 +1,273 @@ +#[cfg(feature = "test-with-containers")] +mod tests { + use std::{ + env::consts::EXE_SUFFIX, + process::{Command, Stdio}, + sync::{Arc, LazyLock, Mutex}, + }; + + use rustup::{ + process::Process, + test::{CliTestContext, Scenario, TestContainer, TestContainerContext}, + }; + + // TODO: Figure out how to programmatically determine the published ports in running containers and then + // use that to set the 'RUSTUP_DIST_SERVER' and 'ALL_PROXY' environment variables. + const RUSTUP_TEST_DIST_SERVER: &str = "http://localhost:8080"; + const RUSTUP_TEST_FORWARD_PROXY: &str = "http://localhost:9080"; + + static RUSTUP_TEST_DIST_SERVER_CONTAINER_CONTEXT: LazyLock>> = + LazyLock::new(|| { + let process = Process::os(); + Arc::new(Mutex::new(TestContainerContext::new( + &process, + TestContainer::DistServer, + ))) + }); + static RUSTUP_TEST_FORWARD_PROXY_CONTAINER_CONTEXT: LazyLock>> = + LazyLock::new(|| { + let process = Process::os(); + Arc::new(Mutex::new(TestContainerContext::new( + &process, + TestContainer::ForwardProxy, + ))) + }); + + async fn start_containers() { + let mut test_dist_server_container_context_guard = + (*RUSTUP_TEST_DIST_SERVER_CONTAINER_CONTEXT).lock().unwrap(); + test_dist_server_container_context_guard + .run() + .await + .unwrap(); + let mut test_forward_proxy_container_context_guard = + (*RUSTUP_TEST_FORWARD_PROXY_CONTAINER_CONTEXT) + .lock() + .unwrap(); + test_forward_proxy_container_context_guard + .run() + .await + .unwrap(); + } + + #[tokio::test] + async fn test_start_containers() { + start_containers().await; + } + + #[tokio::test] + async fn test_dist_server_require_basic_auth_missing_creds() { + start_containers().await; + let cli_test_context = CliTestContext::new(Scenario::None).await; + let rustup_init_path = cli_test_context + .config + .exedir + .join(format!("rustup-init{EXE_SUFFIX}")); + let mut command = Command::new(rustup_init_path); + command + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .args(&["-y", "--no-modify-path"]) + .env( + "RUSTUP_HOME", + cli_test_context.config.rustupdir.to_string().as_str(), + ) + .env( + "CARGO_HOME", + cli_test_context.config.rustupdir.to_string().as_str(), + ) + .env("RUSTUP_DIST_SERVER", RUSTUP_TEST_DIST_SERVER) + .env( + "RUSTUP_UPDATE_ROOT", + format!("{RUSTUP_TEST_DIST_SERVER}/rustup").as_str(), + ); + let mut child = command.spawn().unwrap(); + let exit_status = child.wait().unwrap(); + assert!(!exit_status.success()); + } + + #[tokio::test] + async fn test_dist_server_require_basic_auth_incorrect_creds() { + start_containers().await; + let cli_test_context = CliTestContext::new(Scenario::None).await; + let rustup_init_path = cli_test_context + .config + .exedir + .join(format!("rustup-init{EXE_SUFFIX}")); + let mut command = Command::new(rustup_init_path); + // Basic creds derived from 'test:123?45>67'. + command + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .args(&["-y", "--no-modify-path"]) + .env( + "RUSTUP_HOME", + cli_test_context.config.rustupdir.to_string().as_str(), + ) + .env( + "CARGO_HOME", + cli_test_context.config.rustupdir.to_string().as_str(), + ) + .env("RUSTUP_DIST_SERVER", RUSTUP_TEST_DIST_SERVER) + .env( + "RUSTUP_UPDATE_ROOT", + format!("{RUSTUP_TEST_DIST_SERVER}/rustup").as_str(), + ) + .env("RUSTUP_AUTHORIZATION_HEADER", "Basic dGVzdDoxMjM/NDU+Njc="); + let mut child = command.spawn().unwrap(); + let exit_status = child.wait().unwrap(); + assert!(!exit_status.success()); + } + + #[tokio::test] + async fn test_dist_server_require_basic_auth() { + start_containers().await; + let cli_test_context = CliTestContext::new(Scenario::None).await; + let rustup_init_path = cli_test_context + .config + .exedir + .join(format!("rustup-init{EXE_SUFFIX}")); + let mut command = Command::new(rustup_init_path); + // Basic creds derived from 'test:123?45>6'. + command + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .args(&["-y", "--no-modify-path"]) + .env( + "RUSTUP_HOME", + cli_test_context.config.rustupdir.to_string().as_str(), + ) + .env( + "CARGO_HOME", + cli_test_context.config.rustupdir.to_string().as_str(), + ) + .env("RUSTUP_DIST_SERVER", RUSTUP_TEST_DIST_SERVER) + .env( + "RUSTUP_UPDATE_ROOT", + format!("{RUSTUP_TEST_DIST_SERVER}/rustup").as_str(), + ) + .env("RUSTUP_AUTHORIZATION_HEADER", "Basic dGVzdDoxMjM/NDU+Ng=="); + let mut child = command.spawn().unwrap(); + let exit_status = child.wait().unwrap(); + assert!(exit_status.success()); + } + + #[tokio::test] + async fn test_forward_proxy_require_basic_auth_missing_creds() { + start_containers().await; + let cli_test_context = CliTestContext::new(Scenario::None).await; + let rustup_init_path = cli_test_context + .config + .exedir + .join(format!("rustup-init{EXE_SUFFIX}")); + // Inside of the container, the test dist server needs to be reached using its hostname. + let rust_test_dist_server = format!("http://{}:8080", TestContainer::DistServer); + let mut command = Command::new(rustup_init_path); + command + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .args(&["-y", "--no-modify-path"]) + .env( + "RUSTUP_HOME", + cli_test_context.config.rustupdir.to_string().as_str(), + ) + .env( + "CARGO_HOME", + cli_test_context.config.rustupdir.to_string().as_str(), + ) + .env("RUSTUP_DIST_SERVER", &rust_test_dist_server) + .env( + "RUSTUP_UPDATE_ROOT", + format!("{rust_test_dist_server}/rustup").as_str(), + ) + .env("RUSTUP_AUTHORIZATION_HEADER", "Basic dGVzdDoxMjM/NDU+Ng==") + .env("ALL_PROXY", RUSTUP_TEST_FORWARD_PROXY); + let mut child = command.spawn().unwrap(); + let exit_status = child.wait().unwrap(); + assert!(!exit_status.success()); + } + + #[tokio::test] + async fn test_forward_proxy_require_basic_auth_incorrect_creds() { + start_containers().await; + let cli_test_context = CliTestContext::new(Scenario::None).await; + let rustup_init_path = cli_test_context + .config + .exedir + .join(format!("rustup-init{EXE_SUFFIX}")); + // Inside of the container, the test dist server needs to be reached using its hostname. + let rust_test_dist_server = format!("http://{}:8080", TestContainer::DistServer); + let mut command = Command::new(rustup_init_path); + command + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .args(&["-y", "--no-modify-path"]) + .env( + "RUSTUP_HOME", + cli_test_context.config.rustupdir.to_string().as_str(), + ) + .env( + "CARGO_HOME", + cli_test_context.config.rustupdir.to_string().as_str(), + ) + .env("RUSTUP_DIST_SERVER", &rust_test_dist_server) + .env( + "RUSTUP_UPDATE_ROOT", + format!("{rust_test_dist_server}/rustup").as_str(), + ) + .env("RUSTUP_AUTHORIZATION_HEADER", "Basic dGVzdDoxMjM/NDU+Ng==") + .env("ALL_PROXY", RUSTUP_TEST_FORWARD_PROXY) + .env( + "RUSTUP_PROXY_AUTHORIZATION_HEADER", + "Basic dGVzdDoxMjM/NDU+Njc=", + ); + let mut child = command.spawn().unwrap(); + let exit_status = child.wait().unwrap(); + assert!(!exit_status.success()); + } + + #[tokio::test] + async fn test_forward_proxy_require_basic_auth() { + start_containers().await; + let cli_test_context = CliTestContext::new(Scenario::None).await; + let rustup_init_path = cli_test_context + .config + .exedir + .join(format!("rustup-init{EXE_SUFFIX}")); + // Inside of the container, the test dist server needs to be reached using its hostname. + let rust_test_dist_server = format!("http://{}:8080", TestContainer::DistServer); + let mut command = Command::new(rustup_init_path); + command + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .args(&["-y", "--no-modify-path"]) + .env( + "RUSTUP_HOME", + cli_test_context.config.rustupdir.to_string().as_str(), + ) + .env( + "CARGO_HOME", + cli_test_context.config.rustupdir.to_string().as_str(), + ) + .env("RUSTUP_DIST_SERVER", &rust_test_dist_server) + .env( + "RUSTUP_UPDATE_ROOT", + format!("{rust_test_dist_server}/rustup").as_str(), + ) + .env("RUSTUP_AUTHORIZATION_HEADER", "Basic dGVzdDoxMjM/NDU+Ng==") + .env("ALL_PROXY", RUSTUP_TEST_FORWARD_PROXY) + .env( + "RUSTUP_PROXY_AUTHORIZATION_HEADER", + "Basic dGVzdDoxMjM/NDU+Ng==", + ); + let mut child = command.spawn().unwrap(); + let exit_status = child.wait().unwrap(); + assert!(exit_status.success()); + } +} diff --git a/tests/suite/mod.rs b/tests/suite/mod.rs index 296102d67a..1d9a029ba7 100644 --- a/tests/suite/mod.rs +++ b/tests/suite/mod.rs @@ -6,6 +6,7 @@ mod cli_rustup; mod cli_rustup_init_ui; mod cli_rustup_ui; mod cli_self_upd; +mod cli_test_with_containers; mod cli_v1; mod cli_v2; mod dist_install; From a38f138fadb7b85c6d44c27171362695f7e3084e Mon Sep 17 00:00:00 2001 From: Andres Mejia Sanchez Date: Mon, 12 Jan 2026 20:33:30 -0500 Subject: [PATCH 08/15] Embed the basic configuration needed to start the test distribution server. --- src/test/clitools.rs | 20 ++++++++++---------- tests/resources/rustup-test-dist-server.kdl | 14 -------------- 2 files changed, 10 insertions(+), 24 deletions(-) delete mode 100644 tests/resources/rustup-test-dist-server.kdl diff --git a/src/test/clitools.rs b/src/test/clitools.rs index bfb11671ac..3540e2115f 100644 --- a/src/test/clitools.rs +++ b/src/test/clitools.rs @@ -1256,8 +1256,15 @@ where const DEFAULT_RUSTUP_TEST_NETWORK_NAME: &str = "rustup-test-network"; -const RUSTUP_TEST_DIST_SERVER_CONFIG: &str = - include_str!("../../tests/resources/rustup-test-dist-server.kdl"); +// This is the simplest ferron config that will send all output to stderr and serve the rustup test distribution files +// and require basic authentication. The only valid credentials are 'test:123?45>6'. +const RUSTUP_TEST_DIST_SERVER_CONFIG: &str = ":8080 {\n\ + log \"/dev/stderr\"\n\ + error_log \"/dev/stderr\"\n\ + status 401 users=\"test\"\n\ + user \"test\" \"$argon2id$v=19$m=19456,t=2,p=1$emTillHaS3OqFuvITdXxzg$G00heP8QSXk5H/ruTiLt302Xk3uETfU5QO8hBIwUq08\"\n\ + root \"/mnt/rustup-test-temp-dir\"\n\ +}"; // This is used for ensuring one thread ensures the test network is created. static DOCKER_NETWORK_CREATE_LOCK: LazyLock> = LazyLock::new(|| RwLock::new(())); @@ -1343,20 +1350,13 @@ impl TestContainerContext { .path() .to_string_lossy() .into_owned(); - let re = regex::Regex::new(r"(?sm)\n// BEGIN VARIABLE CONFIG SECTION\n.*\n// END VARIABLE CONFIG SECTION\n").unwrap(); - let config = re - .replace_all( - RUSTUP_TEST_DIST_SERVER_CONFIG, - "\n root \"/mnt/rustup-test-temp-dir\"\n", - ) - .to_string(); tokio::fs::write( cli_test_context .config .test_dist_dir .path() .join("rustup-test-dist-server.kdl"), - config, + RUSTUP_TEST_DIST_SERVER_CONFIG, ) .await .unwrap(); diff --git a/tests/resources/rustup-test-dist-server.kdl b/tests/resources/rustup-test-dist-server.kdl deleted file mode 100644 index 8122750745..0000000000 --- a/tests/resources/rustup-test-dist-server.kdl +++ /dev/null @@ -1,14 +0,0 @@ -// Ferron configuration to start simple test distribution server. See the README.md for details about this file. -:8080 { - log_date_format "%d/%b/%Y:%H:%M:%S %z" - log_format "{client_ip} - {auth_user} [{timestamp}] \"{method} {path_and_query} {version}\" {status_code} {content_length} \"{header:Referer}\" \"{header:User-Agent}\"" - log "/dev/stderr" - error_log "/dev/stderr" - status 401 users="test" - user "test" "$argon2id$v=19$m=19456,t=2,p=1$emTillHaS3OqFuvITdXxzg$G00heP8QSXk5H/ruTiLt302Xk3uETfU5QO8hBIwUq08" -// BEGIN VARIABLE CONFIG SECTION - proxy "https://static.rust-lang.org/" - proxy_request_header_remove "Authorization" - proxy_request_header_remove "Proxy-Authorization" -// END VARIABLE CONFIG SECTION -} From 772a7a14c3ec9426bf3d2f706ef61f96e62056ce Mon Sep 17 00:00:00 2001 From: Andres Mejia Sanchez Date: Mon, 12 Jan 2026 22:56:24 -0500 Subject: [PATCH 09/15] Use a better implementation to run the container tests. --- src/test/clitools.rs | 53 +++++------ tests/suite/cli_test_with_containers.rs | 115 ++++++++++++++++-------- 2 files changed, 101 insertions(+), 67 deletions(-) diff --git a/src/test/clitools.rs b/src/test/clitools.rs index 3540e2115f..1c16ab096d 100644 --- a/src/test/clitools.rs +++ b/src/test/clitools.rs @@ -1267,10 +1267,8 @@ const RUSTUP_TEST_DIST_SERVER_CONFIG: &str = ":8080 {\n\ }"; // This is used for ensuring one thread ensures the test network is created. -static DOCKER_NETWORK_CREATE_LOCK: LazyLock> = LazyLock::new(|| RwLock::new(())); - -static DEFAULT_RUSTUP_TEST_DOCKER_PROGRAM: LazyLock = - LazyLock::new(|| format!("docker{EXE_SUFFIX}")); +static CONTAINER_NETWORK_CREATE_LOCK: LazyLock> = + LazyLock::new(|| tokio::sync::Mutex::new(())); #[derive(Debug)] pub enum TestContainer { @@ -1295,7 +1293,7 @@ pub struct TestContainerContext { container: TestContainer, docker_program: std::ffi::OsString, leave_container_running: bool, - cli_test_context: tokio::sync::Mutex>, + cli_test_context: CliTestContext, } impl Drop for TestContainerContext { @@ -1305,16 +1303,26 @@ impl Drop for TestContainerContext { } impl TestContainerContext { - pub fn new(process: &process::Process, container: TestContainer) -> Self { + pub async fn new(process: &process::Process, container: TestContainer) -> Self { + let default_docker_program = format!("docker{EXE_SUFFIX}"); let docker_program = process .var_os("RUSTUP_TEST_DOCKER_PROGRAM") - .unwrap_or((*DEFAULT_RUSTUP_TEST_DOCKER_PROGRAM).as_str().into()); + .unwrap_or(default_docker_program.into()); let leave_container_running = process .var_opt("RUSTUP_TEST_LEAVE_CONTAINERS_RUNNING") .unwrap() .map(|var| var.eq("true")) .unwrap_or_default(); - let cli_test_context = tokio::sync::Mutex::new(None); + let cli_test_context = match container { + TestContainer::DistServer => { + let mut cli_test_context = CliTestContext::new(Scenario::SimpleV2).await; + if leave_container_running { + cli_test_context.config.test_dist_dir.disable_cleanup(true); + } + cli_test_context + } + TestContainer::ForwardProxy => CliTestContext::new(Scenario::None).await, + }; Self { container, docker_program, @@ -1323,35 +1331,20 @@ impl TestContainerContext { } } - async fn ensure_initialized(&mut self) { - let mut cli_test_context_guard = self.cli_test_context.lock().await; - if cli_test_context_guard.is_none() { - let test_version = "2.0.0"; - let mut cli_test_context = CliTestContext::new(Scenario::SimpleV2).await; - let _dist_guard = cli_test_context.with_update_server(test_version); - if self.leave_container_running { - cli_test_context.config.test_dist_dir.disable_cleanup(true); - } - cli_test_context_guard.replace(cli_test_context); - } - } - pub async fn run(&mut self) -> anyhow::Result<()> { if !self.is_running() { - self.ensure_network_created().unwrap(); - self.ensure_initialized().await; - let cli_test_context_guard = self.cli_test_context.lock().await; - let cli_test_context = cli_test_context_guard.as_ref().unwrap(); + self.ensure_network_created().await.unwrap(); match self.container { TestContainer::DistServer => { - let temp_dir_string = cli_test_context + let temp_dir_string = self + .cli_test_context .config .test_dist_dir .path() .to_string_lossy() .into_owned(); tokio::fs::write( - cli_test_context + self.cli_test_context .config .test_dist_dir .path() @@ -1433,8 +1426,8 @@ impl TestContainerContext { } } - fn ensure_network_created(&self) -> anyhow::Result<()> { - let _guard = (*DOCKER_NETWORK_CREATE_LOCK).write().unwrap(); + async fn ensure_network_created(&self) -> anyhow::Result<()> { + let _guard = (*CONTAINER_NETWORK_CREATE_LOCK).lock().await; let mut command = Command::new(&self.docker_program); let output = command .stdout(std::process::Stdio::piped()) @@ -1486,7 +1479,7 @@ impl TestContainerContext { !output.stdout.is_empty() } - fn cleanup_container(&self) -> anyhow::Result<()> { + pub fn cleanup_container(&self) -> anyhow::Result<()> { if !self.leave_container_running { if self.is_running() { let mut command = Command::new(&self.docker_program); diff --git a/tests/suite/cli_test_with_containers.rs b/tests/suite/cli_test_with_containers.rs index bdd9c1ab44..284b1ff77c 100644 --- a/tests/suite/cli_test_with_containers.rs +++ b/tests/suite/cli_test_with_containers.rs @@ -3,61 +3,96 @@ mod tests { use std::{ env::consts::EXE_SUFFIX, process::{Command, Stdio}, - sync::{Arc, LazyLock, Mutex}, }; use rustup::{ process::Process, test::{CliTestContext, Scenario, TestContainer, TestContainerContext}, }; + use tokio::sync::{Mutex, OnceCell}; // TODO: Figure out how to programmatically determine the published ports in running containers and then // use that to set the 'RUSTUP_DIST_SERVER' and 'ALL_PROXY' environment variables. const RUSTUP_TEST_DIST_SERVER: &str = "http://localhost:8080"; const RUSTUP_TEST_FORWARD_PROXY: &str = "http://localhost:9080"; - static RUSTUP_TEST_DIST_SERVER_CONTAINER_CONTEXT: LazyLock>> = - LazyLock::new(|| { - let process = Process::os(); - Arc::new(Mutex::new(TestContainerContext::new( - &process, - TestContainer::DistServer, - ))) - }); - static RUSTUP_TEST_FORWARD_PROXY_CONTAINER_CONTEXT: LazyLock>> = - LazyLock::new(|| { + struct TestContainerContexts { + dist_server_container_context: TestContainerContext, + forward_proxy_container_context: TestContainerContext, + counter: usize, + } + + impl TestContainerContexts { + async fn new() -> Self { let process = Process::os(); - Arc::new(Mutex::new(TestContainerContext::new( - &process, - TestContainer::ForwardProxy, - ))) - }); + Self { + dist_server_container_context: TestContainerContext::new( + &process, + TestContainer::DistServer, + ) + .await, + forward_proxy_container_context: TestContainerContext::new( + &process, + TestContainer::ForwardProxy, + ) + .await, + counter: 0, + } + } + + async fn start_containers(&mut self) { + self.dist_server_container_context.run().await.unwrap(); + self.forward_proxy_container_context.run().await.unwrap(); + self.counter = self.counter + 1; + } + + async fn stop_containers(&mut self) { + self.counter = self.counter - 1; + if self.counter == 0 { + self.dist_server_container_context + .cleanup_container() + .unwrap(); + self.forward_proxy_container_context + .cleanup_container() + .unwrap(); + } + } + } + + static TEST_CONTAINER_CONTEXTS_ONCE: OnceCell> = + OnceCell::const_new(); + + macro_rules! start_containers { + () => {{ + let mut test_container_contexts_guard = TEST_CONTAINER_CONTEXTS_ONCE + .get_or_init(|| async { Mutex::new(TestContainerContexts::new().await) }) + .await + .lock() + .await; + test_container_contexts_guard.start_containers().await; + }}; + } - async fn start_containers() { - let mut test_dist_server_container_context_guard = - (*RUSTUP_TEST_DIST_SERVER_CONTAINER_CONTEXT).lock().unwrap(); - test_dist_server_container_context_guard - .run() - .await - .unwrap(); - let mut test_forward_proxy_container_context_guard = - (*RUSTUP_TEST_FORWARD_PROXY_CONTAINER_CONTEXT) + macro_rules! stop_containers { + () => {{ + let mut test_container_contexts_guard = TEST_CONTAINER_CONTEXTS_ONCE + .get_or_init(|| async { Mutex::new(TestContainerContexts::new().await) }) + .await .lock() - .unwrap(); - test_forward_proxy_container_context_guard - .run() - .await - .unwrap(); + .await; + test_container_contexts_guard.stop_containers().await; + }}; } #[tokio::test] async fn test_start_containers() { - start_containers().await; + start_containers!(); + stop_containers!(); } #[tokio::test] async fn test_dist_server_require_basic_auth_missing_creds() { - start_containers().await; + start_containers!(); let cli_test_context = CliTestContext::new(Scenario::None).await; let rustup_init_path = cli_test_context .config @@ -85,11 +120,12 @@ mod tests { let mut child = command.spawn().unwrap(); let exit_status = child.wait().unwrap(); assert!(!exit_status.success()); + stop_containers!(); } #[tokio::test] async fn test_dist_server_require_basic_auth_incorrect_creds() { - start_containers().await; + start_containers!(); let cli_test_context = CliTestContext::new(Scenario::None).await; let rustup_init_path = cli_test_context .config @@ -119,11 +155,12 @@ mod tests { let mut child = command.spawn().unwrap(); let exit_status = child.wait().unwrap(); assert!(!exit_status.success()); + stop_containers!(); } #[tokio::test] async fn test_dist_server_require_basic_auth() { - start_containers().await; + start_containers!(); let cli_test_context = CliTestContext::new(Scenario::None).await; let rustup_init_path = cli_test_context .config @@ -153,11 +190,12 @@ mod tests { let mut child = command.spawn().unwrap(); let exit_status = child.wait().unwrap(); assert!(exit_status.success()); + stop_containers!(); } #[tokio::test] async fn test_forward_proxy_require_basic_auth_missing_creds() { - start_containers().await; + start_containers!(); let cli_test_context = CliTestContext::new(Scenario::None).await; let rustup_init_path = cli_test_context .config @@ -189,11 +227,12 @@ mod tests { let mut child = command.spawn().unwrap(); let exit_status = child.wait().unwrap(); assert!(!exit_status.success()); + stop_containers!(); } #[tokio::test] async fn test_forward_proxy_require_basic_auth_incorrect_creds() { - start_containers().await; + start_containers!(); let cli_test_context = CliTestContext::new(Scenario::None).await; let rustup_init_path = cli_test_context .config @@ -229,11 +268,12 @@ mod tests { let mut child = command.spawn().unwrap(); let exit_status = child.wait().unwrap(); assert!(!exit_status.success()); + stop_containers!(); } #[tokio::test] async fn test_forward_proxy_require_basic_auth() { - start_containers().await; + start_containers!(); let cli_test_context = CliTestContext::new(Scenario::None).await; let rustup_init_path = cli_test_context .config @@ -269,5 +309,6 @@ mod tests { let mut child = command.spawn().unwrap(); let exit_status = child.wait().unwrap(); assert!(exit_status.success()); + stop_containers!(); } } From a09c39d97caa0d991896783ea80be06b72655b12 Mon Sep 17 00:00:00 2001 From: Andres Mejia Sanchez Date: Mon, 12 Jan 2026 23:02:31 -0500 Subject: [PATCH 10/15] Fix issue found with cargo clippy. --- tests/suite/cli_test_with_containers.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/suite/cli_test_with_containers.rs b/tests/suite/cli_test_with_containers.rs index 284b1ff77c..d6a84a0a39 100644 --- a/tests/suite/cli_test_with_containers.rs +++ b/tests/suite/cli_test_with_containers.rs @@ -103,7 +103,7 @@ mod tests { .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) - .args(&["-y", "--no-modify-path"]) + .args(["-y", "--no-modify-path"]) .env( "RUSTUP_HOME", cli_test_context.config.rustupdir.to_string().as_str(), @@ -137,7 +137,7 @@ mod tests { .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) - .args(&["-y", "--no-modify-path"]) + .args(["-y", "--no-modify-path"]) .env( "RUSTUP_HOME", cli_test_context.config.rustupdir.to_string().as_str(), @@ -172,7 +172,7 @@ mod tests { .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) - .args(&["-y", "--no-modify-path"]) + .args(["-y", "--no-modify-path"]) .env( "RUSTUP_HOME", cli_test_context.config.rustupdir.to_string().as_str(), @@ -208,7 +208,7 @@ mod tests { .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) - .args(&["-y", "--no-modify-path"]) + .args(["-y", "--no-modify-path"]) .env( "RUSTUP_HOME", cli_test_context.config.rustupdir.to_string().as_str(), @@ -245,7 +245,7 @@ mod tests { .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) - .args(&["-y", "--no-modify-path"]) + .args(["-y", "--no-modify-path"]) .env( "RUSTUP_HOME", cli_test_context.config.rustupdir.to_string().as_str(), @@ -286,7 +286,7 @@ mod tests { .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) - .args(&["-y", "--no-modify-path"]) + .args(["-y", "--no-modify-path"]) .env( "RUSTUP_HOME", cli_test_context.config.rustupdir.to_string().as_str(), From 5c54cc0082969ac254cce7f634c867429ce73776 Mon Sep 17 00:00:00 2001 From: Andres Mejia Sanchez Date: Mon, 12 Jan 2026 23:06:10 -0500 Subject: [PATCH 11/15] Disable input and output through stdio for docker commands. --- src/test/clitools.rs | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/test/clitools.rs b/src/test/clitools.rs index 1c16ab096d..15ac0ce1f0 100644 --- a/src/test/clitools.rs +++ b/src/test/clitools.rs @@ -12,7 +12,7 @@ use std::{ mem, ops::{Deref, DerefMut}, path::{Path, PathBuf}, - process::Command, + process::{Command, Stdio}, string::FromUtf8Error, sync::{Arc, LazyLock, RwLock, RwLockWriteGuard}, time::Instant, @@ -1355,6 +1355,9 @@ impl TestContainerContext { .unwrap(); let mut command = Command::new(&self.docker_program); let exit_status = command + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) .args([ "run", "--detach", @@ -1389,6 +1392,9 @@ impl TestContainerContext { TestContainer::ForwardProxy => { let mut command = Command::new(&self.docker_program); let exit_status = command + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) .args([ "run", "--detach", @@ -1430,7 +1436,9 @@ impl TestContainerContext { let _guard = (*CONTAINER_NETWORK_CREATE_LOCK).lock().await; let mut command = Command::new(&self.docker_program); let output = command - .stdout(std::process::Stdio::piped()) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) .args([ "network", "ls", @@ -1444,6 +1452,9 @@ impl TestContainerContext { if output.stdout.is_empty() { let mut command = Command::new(&self.docker_program); let exit_status = command + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) .args(["network", "create", DEFAULT_RUSTUP_TEST_NETWORK_NAME]) .spawn() .unwrap() @@ -1466,7 +1477,9 @@ impl TestContainerContext { fn is_running(&self) -> bool { let mut command = Command::new(&self.docker_program); let output = command - .stdout(std::process::Stdio::piped()) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) .args([ "ps", "--format", @@ -1484,6 +1497,9 @@ impl TestContainerContext { if self.is_running() { let mut command = Command::new(&self.docker_program); let exit_status = command + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) .args(["stop", format!("{}", self.container).as_str()]) .spawn() .unwrap() @@ -1499,6 +1515,9 @@ impl TestContainerContext { } else { let mut command = Command::new(&self.docker_program); let exit_status = command + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) .args(["rm", format!("{}", self.container).as_str()]) .spawn() .unwrap() From b72574ad41d0b9d4e86c3608966ed8c40854cfdf Mon Sep 17 00:00:00 2001 From: Andres Mejia Sanchez Date: Mon, 12 Jan 2026 23:39:18 -0500 Subject: [PATCH 12/15] Fix clippy lints caught through CI tasks. --- tests/suite/cli_test_with_containers.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/suite/cli_test_with_containers.rs b/tests/suite/cli_test_with_containers.rs index d6a84a0a39..37f687482d 100644 --- a/tests/suite/cli_test_with_containers.rs +++ b/tests/suite/cli_test_with_containers.rs @@ -43,11 +43,11 @@ mod tests { async fn start_containers(&mut self) { self.dist_server_container_context.run().await.unwrap(); self.forward_proxy_container_context.run().await.unwrap(); - self.counter = self.counter + 1; + self.counter += 1; } async fn stop_containers(&mut self) { - self.counter = self.counter - 1; + self.counter -= 1; if self.counter == 0 { self.dist_server_container_context .cleanup_container() From fd0ebbe254ab27bc43d455eaaf293f2d5880eca4 Mon Sep 17 00:00:00 2001 From: Andres Mejia Sanchez Date: Tue, 13 Jan 2026 21:30:57 -0500 Subject: [PATCH 13/15] Add tests for using basic authentication with RUSTUP_USE_CURL enabled and fix issues found with new tests. --- src/download/mod.rs | 56 ++++-- tests/suite/cli_test_with_containers.rs | 228 ++++++++++++++++++++++++ 2 files changed, 268 insertions(+), 16 deletions(-) diff --git a/src/download/mod.rs b/src/download/mod.rs index 006af852ad..7f7e7e6016 100644 --- a/src/download/mod.rs +++ b/src/download/mod.rs @@ -228,6 +228,19 @@ async fn download_file_( res } +#[cfg(any( + feature = "curl-backend", + feature = "reqwest-rustls-tls", + feature = "reqwest-native-tls" +))] +const RUSTUP_AUTHORIZATION_HEADER_ENV_VAR: &str = "RUSTUP_AUTHORIZATION_HEADER"; +#[cfg(any( + feature = "curl-backend", + feature = "reqwest-rustls-tls", + feature = "reqwest-native-tls" +))] +const RUSTUP_PROXY_AUTHORIZATION_HEADER_ENV_VAR: &str = "RUSTUP_PROXY_AUTHORIZATION_HEADER"; + /// User agent header value for HTTP request. /// See: https://github.com/rust-lang/rustup/issues/2860. #[cfg(feature = "curl-backend")] @@ -454,10 +467,13 @@ mod curl { use tracing::debug; use url::Url; - use super::{DownloadError, Event, Process}; + use super::{ + DownloadError, Event, Process, RUSTUP_AUTHORIZATION_HEADER_ENV_VAR, + RUSTUP_PROXY_AUTHORIZATION_HEADER_ENV_VAR, + }; macro_rules! add_header_for_curl_easy_handle { - ($handle:ident, $process:ident, $env_var:literal, $header_name:literal) => { + ($handle:ident, $process:ident, $env_var:ident, $header_name:literal, $header_list:ident) => { if let Some(rustup_header_value) = $process.var_opt($env_var).map_err(|error| { anyhow::anyhow!( "Internal error getting `{}` environment variable: {}", @@ -465,17 +481,13 @@ mod curl { anyhow::format_err!(error) ) })? { - let mut list = List::new(); + let list = $header_list.get_or_insert(List::new()); list.append(format!("{}: {}", $header_name, rustup_header_value).as_str()) .map_err(|_| { // The error could contain sensitive data so give a generic error instead. anyhow::anyhow!("Failed to add `{}` HTTP header.", $header_name) })?; - $handle.http_headers(list).map_err(|_| { - // The error could contain sensitive data so give a generic error instead. - anyhow::anyhow!("Failed to add headers to curl easy handle.") - })?; - debug!("Added `{}` header.", $header_name); + debug!("Adding `{}` header.", $header_name); } }; } @@ -499,18 +511,27 @@ mod curl { handle.url(url.as_ref())?; handle.follow_location(true)?; handle.useragent(super::CURL_USER_AGENT)?; + let mut header_list: Option = None; add_header_for_curl_easy_handle!( handle, process, - "RUSTUP_AUTHORIZATION_HEADER", - "Authorization" + RUSTUP_AUTHORIZATION_HEADER_ENV_VAR, + "Authorization", + header_list ); add_header_for_curl_easy_handle!( handle, process, - "RUSTUP_PROXY_AUTHORIZATION_HEADER", - "Proxy-Authorization" + RUSTUP_PROXY_AUTHORIZATION_HEADER_ENV_VAR, + "Proxy-Authorization", + header_list ); + if let Some(list) = header_list { + handle.http_headers(list).map_err(|_| { + // The error could contain sensitive data so give a generic error instead. + anyhow::anyhow!("Failed to add headers to curl easy handle.") + })?; + } if resume_from > 0 { handle.resume_from(resume_from)?; @@ -615,10 +636,13 @@ mod reqwest_be { use tokio_stream::StreamExt; use url::Url; - use super::{DownloadError, Event, Process, debug}; + use super::{ + DownloadError, Event, Process, RUSTUP_AUTHORIZATION_HEADER_ENV_VAR, + RUSTUP_PROXY_AUTHORIZATION_HEADER_ENV_VAR, debug, + }; macro_rules! add_header_for_client_builder { - ($client_builder:ident, $process:ident, $env_var:literal, $header_name:path) => { + ($client_builder:ident, $process:ident, $env_var:ident, $header_name:path) => { if let Some(rustup_header_value) = $process.var_opt($env_var).map_err(|_| { // The error could contain sensitive data so give a generic error instead. DownloadError::Message(format!( @@ -687,13 +711,13 @@ mod reqwest_be { add_header_for_client_builder!( client_builder, process, - "RUSTUP_AUTHORIZATION_HEADER", + RUSTUP_AUTHORIZATION_HEADER_ENV_VAR, header::AUTHORIZATION ); add_header_for_client_builder!( client_builder, process, - "RUSTUP_PROXY_AUTHORIZATION_HEADER", + RUSTUP_PROXY_AUTHORIZATION_HEADER_ENV_VAR, header::PROXY_AUTHORIZATION ); Ok(client_builder) diff --git a/tests/suite/cli_test_with_containers.rs b/tests/suite/cli_test_with_containers.rs index 37f687482d..544cb1691a 100644 --- a/tests/suite/cli_test_with_containers.rs +++ b/tests/suite/cli_test_with_containers.rs @@ -311,4 +311,232 @@ mod tests { assert!(exit_status.success()); stop_containers!(); } + + #[tokio::test] + async fn test_dist_server_require_basic_auth_missing_creds_with_curl() { + start_containers!(); + let cli_test_context = CliTestContext::new(Scenario::None).await; + let rustup_init_path = cli_test_context + .config + .exedir + .join(format!("rustup-init{EXE_SUFFIX}")); + let mut command = Command::new(rustup_init_path); + command + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .args(["-y", "--no-modify-path"]) + .env( + "RUSTUP_HOME", + cli_test_context.config.rustupdir.to_string().as_str(), + ) + .env( + "CARGO_HOME", + cli_test_context.config.rustupdir.to_string().as_str(), + ) + .env("RUSTUP_DIST_SERVER", RUSTUP_TEST_DIST_SERVER) + .env( + "RUSTUP_UPDATE_ROOT", + format!("{RUSTUP_TEST_DIST_SERVER}/rustup").as_str(), + ) + .env("RUSTUP_USE_CURL", "1"); + let mut child = command.spawn().unwrap(); + let exit_status = child.wait().unwrap(); + assert!(!exit_status.success()); + stop_containers!(); + } + + #[tokio::test] + async fn test_dist_server_require_basic_auth_incorrect_creds_with_curl() { + start_containers!(); + let cli_test_context = CliTestContext::new(Scenario::None).await; + let rustup_init_path = cli_test_context + .config + .exedir + .join(format!("rustup-init{EXE_SUFFIX}")); + let mut command = Command::new(rustup_init_path); + // Basic creds derived from 'test:123?45>67'. + command + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .args(["-y", "--no-modify-path"]) + .env( + "RUSTUP_HOME", + cli_test_context.config.rustupdir.to_string().as_str(), + ) + .env( + "CARGO_HOME", + cli_test_context.config.rustupdir.to_string().as_str(), + ) + .env("RUSTUP_DIST_SERVER", RUSTUP_TEST_DIST_SERVER) + .env( + "RUSTUP_UPDATE_ROOT", + format!("{RUSTUP_TEST_DIST_SERVER}/rustup").as_str(), + ) + .env("RUSTUP_AUTHORIZATION_HEADER", "Basic dGVzdDoxMjM/NDU+Njc=") + .env("RUSTUP_USE_CURL", "1"); + let mut child = command.spawn().unwrap(); + let exit_status = child.wait().unwrap(); + assert!(!exit_status.success()); + stop_containers!(); + } + + #[tokio::test] + async fn test_dist_server_require_basic_auth_with_curl() { + start_containers!(); + let cli_test_context = CliTestContext::new(Scenario::None).await; + let rustup_init_path = cli_test_context + .config + .exedir + .join(format!("rustup-init{EXE_SUFFIX}")); + let mut command = Command::new(rustup_init_path); + // Basic creds derived from 'test:123?45>6'. + command + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .args(["-y", "--no-modify-path"]) + .env( + "RUSTUP_HOME", + cli_test_context.config.rustupdir.to_string().as_str(), + ) + .env( + "CARGO_HOME", + cli_test_context.config.rustupdir.to_string().as_str(), + ) + .env("RUSTUP_DIST_SERVER", RUSTUP_TEST_DIST_SERVER) + .env( + "RUSTUP_UPDATE_ROOT", + format!("{RUSTUP_TEST_DIST_SERVER}/rustup").as_str(), + ) + .env("RUSTUP_AUTHORIZATION_HEADER", "Basic dGVzdDoxMjM/NDU+Ng==") + .env("RUSTUP_USE_CURL", "1"); + let mut child = command.spawn().unwrap(); + let exit_status = child.wait().unwrap(); + assert!(exit_status.success()); + stop_containers!(); + } + + #[tokio::test] + async fn test_forward_proxy_require_basic_auth_missing_creds_with_curl() { + start_containers!(); + let cli_test_context = CliTestContext::new(Scenario::None).await; + let rustup_init_path = cli_test_context + .config + .exedir + .join(format!("rustup-init{EXE_SUFFIX}")); + // Inside of the container, the test dist server needs to be reached using its hostname. + let rust_test_dist_server = format!("http://{}:8080", TestContainer::DistServer); + let mut command = Command::new(rustup_init_path); + command + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .args(["-y", "--no-modify-path"]) + .env( + "RUSTUP_HOME", + cli_test_context.config.rustupdir.to_string().as_str(), + ) + .env( + "CARGO_HOME", + cli_test_context.config.rustupdir.to_string().as_str(), + ) + .env("RUSTUP_DIST_SERVER", &rust_test_dist_server) + .env( + "RUSTUP_UPDATE_ROOT", + format!("{rust_test_dist_server}/rustup").as_str(), + ) + .env("RUSTUP_AUTHORIZATION_HEADER", "Basic dGVzdDoxMjM/NDU+Ng==") + .env("ALL_PROXY", RUSTUP_TEST_FORWARD_PROXY) + .env("RUSTUP_USE_CURL", "1"); + let mut child = command.spawn().unwrap(); + let exit_status = child.wait().unwrap(); + assert!(!exit_status.success()); + stop_containers!(); + } + + #[tokio::test] + async fn test_forward_proxy_require_basic_auth_incorrect_creds_with_curl() { + start_containers!(); + let cli_test_context = CliTestContext::new(Scenario::None).await; + let rustup_init_path = cli_test_context + .config + .exedir + .join(format!("rustup-init{EXE_SUFFIX}")); + // Inside of the container, the test dist server needs to be reached using its hostname. + let rust_test_dist_server = format!("http://{}:8080", TestContainer::DistServer); + let mut command = Command::new(rustup_init_path); + command + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .args(["-y", "--no-modify-path"]) + .env( + "RUSTUP_HOME", + cli_test_context.config.rustupdir.to_string().as_str(), + ) + .env( + "CARGO_HOME", + cli_test_context.config.rustupdir.to_string().as_str(), + ) + .env("RUSTUP_DIST_SERVER", &rust_test_dist_server) + .env( + "RUSTUP_UPDATE_ROOT", + format!("{rust_test_dist_server}/rustup").as_str(), + ) + .env("RUSTUP_AUTHORIZATION_HEADER", "Basic dGVzdDoxMjM/NDU+Ng==") + .env("ALL_PROXY", RUSTUP_TEST_FORWARD_PROXY) + .env( + "RUSTUP_PROXY_AUTHORIZATION_HEADER", + "Basic dGVzdDoxMjM/NDU+Njc=", + ) + .env("RUSTUP_USE_CURL", "1"); + let mut child = command.spawn().unwrap(); + let exit_status = child.wait().unwrap(); + assert!(!exit_status.success()); + stop_containers!(); + } + + #[tokio::test] + async fn test_forward_proxy_require_basic_auth_with_curl() { + start_containers!(); + let cli_test_context = CliTestContext::new(Scenario::None).await; + let rustup_init_path = cli_test_context + .config + .exedir + .join(format!("rustup-init{EXE_SUFFIX}")); + // Inside of the container, the test dist server needs to be reached using its hostname. + let rust_test_dist_server = format!("http://{}:8080", TestContainer::DistServer); + let mut command = Command::new(rustup_init_path); + command + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .args(["-y", "--no-modify-path"]) + .env( + "RUSTUP_HOME", + cli_test_context.config.rustupdir.to_string().as_str(), + ) + .env( + "CARGO_HOME", + cli_test_context.config.rustupdir.to_string().as_str(), + ) + .env("RUSTUP_DIST_SERVER", &rust_test_dist_server) + .env( + "RUSTUP_UPDATE_ROOT", + format!("{rust_test_dist_server}/rustup").as_str(), + ) + .env("RUSTUP_AUTHORIZATION_HEADER", "Basic dGVzdDoxMjM/NDU+Ng==") + .env("ALL_PROXY", RUSTUP_TEST_FORWARD_PROXY) + .env( + "RUSTUP_PROXY_AUTHORIZATION_HEADER", + "Basic dGVzdDoxMjM/NDU+Ng==", + ) + .env("RUSTUP_USE_CURL", "1"); + let mut child = command.spawn().unwrap(); + let exit_status = child.wait().unwrap(); + assert!(exit_status.success()); + stop_containers!(); + } } From 3b4a014f598a837aeeaa72b27568afb06b2589f8 Mon Sep 17 00:00:00 2001 From: Andres Mejia Sanchez Date: Tue, 13 Jan 2026 22:19:49 -0500 Subject: [PATCH 14/15] Start and stop containers per test. This is to prevent triggering the brute-force protection in the Ferron server. --- tests/README.md | 4 ++ tests/suite/cli_test_with_containers.rs | 80 +++++++++++-------------- 2 files changed, 39 insertions(+), 45 deletions(-) diff --git a/tests/README.md b/tests/README.md index 8eae1013b4..25e7d3918b 100644 --- a/tests/README.md +++ b/tests/README.md @@ -29,6 +29,10 @@ $env:RUSTUP_TEST_LEAVE_CONTAINERS_RUNNING = "true" cargo test --features test-with-containers -- suite::cli_test_with_containers::tests::test_start_containers ``` +Note however the automated tests should not be ran while the `RUSTUP_TEST_LEAVE_CONTAINERS_RUNNING` environment +variable is set to true. Otherwise, the many deliberate use of missing or incorrect credentials will trigger the +test distribution server to stop accepting requests for some time. + Use the docker command to determine what host port is being used for each of the containers. For example... ```sh diff --git a/tests/suite/cli_test_with_containers.rs b/tests/suite/cli_test_with_containers.rs index 544cb1691a..b6aca09dd2 100644 --- a/tests/suite/cli_test_with_containers.rs +++ b/tests/suite/cli_test_with_containers.rs @@ -19,7 +19,6 @@ mod tests { struct TestContainerContexts { dist_server_container_context: TestContainerContext, forward_proxy_container_context: TestContainerContext, - counter: usize, } impl TestContainerContexts { @@ -36,26 +35,21 @@ mod tests { TestContainer::ForwardProxy, ) .await, - counter: 0, } } async fn start_containers(&mut self) { self.dist_server_container_context.run().await.unwrap(); self.forward_proxy_container_context.run().await.unwrap(); - self.counter += 1; } async fn stop_containers(&mut self) { - self.counter -= 1; - if self.counter == 0 { - self.dist_server_container_context - .cleanup_container() - .unwrap(); - self.forward_proxy_container_context - .cleanup_container() - .unwrap(); - } + self.dist_server_container_context + .cleanup_container() + .unwrap(); + self.forward_proxy_container_context + .cleanup_container() + .unwrap(); } } @@ -70,29 +64,25 @@ mod tests { .lock() .await; test_container_contexts_guard.start_containers().await; + test_container_contexts_guard }}; } macro_rules! stop_containers { - () => {{ - let mut test_container_contexts_guard = TEST_CONTAINER_CONTEXTS_ONCE - .get_or_init(|| async { Mutex::new(TestContainerContexts::new().await) }) - .await - .lock() - .await; - test_container_contexts_guard.stop_containers().await; + ($guard:ident) => {{ + $guard.stop_containers().await; }}; } #[tokio::test] async fn test_start_containers() { - start_containers!(); - stop_containers!(); + let mut guard = start_containers!(); + stop_containers!(guard); } #[tokio::test] async fn test_dist_server_require_basic_auth_missing_creds() { - start_containers!(); + let mut guard = start_containers!(); let cli_test_context = CliTestContext::new(Scenario::None).await; let rustup_init_path = cli_test_context .config @@ -120,12 +110,12 @@ mod tests { let mut child = command.spawn().unwrap(); let exit_status = child.wait().unwrap(); assert!(!exit_status.success()); - stop_containers!(); + stop_containers!(guard); } #[tokio::test] async fn test_dist_server_require_basic_auth_incorrect_creds() { - start_containers!(); + let mut guard = start_containers!(); let cli_test_context = CliTestContext::new(Scenario::None).await; let rustup_init_path = cli_test_context .config @@ -155,12 +145,12 @@ mod tests { let mut child = command.spawn().unwrap(); let exit_status = child.wait().unwrap(); assert!(!exit_status.success()); - stop_containers!(); + stop_containers!(guard); } #[tokio::test] async fn test_dist_server_require_basic_auth() { - start_containers!(); + let mut guard = start_containers!(); let cli_test_context = CliTestContext::new(Scenario::None).await; let rustup_init_path = cli_test_context .config @@ -190,12 +180,12 @@ mod tests { let mut child = command.spawn().unwrap(); let exit_status = child.wait().unwrap(); assert!(exit_status.success()); - stop_containers!(); + stop_containers!(guard); } #[tokio::test] async fn test_forward_proxy_require_basic_auth_missing_creds() { - start_containers!(); + let mut guard = start_containers!(); let cli_test_context = CliTestContext::new(Scenario::None).await; let rustup_init_path = cli_test_context .config @@ -227,12 +217,12 @@ mod tests { let mut child = command.spawn().unwrap(); let exit_status = child.wait().unwrap(); assert!(!exit_status.success()); - stop_containers!(); + stop_containers!(guard); } #[tokio::test] async fn test_forward_proxy_require_basic_auth_incorrect_creds() { - start_containers!(); + let mut guard = start_containers!(); let cli_test_context = CliTestContext::new(Scenario::None).await; let rustup_init_path = cli_test_context .config @@ -268,12 +258,12 @@ mod tests { let mut child = command.spawn().unwrap(); let exit_status = child.wait().unwrap(); assert!(!exit_status.success()); - stop_containers!(); + stop_containers!(guard); } #[tokio::test] async fn test_forward_proxy_require_basic_auth() { - start_containers!(); + let mut guard = start_containers!(); let cli_test_context = CliTestContext::new(Scenario::None).await; let rustup_init_path = cli_test_context .config @@ -309,12 +299,12 @@ mod tests { let mut child = command.spawn().unwrap(); let exit_status = child.wait().unwrap(); assert!(exit_status.success()); - stop_containers!(); + stop_containers!(guard); } #[tokio::test] async fn test_dist_server_require_basic_auth_missing_creds_with_curl() { - start_containers!(); + let mut guard = start_containers!(); let cli_test_context = CliTestContext::new(Scenario::None).await; let rustup_init_path = cli_test_context .config @@ -343,12 +333,12 @@ mod tests { let mut child = command.spawn().unwrap(); let exit_status = child.wait().unwrap(); assert!(!exit_status.success()); - stop_containers!(); + stop_containers!(guard); } #[tokio::test] async fn test_dist_server_require_basic_auth_incorrect_creds_with_curl() { - start_containers!(); + let mut guard = start_containers!(); let cli_test_context = CliTestContext::new(Scenario::None).await; let rustup_init_path = cli_test_context .config @@ -379,12 +369,12 @@ mod tests { let mut child = command.spawn().unwrap(); let exit_status = child.wait().unwrap(); assert!(!exit_status.success()); - stop_containers!(); + stop_containers!(guard); } #[tokio::test] async fn test_dist_server_require_basic_auth_with_curl() { - start_containers!(); + let mut guard = start_containers!(); let cli_test_context = CliTestContext::new(Scenario::None).await; let rustup_init_path = cli_test_context .config @@ -415,12 +405,12 @@ mod tests { let mut child = command.spawn().unwrap(); let exit_status = child.wait().unwrap(); assert!(exit_status.success()); - stop_containers!(); + stop_containers!(guard); } #[tokio::test] async fn test_forward_proxy_require_basic_auth_missing_creds_with_curl() { - start_containers!(); + let mut guard = start_containers!(); let cli_test_context = CliTestContext::new(Scenario::None).await; let rustup_init_path = cli_test_context .config @@ -453,12 +443,12 @@ mod tests { let mut child = command.spawn().unwrap(); let exit_status = child.wait().unwrap(); assert!(!exit_status.success()); - stop_containers!(); + stop_containers!(guard); } #[tokio::test] async fn test_forward_proxy_require_basic_auth_incorrect_creds_with_curl() { - start_containers!(); + let mut guard = start_containers!(); let cli_test_context = CliTestContext::new(Scenario::None).await; let rustup_init_path = cli_test_context .config @@ -495,12 +485,12 @@ mod tests { let mut child = command.spawn().unwrap(); let exit_status = child.wait().unwrap(); assert!(!exit_status.success()); - stop_containers!(); + stop_containers!(guard); } #[tokio::test] async fn test_forward_proxy_require_basic_auth_with_curl() { - start_containers!(); + let mut guard = start_containers!(); let cli_test_context = CliTestContext::new(Scenario::None).await; let rustup_init_path = cli_test_context .config @@ -537,6 +527,6 @@ mod tests { let mut child = command.spawn().unwrap(); let exit_status = child.wait().unwrap(); assert!(exit_status.success()); - stop_containers!(); + stop_containers!(guard); } } From ab4830771aa059ab275f6cee6c2a30b786574754 Mon Sep 17 00:00:00 2001 From: Andres Mejia Sanchez Date: Wed, 14 Jan 2026 21:07:31 -0500 Subject: [PATCH 15/15] Use the distroless build of the ferron container image. --- src/test/clitools.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/clitools.rs b/src/test/clitools.rs index 15ac0ce1f0..08f3e4ec55 100644 --- a/src/test/clitools.rs +++ b/src/test/clitools.rs @@ -1369,7 +1369,7 @@ impl TestContainerContext { "8080:8080", "--volume", format!("{temp_dir_string}:/mnt/rustup-test-temp-dir").as_str(), - "ferronserver/ferron:2-debian", + "ferronserver/ferron:2", "ferron", "--config", "/mnt/rustup-test-temp-dir/rustup-test-dist-server.kdl",