Skip to content
33 changes: 33 additions & 0 deletions src/auth/git_auth.rs
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)
});
Comment on lines +26 to +30
Copy link
Owner

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


callbacks
}
55 changes: 55 additions & 0 deletions src/auth/git_http.rs
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",
))
}
}
250 changes: 250 additions & 0 deletions src/auth/git_ssh.rs
Copy link
Owner

Choose a reason for hiding this comment

The 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 {
Copy link
Owner

Choose a reason for hiding this comment

The 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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

code smell, remove it!

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can use ssh-add -l for checking if keys already exists

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

confirm if there is any identity?
if none, then add keys, if few then try using that if that fails then retry adding new keys
no need to spawn a ssh agent for this

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
Copy link
Owner

Choose a reason for hiding this comment

The 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",
))
}
4 changes: 4 additions & 0 deletions src/auth/mod.rs
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;
Loading
Loading