Use repository Git remotes and SSH config
Some checks failed
Dependency security audit / rustsec (push) Has been cancelled
Some checks failed
Dependency security audit / rustsec (push) Has been cancelled
This commit is contained in:
@@ -24,7 +24,10 @@ use russh::{
|
||||
use sha1::Sha1;
|
||||
|
||||
use crate::{
|
||||
config::{GitRemote, RemoteEndpoint, SshFingerprint, SshIdentitySource, SshRepositoryPath},
|
||||
config::{
|
||||
GitRemote, RemoteEndpoint, SshEndpoint, SshFingerprint, SshIdentitySource,
|
||||
SshRemoteAuthentication, SshRepositoryPath,
|
||||
},
|
||||
git::{GitError, GitOperationControl, GitProgressPhase, SshHostKey, SshPassphraseProvider},
|
||||
};
|
||||
|
||||
@@ -124,18 +127,16 @@ impl SshSession {
|
||||
let RemoteEndpoint::Ssh(endpoint) = remote.endpoint() else {
|
||||
return Err(GitError::ForbiddenRemoteUrl);
|
||||
};
|
||||
let authentication = remote
|
||||
.ssh_authentication()
|
||||
.ok_or(GitError::SshAuthenticationNotConfigured)?;
|
||||
let connection = resolve_connection(endpoint, remote.ssh_authentication())?;
|
||||
control.report(GitProgressPhase::Validating)?;
|
||||
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.map_err(|_| GitError::SshProtocolFailed)?;
|
||||
let host = endpoint.host().to_owned();
|
||||
let port = endpoint.port();
|
||||
let known_hosts = authentication.known_hosts_file().to_owned();
|
||||
let host = connection.host.clone();
|
||||
let port = connection.port;
|
||||
let known_hosts = connection.known_hosts.clone();
|
||||
let verifier = HostVerifier {
|
||||
host: host.clone(),
|
||||
port,
|
||||
@@ -159,8 +160,8 @@ impl SshSession {
|
||||
};
|
||||
|
||||
control.report(GitProgressPhase::Authenticating)?;
|
||||
let user = endpoint.user().unwrap_or("git");
|
||||
let authenticated = match authentication.identity() {
|
||||
let user = connection.user.as_deref().unwrap_or("git");
|
||||
let authenticated = match &connection.identity {
|
||||
SshIdentitySource::KeyFile(path) => runtime.block_on(authenticate_key_file(
|
||||
&mut handle,
|
||||
user,
|
||||
@@ -289,6 +290,150 @@ impl SshSession {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SshConnection {
|
||||
pub(crate) host: String,
|
||||
pub(crate) port: u16,
|
||||
user: Option<String>,
|
||||
identity: SshIdentitySource,
|
||||
pub(crate) known_hosts: PathBuf,
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_connection(
|
||||
endpoint: &SshEndpoint,
|
||||
authentication: Option<&SshRemoteAuthentication>,
|
||||
) -> Result<SshConnection, GitError> {
|
||||
let home = home_directory().ok_or(GitError::SshAuthenticationNotConfigured)?;
|
||||
let mut connection = SshConnection {
|
||||
host: endpoint.host().to_owned(),
|
||||
port: endpoint.port(),
|
||||
user: endpoint.user().map(str::to_owned),
|
||||
identity: authentication
|
||||
.map(|authentication| authentication.identity().clone())
|
||||
.unwrap_or_else(|| SshIdentitySource::KeyFile(home.join(".ssh/id_ed25519"))),
|
||||
known_hosts: authentication
|
||||
.map(|authentication| authentication.known_hosts_file().to_owned())
|
||||
.unwrap_or_else(|| home.join(".ssh/known_hosts")),
|
||||
};
|
||||
apply_openssh_config(
|
||||
&home.join(".ssh/config"),
|
||||
endpoint.host(),
|
||||
&home,
|
||||
authentication.is_none(),
|
||||
&mut connection,
|
||||
)?;
|
||||
Ok(connection)
|
||||
}
|
||||
|
||||
fn home_directory() -> Option<PathBuf> {
|
||||
#[cfg(target_os = "windows")]
|
||||
let home = std::env::var_os("USERPROFILE");
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let home = std::env::var_os("HOME");
|
||||
home.filter(|home| !home.is_empty()).map(PathBuf::from)
|
||||
}
|
||||
|
||||
fn apply_openssh_config(
|
||||
path: &Path,
|
||||
alias: &str,
|
||||
home: &Path,
|
||||
use_identity: bool,
|
||||
connection: &mut SshConnection,
|
||||
) -> Result<(), GitError> {
|
||||
let Ok(contents) = fs::read_to_string(path) else {
|
||||
return Ok(());
|
||||
};
|
||||
let mut active = true;
|
||||
let mut hostname_set = false;
|
||||
let mut user_set = connection.user.is_some();
|
||||
let mut port_set = connection.port != 22;
|
||||
let mut identity_set = !use_identity;
|
||||
for line in contents.lines() {
|
||||
let Some(words) = shlex::split(line.split('#').next().unwrap_or_default()) else {
|
||||
return Err(GitError::SshAuthenticationNotConfigured);
|
||||
};
|
||||
let Some((keyword, values)) = words.split_first() else {
|
||||
continue;
|
||||
};
|
||||
if keyword.eq_ignore_ascii_case("host") {
|
||||
active = host_patterns_match(values, alias);
|
||||
} else if active && keyword.eq_ignore_ascii_case("hostname") && !hostname_set {
|
||||
if let Some(value) = values.first() {
|
||||
connection.host = value.clone();
|
||||
hostname_set = true;
|
||||
}
|
||||
} else if active && keyword.eq_ignore_ascii_case("user") && !user_set {
|
||||
if let Some(value) = values.first() {
|
||||
connection.user = Some(value.clone());
|
||||
user_set = true;
|
||||
}
|
||||
} else if active && keyword.eq_ignore_ascii_case("port") && !port_set {
|
||||
if let Some(value) = values.first() {
|
||||
connection.port = value
|
||||
.parse()
|
||||
.ok()
|
||||
.filter(|port| *port != 0)
|
||||
.ok_or(GitError::SshAuthenticationNotConfigured)?;
|
||||
port_set = true;
|
||||
}
|
||||
} else if active
|
||||
&& keyword.eq_ignore_ascii_case("identityfile")
|
||||
&& !identity_set
|
||||
&& let Some(value) = values.first()
|
||||
{
|
||||
let expanded = value
|
||||
.replace("%h", &connection.host)
|
||||
.replace("%p", &connection.port.to_string())
|
||||
.replace("%r", connection.user.as_deref().unwrap_or("git"));
|
||||
let identity = expanded
|
||||
.strip_prefix("~/")
|
||||
.map_or_else(|| PathBuf::from(&expanded), |path| home.join(path));
|
||||
connection.identity = SshIdentitySource::KeyFile(identity);
|
||||
identity_set = true;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn host_patterns_match(patterns: &[String], host: &str) -> bool {
|
||||
let mut matched = false;
|
||||
for pattern in patterns {
|
||||
let (negated, pattern) = pattern
|
||||
.strip_prefix('!')
|
||||
.map_or((false, pattern.as_str()), |pattern| (true, pattern));
|
||||
if wildcard_match(pattern.as_bytes(), host.as_bytes()) {
|
||||
if negated {
|
||||
return false;
|
||||
}
|
||||
matched = true;
|
||||
}
|
||||
}
|
||||
matched
|
||||
}
|
||||
|
||||
fn wildcard_match(pattern: &[u8], value: &[u8]) -> bool {
|
||||
let (mut pattern_index, mut value_index, mut star, mut retry) = (0, 0, None, 0);
|
||||
while value_index < value.len() {
|
||||
if pattern
|
||||
.get(pattern_index)
|
||||
.is_some_and(|byte| *byte == b'?' || byte.eq_ignore_ascii_case(&value[value_index]))
|
||||
{
|
||||
pattern_index += 1;
|
||||
value_index += 1;
|
||||
} else if pattern.get(pattern_index) == Some(&b'*') {
|
||||
star = Some(pattern_index);
|
||||
pattern_index += 1;
|
||||
retry = value_index;
|
||||
} else if let Some(star_index) = star {
|
||||
pattern_index = star_index + 1;
|
||||
retry += 1;
|
||||
value_index = retry;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
pattern[pattern_index..].iter().all(|byte| *byte == b'*')
|
||||
}
|
||||
|
||||
fn client_config() -> client::Config {
|
||||
client::Config {
|
||||
inactivity_timeout: Some(CONNECTION_TIMEOUT),
|
||||
@@ -1034,6 +1179,39 @@ mod tests {
|
||||
server,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn openssh_host_alias_resolves_connection_and_identity() {
|
||||
let temporary = tempfile::tempdir().expect("temporary directory");
|
||||
let config = temporary.path().join("config");
|
||||
fs::write(
|
||||
&config,
|
||||
"Host *\n IdentityFile ~/.ssh/id_ed25519\nHost git.example\n HostName internal.example\n User git\n Port 2222\n",
|
||||
)
|
||||
.expect("SSH config");
|
||||
let mut connection = super::SshConnection {
|
||||
host: "git.example".to_owned(),
|
||||
port: 22,
|
||||
user: None,
|
||||
identity: crate::config::SshIdentitySource::KeyFile(temporary.path().join("unused")),
|
||||
known_hosts: temporary.path().join("known_hosts"),
|
||||
};
|
||||
super::apply_openssh_config(
|
||||
&config,
|
||||
"git.example",
|
||||
temporary.path(),
|
||||
true,
|
||||
&mut connection,
|
||||
)
|
||||
.expect("resolve SSH config");
|
||||
assert_eq!(connection.host, "internal.example");
|
||||
assert_eq!(connection.user.as_deref(), Some("git"));
|
||||
assert_eq!(connection.port, 2222);
|
||||
assert_eq!(
|
||||
connection.identity.key_file(),
|
||||
Some(temporary.path().join(".ssh/id_ed25519").as_path())
|
||||
);
|
||||
}
|
||||
|
||||
use crate::{
|
||||
config::{GitRemote, RemoteEndpoint, SshFingerprint, SshRemoteAuthentication},
|
||||
git::{GitError, GitOperationControl, SshPassphraseProvider},
|
||||
|
||||
Reference in New Issue
Block a user