-
Notifications
You must be signed in to change notification settings - Fork 1
spawn ssh_agent instead of trying files manually #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
515150b
12ad39b
e4e5f3d
d6dedd9
e98c5a3
5c7d06f
b5d6b01
21df2e3
3db14ba
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| use git2::{CertificateCheckStatus, CredentialType, RemoteCallbacks}; | ||
| use log::debug; | ||
| use std::sync::{Arc, Mutex}; | ||
|
|
||
| use crate::auth::{git_http::try_userpass_authentication, git_ssh::ssh_authenticate_git}; | ||
|
|
||
| pub fn setup_auth_callbacks() -> RemoteCallbacks<'static> { | ||
| let mut callbacks = RemoteCallbacks::new(); | ||
|
|
||
| // Track attempt count across callback invocations | ||
| let attempt_count: Arc<Mutex<usize>> = Arc::new(Mutex::new(0)); | ||
|
|
||
| callbacks.credentials(move |url, username_from_url, allowed_types| { | ||
| let mut count = attempt_count.lock().unwrap(); | ||
| *count += 1; | ||
| let current_attempt = *count; | ||
| drop(count); | ||
|
|
||
| if allowed_types.contains(CredentialType::USER_PASS_PLAINTEXT) { | ||
| try_userpass_authentication(username_from_url) | ||
| } else { | ||
| ssh_authenticate_git(url, username_from_url, allowed_types, current_attempt) | ||
| } | ||
| }); | ||
|
|
||
| // Set up certificate check callback for HTTPS | ||
| callbacks.certificate_check(|_cert, _host| { | ||
| debug!("Skipping certificate verification (INSECURE)"); | ||
| Ok(CertificateCheckStatus::CertificateOk) | ||
| }); | ||
|
|
||
| callbacks | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| use dialoguer::{Input, Password, theme::ColorfulTheme}; | ||
| use git2::{Cred, Error, ErrorClass, ErrorCode}; | ||
| use log::debug; | ||
|
|
||
| pub fn try_userpass_authentication(username_from_url: Option<&str>) -> Result<Cred, Error> { | ||
| debug!("USER_PASS_PLAINTEXT authentication is allowed, prompting for credentials"); | ||
|
|
||
| // Prompt for username if not provided in URL | ||
| let username = if let Some(user) = username_from_url { | ||
| user.to_string() | ||
| } else { | ||
| Input::<String>::with_theme(&ColorfulTheme::default()) | ||
| .with_prompt("Enter your username") | ||
| .interact() | ||
| .map_err(|e| { | ||
| Error::new( | ||
| ErrorCode::Auth, | ||
| ErrorClass::Net, | ||
| format!("Failed to read username: {e}"), | ||
| ) | ||
| })? | ||
| }; | ||
|
|
||
| let token = Password::with_theme(&ColorfulTheme::default()) | ||
| .with_prompt("Enter your personal access token") | ||
| .interact() | ||
| .map_err(|e| { | ||
| Error::new( | ||
| ErrorCode::Auth, | ||
| ErrorClass::Net, | ||
| format!("Failed to read token: {e}"), | ||
| ) | ||
| })?; | ||
|
|
||
| if !username.is_empty() && !token.is_empty() { | ||
| debug!("Creating credentials with username and token"); | ||
| match Cred::userpass_plaintext(&username, &token) { | ||
| Ok(cred) => { | ||
| debug!("Username/token authentication succeeded"); | ||
| Ok(cred) | ||
| } | ||
| Err(e) => { | ||
| debug!("Username/token authentication failed: {e}"); | ||
| Err(e) | ||
| } | ||
| } | ||
| } else { | ||
| debug!("Username or token is empty, skipping userpass authentication"); | ||
| Err(Error::new( | ||
| ErrorCode::Auth, | ||
| ErrorClass::Net, | ||
| "Username or token cannot be empty", | ||
| )) | ||
| } | ||
| } |
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. check windows and mac compatibility! |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,250 @@ | ||
| use git2::{Cred, CredentialType, Error, ErrorClass, ErrorCode}; | ||
| use log::debug; | ||
| use std::path::Path; | ||
| use std::process::{Command, Stdio}; | ||
|
|
||
| use crate::auth::ssh_utils::{add_key_interactive, parse_ssh_agent_output}; | ||
|
|
||
| pub fn ssh_authenticate_git( | ||
| url: &str, | ||
| username_from_url: Option<&str>, | ||
| allowed_types: CredentialType, | ||
| attempt_count: usize, | ||
| ) -> Result<Cred, Error> { | ||
| debug!("Git authentication attempt #{attempt_count} for URL: {url}"); | ||
| debug!("Username from URL: {username_from_url:?}"); | ||
| debug!("Allowed credential types: {allowed_types:?}"); | ||
|
|
||
| // Prevent infinite loops | ||
| if attempt_count > 3 { | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. make MAX_ATTEMPT_COUNT configurable using constants |
||
| debug!( | ||
| "Too many authentication attempts ({attempt_count}), failing to prevent infinite loop" | ||
| ); | ||
| return Err(Error::new( | ||
| ErrorCode::Auth, | ||
| ErrorClass::Net, | ||
| "Too many authentication attempts", | ||
| )); | ||
| } | ||
|
|
||
| if allowed_types.contains(CredentialType::SSH_KEY) { | ||
| if let Some(username) = username_from_url { | ||
| debug!("SSH key authentication is allowed, trying SSH agent"); | ||
|
|
||
| // handling the case where ssh-agent is running but empty | ||
| if attempt_count == 2 { | ||
|
Comment on lines
+34
to
+35
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. code smell, remove it!
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you can use
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| debug!("Second attempt: trying to add SSH keys to agent before authentication"); | ||
| if std::env::var("SSH_AUTH_SOCK").is_ok() { | ||
| if let Err(e) = add_all_ssh_keys() { | ||
| debug!("Failed to add keys to ssh-agent on second attempt: {e}"); | ||
| } else { | ||
| debug!("Keys added to ssh-agent, proceeding with authentication"); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if let Ok(cred) = try_ssh_agent_auth(username) { | ||
| return Ok(cred); | ||
| } | ||
| } else { | ||
| debug!("No username provided for SSH authentication"); | ||
| } | ||
| } | ||
|
|
||
| debug!("All authentication methods failed for attempt {attempt_count}"); | ||
| Err(Error::new( | ||
| ErrorCode::Auth, | ||
| ErrorClass::Net, | ||
| format!("Authentication failed - attempt {attempt_count}"), | ||
| )) | ||
| } | ||
|
|
||
| fn try_ssh_agent_auth(username: &str) -> Result<Cred, Error> { | ||
| debug!("Attempting SSH agent authentication for user: {username}"); | ||
|
|
||
| if std::env::var("SSH_AUTH_SOCK").is_err() { | ||
| debug!("SSH_AUTH_SOCK not set, attempting to spawn ssh-agent and add keys"); | ||
| spawn_ssh_agent_and_add_keys()?; | ||
| } | ||
|
Comment on lines
+65
to
+68
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. use the update logic for ssh-agent with a global sock file ### ── Persistent ssh‑agent (portable version) ──────────────────────────
SOCKET="$HOME/.ssh/ssh-agent.sock" # fixed socket location
STARTUP_KEY="$HOME/.ssh/id_ed25519" # pre‑load this key if none loaded
# Ensure ~/.ssh exists with correct perms
[[ -d $HOME/.ssh ]] || mkdir -m 700 "$HOME/.ssh"
export SSH_AUTH_SOCK="$SOCKET"
# Helper: true if an agent is listening on our socket
_agent_alive() {
[[ -S $SOCKET ]] || return 1
pgrep -u "$UID" -f "ssh-agent.*${SOCKET//\//\\/}" >/dev/null 2>&1
}
# Clean up stale socket
[[ -e $SOCKET ]] && ! _agent_alive && rm -f "$SOCKET"
# Start agent if needed
if ! _agent_alive; then
echo "[ssh‑agent] launching new agent bound to $SOCKET"
if command -v setsid >/dev/null; then
DETACH="setsid"
elif command -v nohup >/dev/null; then
DETACH="nohup"
else
DETACH=""
fi
if ssh-agent -h 2>&1 | grep -q -- '-a'; then
$DETACH ssh-agent -a "$SOCKET" -D >/dev/null 2>&1 &
else
$DETACH ssh-agent -D >/dev/null 2>&1 &
fi
for _ in {1..20}; do [[ -S $SOCKET ]] && break; sleep 0.1; done
fi
# Load at least one key (prompt once per boot/session)
if _agent_alive && ! ssh-add -l >/dev/null 2>&1; then
[[ -f $STARTUP_KEY ]] && ssh-add "$STARTUP_KEY" || ssh-add
fi
# House‑keeping: remove helpers
unset -f _agent_alive # delete the function
unset DETACH # delete the temp variable
### ───────────────────────────────────────────────────────────────────── |
||
|
|
||
| match Cred::ssh_key_from_agent(username) { | ||
| Ok(cred) => { | ||
| debug!("SSH agent authentication succeeded"); | ||
|
|
||
| Ok(cred) | ||
| } | ||
| Err(e) => { | ||
| debug!("SSH agent authentication failed: {e}"); | ||
|
|
||
| // Fallback to trying SSH key files directly | ||
| debug!("Falling back to direct SSH key file authentication"); | ||
| try_ssh_key_files_directly(username) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| fn spawn_ssh_agent_and_add_keys() -> Result<(), Error> { | ||
| debug!("SSH_AUTH_SOCK not set, spawning ssh-agent"); | ||
|
|
||
| let output = Command::new("ssh-agent").arg("-s").output().map_err(|e| { | ||
| Error::new( | ||
| ErrorCode::Auth, | ||
| ErrorClass::Net, | ||
| format!("Failed to spawn ssh-agent: {e}"), | ||
| ) | ||
| })?; | ||
|
|
||
| if !output.status.success() { | ||
| return Err(Error::new( | ||
| ErrorCode::Auth, | ||
| ErrorClass::Net, | ||
| format!("ssh-agent failed with status: {}", output.status), | ||
| )); | ||
| } | ||
|
|
||
| let agent_output = String::from_utf8_lossy(&output.stdout); | ||
| debug!("ssh-agent output: {agent_output}"); | ||
|
|
||
| let env_vars = parse_ssh_agent_output(&agent_output); | ||
|
|
||
| for (key, value) in &env_vars { | ||
| unsafe { | ||
| std::env::set_var(key, value); | ||
| } | ||
| debug!("Set environment variable: {key}={value}"); | ||
| } | ||
|
|
||
| if !env_vars.contains_key("SSH_AUTH_SOCK") { | ||
| return Err(Error::new( | ||
| ErrorCode::Auth, | ||
| ErrorClass::Net, | ||
| "Failed to parse SSH_AUTH_SOCK from ssh-agent output", | ||
| )); | ||
| } | ||
|
|
||
| add_all_ssh_keys()?; | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
| fn add_all_ssh_keys() -> Result<(), Error> { | ||
| debug!("Adding all SSH keys from .ssh folder to ssh-agent"); | ||
|
|
||
| let home_dir = std::env::var("HOME") | ||
| .or_else(|_| std::env::var("USERPROFILE")) | ||
| .unwrap_or_else(|_| ".".to_string()); | ||
|
|
||
| let ssh_dir = Path::new(&home_dir).join(".ssh"); | ||
|
|
||
| if !ssh_dir.exists() { | ||
| debug!("SSH directory {ssh_dir:?} does not exist"); | ||
| return Ok(()); | ||
| } | ||
|
|
||
| let key_files = ["id_ed25519", "id_rsa", "id_ecdsa", "id_dsa"]; | ||
| let mut added_count = 0; | ||
|
|
||
| for key_name in &key_files { | ||
| let key_path = ssh_dir.join(key_name); | ||
|
|
||
| if key_path.exists() { | ||
| debug!("Found SSH key: {key_path:?}"); | ||
|
|
||
| // First try a quick non-interactive add (for keys without passphrase) | ||
| let quick_result = Command::new("ssh-add") | ||
| .arg(&key_path) | ||
| .env( | ||
| "SSH_AUTH_SOCK", | ||
| std::env::var("SSH_AUTH_SOCK").unwrap_or_default(), | ||
| ) | ||
| .stdin(Stdio::null()) // No input for quick try | ||
| .stdout(Stdio::null()) // Suppress output for quick try | ||
| .stderr(Stdio::piped()) // Capture errors to check if passphrase is needed | ||
| .output(); | ||
|
|
||
| match quick_result { | ||
| Ok(output) if output.status.success() => { | ||
| debug!("Successfully added key without interaction: {key_name}"); | ||
| added_count += 1; | ||
| } | ||
| Ok(output) => { | ||
| let stderr = String::from_utf8_lossy(&output.stderr); | ||
| debug!("Quick add failed for {key_name}: {stderr}"); | ||
|
|
||
| debug!("Key {key_name} appears to need passphrase, trying interactive add"); | ||
|
|
||
| match add_key_interactive(&key_path, key_name) { | ||
| Ok(true) => { | ||
| debug!("Successfully added key interactively: {key_name}"); | ||
| added_count += 1; | ||
| } | ||
| Ok(false) => { | ||
| debug!("User skipped key: {key_name}"); | ||
| } | ||
| Err(e) => { | ||
| debug!("Interactive add failed for {key_name}: {e}"); | ||
| } | ||
| } | ||
| } | ||
| Err(e) => { | ||
| debug!("Error running ssh-add for {key_name}: {e}"); | ||
| } | ||
| } | ||
| } else { | ||
| debug!("SSH key not found: {key_path:?}"); | ||
| } | ||
| } | ||
|
|
||
| debug!("Added {added_count} SSH keys to ssh-agent"); | ||
|
|
||
| if added_count == 0 { | ||
| debug!("No SSH keys were added"); | ||
| println!("No SSH keys were added to ssh-agent."); | ||
| println!("You may need to generate SSH keys or check your ~/.ssh directory."); | ||
| } else { | ||
| println!("Successfully added {added_count} SSH key(s) to ssh-agent."); | ||
| } | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
| fn try_ssh_key_files_directly(username: &str) -> Result<Cred, Error> { | ||
| debug!("Trying SSH key files directly for user: {username}"); | ||
|
|
||
| let home_dir = std::env::var("HOME") | ||
| .or_else(|_| std::env::var("USERPROFILE")) | ||
| .unwrap_or_else(|_| ".".to_string()); | ||
|
|
||
| let ssh_dir = Path::new(&home_dir).join(".ssh"); | ||
| let key_files = ["id_ed25519", "id_rsa", "id_ecdsa", "id_dsa"]; | ||
|
|
||
| for key_name in &key_files { | ||
| let private_key_path = ssh_dir.join(key_name); | ||
| let public_key_path = ssh_dir.join(format!("{key_name}.pub")); | ||
|
|
||
| if private_key_path.exists() && public_key_path.exists() { | ||
| debug!("Trying SSH key pair: {key_name} / {key_name}.pub"); | ||
|
|
||
| match Cred::ssh_key( | ||
| username, | ||
| Some(&public_key_path), | ||
| &private_key_path, | ||
| None, // No passphrase for now | ||
| ) { | ||
| Ok(cred) => { | ||
| debug!("SSH key authentication succeeded with {key_name}"); | ||
| return Ok(cred); | ||
| } | ||
| Err(e) => { | ||
| debug!("SSH key authentication failed with {key_name}: {e}"); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Err(Error::new( | ||
| ErrorCode::Auth, | ||
| ErrorClass::Net, | ||
| "No valid SSH key pairs found or all failed authentication", | ||
| )) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| pub mod git_auth; | ||
| mod git_http; | ||
| mod git_ssh; | ||
| mod ssh_utils; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add TODO here for cleanup later