diff --git a/git-repository/src/config/cache/init.rs b/git-repository/src/config/cache/init.rs index a6ce3a7920a..d9d65f533ba 100644 --- a/git-repository/src/config/cache/init.rs +++ b/git-repository/src/config/cache/init.rs @@ -273,6 +273,8 @@ fn apply_environment_overrides( ("GIT_HTTP_PROXY_AUTHMETHOD", "proxyAuthMethod"), ("all_proxy", "all-proxy-lower"), ("ALL_PROXY", "all-proxy"), + ("GIT_SSL_CAINFO", "sslCAInfo"), + ("GIT_SSL_VERSION", "sslVersion"), ] { if let Some(value) = var_as_bstring(var, http_transport) { section.push_with_comment( diff --git a/git-repository/src/config/mod.rs b/git-repository/src/config/mod.rs index 7632312fdfa..f11553a1b88 100644 --- a/git-repository/src/config/mod.rs +++ b/git-repository/src/config/mod.rs @@ -139,6 +139,11 @@ pub mod transport { source: git_config::value::Error, key: &'static str, }, + #[error("Could not interpolate path at key {key:?}")] + InterpolatePath { + source: git_config::path::interpolate::Error, + key: &'static str, + }, #[error("Could not decode value at key {key:?} as UTF-8 string")] IllformedUtf8 { key: Cow<'static, BStr>, @@ -154,7 +159,7 @@ pub mod transport { pub mod http { use std::borrow::Cow; - use crate::bstr::BStr; + use crate::bstr::{BStr, BString}; /// The error produced when configuring a HTTP transport. #[derive(Debug, thiserror::Error)] @@ -164,6 +169,10 @@ pub mod transport { InvalidProxyAuthMethod { value: String, key: Cow<'static, BStr> }, #[error("Could not configure the credential helpers for the authenticated proxy url")] ConfigureProxyAuthenticate(#[from] crate::config::snapshot::credential_helpers::Error), + #[error("The SSL version at key `{key} named {name:?} is unknown")] + InvalidSslVersion { key: &'static str, name: BString }, + #[error("The HTTP version at key `{key} named {name:?} is unknown")] + InvalidHttpVersion { key: &'static str, name: BString }, } } } diff --git a/git-repository/src/repository/config/transport.rs b/git-repository/src/repository/config/transport.rs index 8e8da842b23..2271cf6ff88 100644 --- a/git-repository/src/repository/config/transport.rs +++ b/git-repository/src/repository/config/transport.rs @@ -47,6 +47,7 @@ impl crate::Repository { sync::{Arc, Mutex}, }; + use git_transport::client::http::options::{HttpVersion, SslVersion, SslVersionRangeInclusive}; use git_transport::client::{http, http::options::ProxyAuthMethod}; use crate::{bstr::ByteVec, config::cache::util::ApplyLeniency}; @@ -142,6 +143,39 @@ impl crate::Repository { Ok(value) } + fn ssl_version( + config: &git_config::File<'static>, + key: &'static str, + mut filter: fn(&git_config::file::Metadata) -> bool, + lenient: bool, + ) -> Result, crate::config::transport::Error> { + config + .string_filter_by_key(key, &mut filter) + .filter(|v| !v.is_empty()) + .map(|v| { + use git_protocol::transport::client::http::options::SslVersion::*; + Ok(match v.as_ref().as_ref() { + b"default" => Default, + b"tlsv1" => TlsV1, + b"sslv2" => SslV2, + b"sslv3" => SslV3, + b"tlsv1.0" => TlsV1_0, + b"tlsv1.1" => TlsV1_1, + b"tlsv1.2" => TlsV1_2, + b"tlsv1.3" => TlsV1_3, + _ => { + return Err(crate::config::transport::http::Error::InvalidSslVersion { + key, + name: v.into_owned(), + }) + } + }) + }) + .transpose() + .with_leniency(lenient) + .map_err(Into::into) + } + fn proxy( value: Option<(Cow<'_, BStr>, Cow<'static, BStr>)>, lenient: bool, @@ -286,18 +320,101 @@ impl crate::Repository { opts.connect_timeout = integer_opt(config, lenient, "gitoxide.http.connectTimeout", "u64", trusted_only)? .map(std::time::Duration::from_millis); - opts.user_agent = config - .string_filter_by_key("http.userAgent", &mut trusted_only) - .and_then(|v| try_cow_to_string(v, lenient, Cow::Borrowed("http.userAgent".into())).transpose()) - .transpose()? - .or_else(|| Some(crate::env::agent().into())); - let key = "gitoxide.http.verbose"; - opts.verbose = config - .boolean_filter_by_key(key, &mut trusted_only) - .transpose() - .with_leniency(lenient) - .map_err(|err| crate::config::transport::Error::ConfigValue { source: err, key })? - .unwrap_or_default(); + { + let key = "http.userAgent"; + opts.user_agent = config + .string_filter_by_key(key, &mut trusted_only) + .and_then(|v| try_cow_to_string(v, lenient, Cow::Borrowed(key.into())).transpose()) + .transpose()? + .or_else(|| Some(crate::env::agent().into())); + } + + { + let key = "http.version"; + opts.http_version = config + .string_filter_by_key(key, &mut trusted_only) + .map(|v| { + Ok(match v.as_ref().as_ref() { + b"HTTP/1.1" => HttpVersion::V1_1, + b"HTTP/2" => HttpVersion::V2, + _ => { + return Err(crate::config::transport::http::Error::InvalidHttpVersion { + name: v.into_owned(), + key, + }) + } + }) + }) + .transpose()?; + } + + { + let key = "gitoxide.http.verbose"; + opts.verbose = config + .boolean_filter_by_key(key, &mut trusted_only) + .transpose() + .with_leniency(lenient) + .map_err(|err| crate::config::transport::Error::ConfigValue { source: err, key })? + .unwrap_or_default(); + } + + let may_use_cainfo = { + let key = "http.schannelUseSSLCAInfo"; + config + .boolean_filter_by_key(key, &mut trusted_only) + .transpose() + .with_leniency(lenient) + .map_err(|err| crate::config::transport::Error::ConfigValue { source: err, key })? + .unwrap_or(true) + }; + + if may_use_cainfo { + let key = "http.sslCAInfo"; + opts.ssl_ca_info = config + .path_filter_by_key(key, &mut trusted_only) + .map(|p| { + use crate::config::cache::interpolate_context; + p.interpolate(interpolate_context( + self.install_dir().ok().as_deref(), + self.config.home_dir().as_deref(), + )) + .map(|cow| cow.into_owned()) + }) + .transpose() + .with_leniency(lenient) + .map_err(|err| crate::config::transport::Error::InterpolatePath { source: err, key })?; + } + + { + opts.ssl_version = ssl_version(config, "http.sslVersion", trusted_only, lenient)? + .map(|v| SslVersionRangeInclusive { min: v, max: v }); + let min_max = ssl_version(config, "gitoxide.http.sslVersionMin", trusted_only, lenient) + .and_then(|min| { + ssl_version(config, "gitoxide.http.sslVersionMax", trusted_only, lenient) + .map(|max| min.and_then(|min| max.map(|max| (min, max)))) + })?; + if let Some((min, max)) = min_max { + let v = opts.ssl_version.get_or_insert_with(|| SslVersionRangeInclusive { + min: SslVersion::TlsV1_3, + max: SslVersion::TlsV1_3, + }); + v.min = min; + v.max = max; + } + } + + #[cfg(feature = "blocking-http-transport-curl")] + { + let key = "http.schannelCheckRevoke"; + let schannel_check_revoke = config + .boolean_filter_by_key(key, &mut trusted_only) + .transpose() + .with_leniency(lenient) + .map_err(|err| crate::config::transport::Error::ConfigValue { source: err, key })?; + let backend = git_protocol::transport::client::http::curl::Options { schannel_check_revoke }; + opts.backend = + Some(Arc::new(Mutex::new(backend)) as Arc>); + } Ok(Some(Box::new(opts))) } diff --git a/git-repository/tests/fixtures/generated-archives/make_config_repos.tar.xz b/git-repository/tests/fixtures/generated-archives/make_config_repos.tar.xz index 3c5c3b9d700..3bd3b394cde 100644 --- a/git-repository/tests/fixtures/generated-archives/make_config_repos.tar.xz +++ b/git-repository/tests/fixtures/generated-archives/make_config_repos.tar.xz @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f90e312e56b67e6da1596b444bb4a266238b28b7ca6744b59f650ab86ec195fa -size 14404 +oid sha256:efd00ab741a24adfab823d76b5ae486b4448cbc8ae82ba0d5fd8cf9862782337 +size 15232 diff --git a/git-repository/tests/fixtures/make_config_repos.sh b/git-repository/tests/fixtures/make_config_repos.sh index 46b0f88013b..c289405eb77 100644 --- a/git-repository/tests/fixtures/make_config_repos.sh +++ b/git-repository/tests/fixtures/make_config_repos.sh @@ -14,6 +14,10 @@ git init http-config git config http.proxyAuthMethod basic git config http.userAgent agentJustForHttp git config gitoxide.http.connectTimeout 60k + git config http.schannelCheckRevoke true + git config http.sslCAInfo ./CA.pem + git config http.sslVersion sslv2 + git config http.version HTTP/1.1 ) git clone --shared http-config http-remote-override @@ -33,6 +37,23 @@ git init http-no-proxy git config gitoxide.http.noProxy "no validation done here" ) +git init http-ssl-version-min-max +(cd http-ssl-version-min-max + git config http.sslVersion sslv3 + git config gitoxide.http.sslVersionMin tlsv1.1 + git config gitoxide.http.sslVersionMax tlsv1.2 +) + +git init http-ssl-version-default +(cd http-ssl-version-default + git config http.sslVersion default +) + +git init http-disabled-cainfo +(cd http-disabled-cainfo + git config http.sslCAInfo ./CA.pem + git config http.schannelUseSSLCAInfo false +) git init http-proxy-empty (cd http-proxy-empty diff --git a/git-repository/tests/repository/config/transport_options.rs b/git-repository/tests/repository/config/transport_options.rs index ec3c708f6cf..6bd9e171835 100644 --- a/git-repository/tests/repository/config/transport_options.rs +++ b/git-repository/tests/repository/config/transport_options.rs @@ -4,11 +4,20 @@ ))] mod http { use git_repository as git; - use git_transport::client::http::options::{FollowRedirects, ProxyAuthMethod}; + use git_transport::client::http::options::{ + FollowRedirects, HttpVersion, ProxyAuthMethod, SslVersion, SslVersionRangeInclusive, + }; pub(crate) fn repo(name: &str) -> git::Repository { + repo_opts(name, |opts| opts.strict_config(true)) + } + + pub(crate) fn repo_opts( + name: &str, + modify: impl FnOnce(git::open::Options) -> git::open::Options, + ) -> git::Repository { let dir = git_testtools::scripted_fixture_read_only("make_config_repos.sh").unwrap(); - git::open_opts(dir.join(name), git::open::Options::isolated()).unwrap() + git::open_opts(dir.join(name), modify(git::open::Options::isolated())).unwrap() } fn http_options( @@ -55,6 +64,9 @@ mod http { user_agent, connect_timeout, verbose, + ssl_ca_info, + ssl_version, + http_version, backend, } = http_options(&repo, None, "https://example.com/does/not/matter"); assert_eq!( @@ -75,10 +87,80 @@ mod http { assert_eq!(connect_timeout, Some(std::time::Duration::from_millis(60 * 1024))); assert_eq!(no_proxy, None); assert!(!verbose, "verbose is disabled by default"); + assert_eq!(ssl_ca_info.as_deref(), Some(std::path::Path::new("./CA.pem"))); + #[cfg(feature = "blocking-http-transport-reqwest")] + { + assert!( + backend.is_none(), + "backed is never set as it's backend specific, rather custom options typically" + ) + } + #[cfg(feature = "blocking-http-transport-curl")] + { + let backend = backend + .as_ref() + .map(|b| b.lock().expect("not poisoned")) + .expect("backend is set for curl due to specific options"); + match backend.downcast_ref::() { + Some(opts) => { + assert_eq!(opts.schannel_check_revoke, Some(true)); + } + None => panic!("Correct backend option type is used"), + } + } + + let version = SslVersion::SslV2; + assert_eq!( + ssl_version, + Some(SslVersionRangeInclusive { + min: version, + max: version + }) + ); + assert_eq!(http_version, Some(HttpVersion::V1_1)); + } + + #[test] + fn http_ssl_cainfo_suppressed_by_() { + let repo = repo("http-disabled-cainfo"); + let opts = http_options(&repo, None, "https://example.com/does/not/matter"); assert!( - backend.is_none(), - "backed is never set as it's backend specific, rather custom options typically" - ) + opts.ssl_ca_info.is_none(), + "http.schannelUseSSLCAInfo is explicitly set and prevents the ssl_ca_info to be set" + ); + } + + #[test] + fn http_ssl_version_min_max_overrides_ssl_version() { + let repo = repo("http-ssl-version-min-max"); + let opts = http_options(&repo, None, "https://example.com/does/not/matter"); + assert_eq!( + opts.ssl_version, + Some(SslVersionRangeInclusive { + min: SslVersion::TlsV1_1, + max: SslVersion::TlsV1_2 + }) + ); + } + + #[test] + fn http_ssl_version_default() { + let repo = repo("http-ssl-version-default"); + let opts = http_options(&repo, None, "https://example.com/does/not/matter"); + assert_eq!( + opts.ssl_version, + Some(SslVersionRangeInclusive { + min: SslVersion::Default, + max: SslVersion::Default + }) + ); + } + + #[test] + fn http_ssl_version_empty_resets_prior_values() { + let repo = repo_opts("http-config", |opts| opts.config_overrides(["http.sslVersion="])); + let opts = http_options(&repo, None, "https://example.com/does/not/matter"); + assert!(opts.ssl_version.is_none(), "empty strings reset what was there"); } #[test] diff --git a/git-repository/tests/repository/open.rs b/git-repository/tests/repository/open.rs index 9f06ea32420..6e5c5b31454 100644 --- a/git-repository/tests/repository/open.rs +++ b/git-repository/tests/repository/open.rs @@ -184,17 +184,23 @@ mod with_overrides { .set("GIT_AUTHOR_DATE", default_date) .set("EMAIL", "user email") .set("GITOXIDE_PACK_CACHE_MEMORY", "0") - .set("GITOXIDE_OBJECT_CACHE_MEMORY", "5m"); + .set("GITOXIDE_OBJECT_CACHE_MEMORY", "5m") + .set("GIT_SSL_CAINFO", "./env.pem") + .set("GIT_SSL_VERSION", "tlsv1.3"); let mut opts = git::open::Options::isolated() - .config_overrides([ - "http.userAgent=agent-from-api", - "http.lowSpeedLimit=2", - "http.lowSpeedTime=2", - ]) .cli_overrides([ "http.userAgent=agent-from-cli", "http.lowSpeedLimit=3", "http.lowSpeedTime=3", + "http.sslCAInfo=./cli.pem", + "http.sslVersion=sslv3", + ]) + .config_overrides([ + "http.userAgent=agent-from-api", + "http.lowSpeedLimit=2", + "http.lowSpeedTime=2", + "http.sslCAInfo=./api.pem", + "http.sslVersion=tlsv1", ]); opts.permissions.env.git_prefix = Permission::Allow; opts.permissions.env.http_transport = Permission::Allow; @@ -268,6 +274,24 @@ mod with_overrides { cow_bstr(if cfg!(windows) { "no-proxy" } else { "no-proxy-lower" }) ] ); + assert_eq!( + config.strings_by_key("http.sslCAInfo").expect("at least one value"), + [ + cow_bstr("./CA.pem"), + cow_bstr("./cli.pem"), + cow_bstr("./api.pem"), + cow_bstr("./env.pem") + ] + ); + assert_eq!( + config.strings_by_key("http.sslVersion").expect("at least one value"), + [ + cow_bstr("sslv2"), + cow_bstr("sslv3"), + cow_bstr("tlsv1"), + cow_bstr("tlsv1.3") + ] + ); for (key, expected) in [ ("gitoxide.http.verbose", "true"), ("gitoxide.allow.protocolFromUser", "file-allowed"), diff --git a/src/plumbing/progress.rs b/src/plumbing/progress.rs index 29d8af93cac..15ceb11a96a 100644 --- a/src/plumbing/progress.rs +++ b/src/plumbing/progress.rs @@ -599,7 +599,7 @@ static GIT_CONFIG: &[Record] = &[ }, Record { config: "http.version", - usage: NotPlanned { reason: "on demand" } + usage: InModule { name: "repository::config::transport", deviation: None } }, Record { config: "http.curloptResolve", @@ -607,7 +607,7 @@ static GIT_CONFIG: &[Record] = &[ }, Record { config: "http.sslVersion", - usage: NotPlanned { reason: "on demand" } + usage: InModule { name: "repository::config::transport", deviation: Some("Adds a new 'default' value to keep the implementation default explicitly.") } }, Record { config: "http.sslCipherList", @@ -639,7 +639,7 @@ static GIT_CONFIG: &[Record] = &[ }, Record { config: "http.sslCAInfo", - usage: NotPlanned { reason: "on demand" } + usage: InModule { name: "repository::config::transport", deviation: None } }, Record { config: "http.sslCAPath", @@ -655,7 +655,7 @@ static GIT_CONFIG: &[Record] = &[ }, Record { config: "http.schannelUseSSLCAInfo", - usage: NotPlanned { reason: "on demand" } + usage: InModule { name: "repository::config::transport", deviation: Some("only used as switch internally to turn off using the sslCAInfo, unconditionally. If unset, it has no effect, whereas in `git` it defaults to false.") } }, Record { config: "http.pinnedPubkey", @@ -778,6 +778,13 @@ static GIT_CONFIG: &[Record] = &[ deviation: Some("created from 'no_proxy' or 'NO_PROXY' env-var.") } }, + Record { + config: "gitoxide.http.multiplexing", + usage: InModule { + name: "repository::config::transport", + deviation: Some("Takes boolean true or false values, default true, and can take effect only if `http.version` is set to 'HTTP/2'") + } + }, Record { config: "gitoxide.http.connectTimeout", usage: InModule { @@ -785,6 +792,20 @@ static GIT_CONFIG: &[Record] = &[ deviation: Some("entirely new, and in milliseconds like all other timeout suffixed variables in the git config") } }, + Record { + config: "gitoxide.http.sslVersionMin", + usage: InModule { + name: "repository::config::transport", + deviation: Some("entirely new to set the lower bound for the allowed ssl version range. Overwrites the min bound of `http.sslVersion` if set. Min and Max must be set to become effective.") + } + }, + Record { + config: "gitoxide.http.sslVersionMax", + usage: InModule { + name: "repository::config::transport", + deviation: Some("entirely new to set the upper bound for the allowed ssl version range. Overwrites the max bound of `http.sslVersion` if set. Min and Max must be set to become effective.") + } + }, Record { config: "gitoxide.allow.protocolFromUser", usage: InModule {