v0.24.9
This commit is contained in:
1 parent
1297ea2920
commit
dc0fcabf51
49 files changed
+2632
-252
No files matched your search
+149
-28
@@ -17,6 +17,9 @@ use hyper_util::{
|
||||
use rustls::{ClientConfig, RootCertStore};
|
||||
use serde::Serialize;
|
||||
use std::{
|
||||
env,
|
||||
io::{BufReader, Cursor},
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, RwLock},
|
||||
time::Duration,
|
||||
};
|
||||
@@ -26,6 +29,19 @@ use url::Url;
|
||||
const EMBEDDED_DEFAULTS: &str = include_str!("../UPDATE_CHECK.toml");
|
||||
const REQUEST_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
const MAX_VERSION_DOCUMENT_BYTES: usize = 256 * 1024;
|
||||
const MAX_REDIRECTS: usize = 5;
|
||||
const SYSTEM_CA_BUNDLES: &[&str] = &[
|
||||
// FreeBSD / OPNsense and common ca_root_nss locations.
|
||||
"/etc/ssl/cert.pem",
|
||||
"/usr/local/share/certs/ca-root-nss.crt",
|
||||
"/usr/local/etc/ssl/cert.pem",
|
||||
// Debian/Ubuntu, RHEL/Fedora and Alpine families.
|
||||
"/etc/ssl/certs/ca-certificates.crt",
|
||||
"/etc/pki/tls/certs/ca-bundle.crt",
|
||||
"/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem",
|
||||
"/etc/openssl/cert.pem",
|
||||
"/etc/openssl/certs/ca-certificates.crt",
|
||||
];
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Config {
|
||||
@@ -188,7 +204,11 @@ pub async fn poll_loop(status: Arc<SharedStatus>, config: Config, shutdown: Canc
|
||||
loop {
|
||||
match check_once(&config).await {
|
||||
Ok(version) => status.record_success(version),
|
||||
Err(error) => tracing::debug!(%error, "update check failed; retrying on the next interval"),
|
||||
Err(error) => tracing::error!(
|
||||
error = %format!("{error:#}"),
|
||||
version_url = %config.version_url,
|
||||
"update check failed; retrying on the next interval"
|
||||
),
|
||||
}
|
||||
tokio::select! {
|
||||
_ = shutdown.cancelled() => return,
|
||||
@@ -205,8 +225,7 @@ async fn check_once(config: &Config) -> Result<String> {
|
||||
}
|
||||
|
||||
async fn fetch(url: &str) -> Result<String> {
|
||||
let mut roots = RootCertStore::empty();
|
||||
roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
|
||||
let roots = load_root_store();
|
||||
let tls = ClientConfig::builder_with_provider(Arc::new(
|
||||
rustls::crypto::ring::default_provider(),
|
||||
))
|
||||
@@ -223,32 +242,117 @@ async fn fetch(url: &str) -> Result<String> {
|
||||
.enable_http1()
|
||||
.wrap_connector(http);
|
||||
let client: Client<_, Body> = Client::builder(TokioExecutor::new()).build(connector);
|
||||
let uri: Uri = url.parse().context("invalid update version URI")?;
|
||||
let request = Request::builder()
|
||||
.method(http::Method::GET)
|
||||
.uri(uri)
|
||||
.header(header::ACCEPT, "text/plain, text/x-toml;q=0.9")
|
||||
.header(
|
||||
header::USER_AGENT,
|
||||
concat!("RustProxy/", env!("CARGO_PKG_VERSION"), " update-check"),
|
||||
)
|
||||
.body(Body::empty())?;
|
||||
let response = client.request(request).await.context("update version request failed")?;
|
||||
ensure!(
|
||||
response.status().is_success(),
|
||||
"update version request returned HTTP {}",
|
||||
response.status()
|
||||
);
|
||||
let body = Limited::new(response.into_body(), MAX_VERSION_DOCUMENT_BYTES)
|
||||
.collect()
|
||||
.await
|
||||
.map_err(|error| {
|
||||
anyhow::anyhow!(
|
||||
"update version document is too large or incomplete: {error}"
|
||||
let mut current = Url::parse(url).context("invalid update version URL")?;
|
||||
|
||||
for redirects in 0..=MAX_REDIRECTS {
|
||||
let uri: Uri = current
|
||||
.as_str()
|
||||
.parse()
|
||||
.context("invalid update version URI")?;
|
||||
let request = Request::builder()
|
||||
.method(http::Method::GET)
|
||||
.uri(uri)
|
||||
.header(header::ACCEPT, "text/plain, text/x-toml;q=0.9")
|
||||
.header(
|
||||
header::USER_AGENT,
|
||||
concat!("RustProxy/", env!("CARGO_PKG_VERSION"), " update-check"),
|
||||
)
|
||||
})?
|
||||
.to_bytes();
|
||||
String::from_utf8(body.to_vec()).context("update version document is not UTF-8")
|
||||
.body(Body::empty())?;
|
||||
let response = client
|
||||
.request(request)
|
||||
.await
|
||||
.with_context(|| format!("update version request failed for {}", current))?;
|
||||
|
||||
if response.status().is_redirection() {
|
||||
ensure!(
|
||||
redirects < MAX_REDIRECTS,
|
||||
"update version request exceeded redirect limit"
|
||||
);
|
||||
let location = response
|
||||
.headers()
|
||||
.get(header::LOCATION)
|
||||
.context("update version redirect is missing Location")?
|
||||
.to_str()
|
||||
.context("update version redirect Location is not valid UTF-8")?;
|
||||
current = redirect_target(¤t, location)?;
|
||||
continue;
|
||||
}
|
||||
|
||||
ensure!(
|
||||
response.status().is_success(),
|
||||
"update version request returned HTTP {}",
|
||||
response.status()
|
||||
);
|
||||
let body = Limited::new(response.into_body(), MAX_VERSION_DOCUMENT_BYTES)
|
||||
.collect()
|
||||
.await
|
||||
.map_err(|error| {
|
||||
anyhow::anyhow!(
|
||||
"update version document is too large or incomplete: {error}"
|
||||
)
|
||||
})?
|
||||
.to_bytes();
|
||||
return String::from_utf8(body.to_vec())
|
||||
.context("update version document is not UTF-8");
|
||||
}
|
||||
|
||||
unreachable!("redirect loop always returns or fails at the configured limit")
|
||||
}
|
||||
|
||||
fn load_root_store() -> RootCertStore {
|
||||
let mut roots = RootCertStore::empty();
|
||||
roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
|
||||
|
||||
let mut bundles = Vec::<PathBuf>::new();
|
||||
for variable in ["SSL_CERT_FILE", "CURL_CA_BUNDLE"] {
|
||||
if let Some(path) = env::var_os(variable).filter(|value| !value.is_empty()) {
|
||||
bundles.push(PathBuf::from(path));
|
||||
}
|
||||
}
|
||||
bundles.extend(SYSTEM_CA_BUNDLES.iter().map(PathBuf::from));
|
||||
bundles.sort();
|
||||
bundles.dedup();
|
||||
|
||||
for path in bundles {
|
||||
add_system_ca_bundle(&mut roots, &path);
|
||||
}
|
||||
roots
|
||||
}
|
||||
|
||||
fn add_system_ca_bundle(roots: &mut RootCertStore, path: &Path) {
|
||||
let Ok(bytes) = std::fs::read(path) else {
|
||||
return;
|
||||
};
|
||||
let mut reader = BufReader::new(Cursor::new(bytes));
|
||||
let certificates =
|
||||
rustls_pemfile::certs(&mut reader).filter_map(|certificate| certificate.ok());
|
||||
let (added, _ignored) = roots.add_parsable_certificates(certificates);
|
||||
if added > 0 {
|
||||
tracing::debug!(path=%path.display(), added, "loaded system CA certificates for update checker");
|
||||
}
|
||||
}
|
||||
|
||||
fn redirect_target(base: &Url, location: &str) -> Result<Url> {
|
||||
let target = base
|
||||
.join(location)
|
||||
.context("invalid update version redirect URL")?;
|
||||
ensure!(
|
||||
matches!(target.scheme(), "http" | "https"),
|
||||
"update version redirect must use http:// or https://"
|
||||
);
|
||||
ensure!(
|
||||
target.host_str().is_some(),
|
||||
"update version redirect must contain a host"
|
||||
);
|
||||
ensure!(
|
||||
base.scheme() != "https" || target.scheme() == "https",
|
||||
"update version redirect must not downgrade HTTPS to HTTP"
|
||||
);
|
||||
ensure!(
|
||||
target.username().is_empty() && target.password().is_none(),
|
||||
"update version redirect must not contain credentials"
|
||||
);
|
||||
Ok(target)
|
||||
}
|
||||
|
||||
fn package_version(document: &str) -> Result<String> {
|
||||
@@ -345,6 +449,23 @@ foo = { version = "100.0.0" }
|
||||
assert_eq!(package_version(document).unwrap(), "0.19.4");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_redirects_accept_relative_https_and_reject_unsafe_targets() {
|
||||
let base =
|
||||
Url::parse("https://updates.example.invalid/releases/current/Cargo.toml").unwrap();
|
||||
assert_eq!(
|
||||
redirect_target(&base, "../next/Cargo.toml").unwrap().as_str(),
|
||||
"https://updates.example.invalid/releases/next/Cargo.toml"
|
||||
);
|
||||
assert!(redirect_target(&base, "ftp://updates.example.invalid/Cargo.toml").is_err());
|
||||
assert!(redirect_target(&base, "http://updates.example.invalid/Cargo.toml").is_err());
|
||||
assert!(redirect_target(
|
||||
&base,
|
||||
"https://user:secret@updates.example.invalid/Cargo.toml"
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compares_release_versions() {
|
||||
assert!(is_newer_version("0.19.5", "0.19.4"));
|
||||
|
||||
Reference in new issue
Block a user