auth_git2/
lib.rs

1//! Easy authentication for [`git2`].
2//!
3//! Authentication with [`git2`] can be quite difficult to implement correctly.
4//! This crate aims to make it easy.
5//!
6//! # Features
7//!
8//! * Has a small dependency tree.
9//! * Can query the SSH agent for private key authentication.
10//! * Can get SSH keys from files.
11//! * Can prompt the user for passwords for encrypted SSH keys.
12//!     * Only supported for OpenSSH private keys.
13//! * Can query the git credential helper for usernames and passwords.
14//! * Can use pre-provided plain usernames and passwords.
15//! * Can prompt the user for credentials as a last resort.
16//! * Allows you to fully customize all user prompts.
17//!
18//! The default user prompts will:
19//! * Use the git `askpass` helper if it is configured.
20//! * Fall back to prompting the user on the terminal if there is no `askpass` program configured.
21//! * Skip the prompt if there is also no terminal available for the process.
22//!
23//! # Creating an authenticator and enabling authentication mechanisms
24//!
25//! You can create use [`GitAuthenticator::new()`] (or [`default()`][`GitAuthenticator::default()`]) to create a ready-to-use authenticator.
26//! Using one of these constructors will enable all supported authentication mechanisms.
27//! You can still add more private key files from non-default locations to try if desired.
28//!
29//! You can also use [`GitAuthenticator::new_empty()`] to create an authenticator without any authentication mechanism enabled.
30//! Then you can selectively enable authentication mechanisms and add custom private key files.
31//!
32//! # Using the authenticator
33//!
34//! For the most flexibility, you can get a [`git2::Credentials`] callback using the [`GitAuthenticator::credentials()`] function.
35//! You can use it with any git operation that requires authentication.
36//! Doing this gives you full control to set other options and callbacks for the git operation.
37//!
38//! If you don't need to set other options or callbacks, you can also use the convenience functions on [`GitAuthenticator`].
39//! They wrap git operations with the credentials callback set:
40//!
41//! * [`GitAuthenticator::clone_repo()`]
42//! * [`GitAuthenticator::fetch()`]
43//! * [`GitAuthenticator::download()`]
44//! * [`GitAuthenticator::push()`]
45//!
46//! # Customizing user prompts
47//!
48//! All user prompts can be fully customized by calling [`GitAuthenticator::set_prompter()`].
49//! This allows you to override the way that the user is prompted for credentials or passphrases.
50//!
51//! If you have a fancy user interface, you can use a custom prompter to integrate the prompts with your user interface.
52//!
53//! # Example: Clone a repository
54//!
55//! ```no_run
56//! # fn main() -> Result<(), git2::Error> {
57//! use auth_git2::GitAuthenticator;
58//! use std::path::Path;
59//!
60//! let url = "https://github.com/de-vri-es/auth-git2-rs";
61//! let into = Path::new("/tmp/dyfhxoaj/auth-git2-rs");
62//!
63//! let auth = GitAuthenticator::default();
64//! let mut repo = auth.clone_repo(url, into);
65//! # let _ = repo;
66//! # Ok(())
67//! # }
68//! ```
69//!
70//! # Example: Clone a repository with full control over fetch options
71//!
72//! ```no_run
73//! # fn main() -> Result<(), git2::Error> {
74//! use auth_git2::GitAuthenticator;
75//! use std::path::Path;
76//!
77//! let auth = GitAuthenticator::default();
78//! let git_config = git2::Config::open_default()?;
79//! let mut repo_builder = git2::build::RepoBuilder::new();
80//! let mut fetch_options = git2::FetchOptions::new();
81//! let mut remote_callbacks = git2::RemoteCallbacks::new();
82//!
83//! remote_callbacks.credentials(auth.credentials(&git_config));
84//! fetch_options.remote_callbacks(remote_callbacks);
85//! repo_builder.fetch_options(fetch_options);
86//!
87//! let url = "https://github.com/de-vri-es/auth-git2-rs";
88//! let into = Path::new("/tmp/dyfhxoaj/auth-git2-rs");
89//! let mut repo = repo_builder.clone(url, into);
90//! # let _ = repo;
91//! # Ok(())
92//! # }
93//! ```
94
95#![warn(missing_docs)]
96
97use std::collections::BTreeMap;
98use std::path::{PathBuf, Path};
99
100#[cfg(feature = "log")]
101mod log {
102	pub use ::log::warn;
103	pub use ::log::debug;
104	pub use ::log::trace;
105}
106
107#[cfg(feature = "log")]
108use crate::log::*;
109
110#[cfg(not(feature = "log"))]
111#[macro_use]
112mod log {
113	macro_rules! warn {
114		($($tokens:tt)*) => { { let _ = format_args!($($tokens)*); } };
115	}
116
117	macro_rules! debug {
118		($($tokens:tt)*) => { { let _ = format_args!($($tokens)*); } };
119	}
120
121	macro_rules! trace {
122		($($tokens:tt)*) => { { let _ = format_args!($($tokens)*); } };
123	}
124}
125
126mod base64_decode;
127mod default_prompt;
128mod prompter;
129mod ssh_key;
130
131pub use prompter::Prompter;
132
133/// Configurable authenticator to use with [`git2`].
134#[derive(Clone)]
135pub struct GitAuthenticator {
136	/// Map of domain names to plaintext credentials.
137	plaintext_credentials: BTreeMap<String, PlaintextCredentials>,
138
139	/// Try getting username/password from the git credential helper.
140	try_cred_helper: bool,
141
142	/// Number of times to ask the user for a username/password on the terminal.
143	try_password_prompt: u32,
144
145	/// Map of domain names to usernames to try for SSH connections if no username was specified.
146	usernames: BTreeMap<String, String>,
147
148	/// Try to use the SSH agent to get a working SSH key.
149	try_ssh_agent: bool,
150
151	/// SSH keys to use from file.
152	ssh_keys: Vec<PrivateKeyFile>,
153
154	/// Prompt for passwords for encrypted SSH keys.
155	prompt_ssh_key_password: bool,
156
157	/// Custom prompter to use.
158	prompter: Box<dyn prompter::ClonePrompter>,
159}
160
161impl std::fmt::Debug for GitAuthenticator {
162	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163		f.debug_struct("GitAuthenticator")
164			.field("plaintext_credentials", &self.plaintext_credentials)
165			.field("try_cred_helper", &self.try_cred_helper)
166			.field("try_password_prompt", &self.try_password_prompt)
167			.field("usernames", &self.usernames)
168			.field("try_ssh_agent", &self.try_ssh_agent)
169			.field("ssh_keys", &self.ssh_keys)
170			.field("prompt_ssh_key_password", &self.prompt_ssh_key_password)
171			.finish()
172	}
173}
174
175impl Default for GitAuthenticator {
176	/// Create a new authenticator with all supported options enabled.
177	///
178	/// This is the same as [`GitAuthenticator::new()`].
179	fn default() -> Self {
180		Self::new()
181	}
182}
183
184impl GitAuthenticator {
185	/// Create a new authenticator with all supported options enabled.
186	///
187	/// This is equivalent to:
188	/// ```
189	/// # use auth_git2::GitAuthenticator;
190	/// GitAuthenticator::new_empty()
191	///     .try_cred_helper(true)
192	///     .try_password_prompt(3)
193	///     .add_default_username()
194	///     .try_ssh_agent(true)
195	///     .add_default_ssh_keys()
196	///     .prompt_ssh_key_password(true)
197	/// # ;
198	/// ```
199	pub fn new() -> Self {
200		Self::new_empty()
201			.try_cred_helper(true)
202			.try_password_prompt(3)
203			.add_default_username()
204			.try_ssh_agent(true)
205			.add_default_ssh_keys()
206			.prompt_ssh_key_password(true)
207	}
208
209	/// Create a new authenticator with all authentication options disabled.
210	pub fn new_empty() -> Self {
211		Self {
212			try_ssh_agent: false,
213			try_cred_helper: false,
214			plaintext_credentials: BTreeMap::new(),
215			try_password_prompt: 0,
216			usernames: BTreeMap::new(),
217			ssh_keys: Vec::new(),
218			prompt_ssh_key_password: false,
219			prompter: prompter::wrap_prompter(default_prompt::DefaultPrompter),
220		}
221	}
222
223	/// Set the username + password to use for a specific domain.
224	///
225	/// Use the special value "*" for the domain name to add fallback credentials when there is no exact match for the domain.
226	pub fn add_plaintext_credentials(mut self, domain: impl Into<String>, username: impl Into<String>, password: impl Into<String>) -> Self {
227		let domain = domain.into();
228		let username = username.into();
229		let password = password.into();
230		self.plaintext_credentials.insert(domain, PlaintextCredentials {
231			username,
232			password,
233		});
234		self
235	}
236
237	/// Configure if the git credentials helper should be used.
238	///
239	/// See the git documentation of the `credential.helper` configuration options for more details.
240	pub fn try_cred_helper(mut self, enable: bool) -> Self {
241		self.try_cred_helper = enable;
242		self
243	}
244
245	/// Configure the number of times we should prompt the user for a username/password.
246	///
247	/// Setting this value to `0` disables password prompts.
248	///
249	/// By default, if an `askpass` helper is configured, it will be used for the prompts.
250	/// Otherwise, the user will be prompted directly on the terminal of the current process.
251	/// If there is also no terminal available, the prompt is skipped.
252	///
253	/// An `askpass` helper can be configured in the `GIT_ASKPASS` environment variable,
254	/// the `core.askPass` configuration value or the `SSH_ASKPASS` environment variable.
255	///
256	/// You can override the prompt behaviour by calling [`Self::set_prompter()`].
257	pub fn try_password_prompt(mut self, max_count: u32) -> Self {
258		self.try_password_prompt = max_count;
259		self
260	}
261
262	/// Use a custom [`Prompter`] to prompt the user for credentials and passphrases.
263	///
264	/// If you set a custom prompter,
265	/// the authenticator will no longer try to use the `askpass` helper or prompt the user on the terminal.
266	/// Instead, the provided prompter will be called.
267	///
268	/// Note that prompts must still be enabled with [`Self::try_password_prompt()`] and [`Self::prompt_ssh_key_password()`].
269	/// If prompts are disabled, your custom prompter will not be called.
270	///
271	/// You can use this function to integrate the prompts with your own user interface
272	/// or simply to tweak the way the user is prompted on the terminal.
273	///
274	/// A unique clone of the prompter will be used for each [`git2::Credentials`] callback returned by [`Self::credentials()`].
275	pub fn set_prompter<P: Prompter + Clone + Send + 'static>(mut self, prompter: P) -> Self {
276		self.prompter = prompter::wrap_prompter(prompter);
277		self
278	}
279
280	/// Add a username to try for authentication for a specific domain.
281	///
282	/// Some authentication mechanisms need a username, but not all valid git URLs specify one.
283	/// You can add one or more usernames to try in that situation.
284	///
285	/// You can use the special domain name "*" to set a fallback username for domains that do not have a specific username set.
286	pub fn add_username(mut self, domain: impl Into<String>, username: impl Into<String>) -> Self {
287		let domain = domain.into();
288		let username = username.into();
289		self.usernames.insert(domain, username);
290		self
291	}
292
293	/// Add the default username to try.
294	///
295	/// The default username if read from the `USER` or `USERNAME` environment variable.
296	pub fn add_default_username(self) -> Self {
297		if let Ok(username) = std::env::var("USER").or_else(|_| std::env::var("USERNAME")) {
298			self.add_username("*", username)
299		} else {
300			self
301		}
302	}
303
304	/// Configure if the SSH agent should be used for public key authentication.
305	pub fn try_ssh_agent(mut self, enable: bool) -> Self {
306		self.try_ssh_agent = enable;
307		self
308	}
309
310	/// Add a private key to use for public key authentication.
311	///
312	/// The key will be read from disk by `git2`, so it must still exist when the authentication is performed.
313	///
314	/// You can provide a password for decryption of the private key.
315	/// If no password is provided and the `Self::prompt_ssh_key_password()` is enabled,
316	/// the user will be prompted for the passphrase of encrypted keys.
317	/// Note that currently only the `OpenSSH` private key format is supported for detecting that a key is encrypted.
318	///
319	/// A matching `.pub` file will also be read if it exists.
320	/// For example, if you add the private key `"foo/my_ssh_id"`,
321	/// then `"foo/my_ssh_id.pub"` will be used too, if it exists.
322	pub fn add_ssh_key_from_file(mut self, private_key: impl Into<PathBuf>, password: impl Into<Option<String>>) -> Self {
323		let private_key = private_key.into();
324		let public_key = get_pub_key_path(&private_key);
325		let password = password.into();
326		self.ssh_keys.push(PrivateKeyFile {
327			private_key,
328			public_key,
329			password,
330		});
331		self
332	}
333
334	/// Add all default SSH keys for public key authentication.
335	///
336	/// This will add all of the following files, if they exist:
337	///
338	/// * `"$HOME/.ssh/id_rsa"`
339	/// * `"$HOME/.ssh/id_ecdsa"`
340	/// * `"$HOME/.ssh/id_ecdsa_sk"`
341	/// * `"$HOME/.ssh/id_ed25519"`
342	/// * `"$HOME/.ssh/id_ed25519_sk"`
343	/// * `"$HOME/.ssh/id_dsa"`
344	pub fn add_default_ssh_keys(mut self) -> Self {
345		let ssh_dir = match dirs::home_dir() {
346			Some(x) => x.join(".ssh"),
347			None => return self,
348		};
349
350		let candidates = [
351			"id_rsa",
352			"id_ecdsa",
353			"id_ecdsa_sk",
354			"id_ed25519",
355			"id_ed25519_sk",
356			"id_dsa",
357		];
358
359		for candidate in candidates {
360			let private_key = ssh_dir.join(candidate);
361			if !private_key.is_file() {
362				continue;
363			}
364			self = self.add_ssh_key_from_file(private_key, None);
365		}
366
367		self
368	}
369
370	/// Prompt for passwords for encrypted SSH keys if needed.
371	///
372	/// By default, if an `askpass` helper is configured, it will be used for the prompts.
373	/// Otherwise, the user will be prompted directly on the terminal of the current process.
374	/// If there is also no terminal available, the prompt is skipped.
375	///
376	/// An `askpass` helper can be configured in the `GIT_ASKPASS` environment variable,
377	/// the `core.askPass` configuration value or the `SSH_ASKPASS` environment variable.
378	///
379	/// You can override the prompt behaviour by calling [`Self::set_prompter()`].
380	pub fn prompt_ssh_key_password(mut self, enable: bool) -> Self {
381		self.prompt_ssh_key_password = enable;
382		self
383	}
384
385	/// Get the credentials callback to use for [`git2::Credentials`].
386	///
387	/// # Example: Fetch from a remote with authentication
388	/// ```no_run
389	/// # fn foo(repo: &mut git2::Repository) -> Result<(), git2::Error> {
390	/// use auth_git2::GitAuthenticator;
391	///
392	/// let auth = GitAuthenticator::default();
393	/// let git_config = repo.config()?;
394	/// let mut fetch_options = git2::FetchOptions::new();
395	/// let mut remote_callbacks = git2::RemoteCallbacks::new();
396	///
397	/// remote_callbacks.credentials(auth.credentials(&git_config));
398	/// fetch_options.remote_callbacks(remote_callbacks);
399	///
400	/// repo.find_remote("origin")?
401	///     .fetch(&["main"], Some(&mut fetch_options), None)?;
402	/// # Ok(())
403	/// # }
404	/// ```
405	pub fn credentials<'a>(
406		&'a self,
407		git_config: &'a git2::Config,
408	) -> impl 'a + FnMut(&str, Option<&str>, git2::CredentialType) -> Result<git2::Cred, git2::Error> {
409		make_credentials_callback(self, git_config)
410	}
411
412	/// Clone a repository using the git authenticator.
413	///
414	/// If you need more control over the clone options,
415	/// use [`Self::credentials()`] with a [`git2::build::RepoBuilder`].
416	pub fn clone_repo(&self, url: impl AsRef<str>, into: impl AsRef<Path>) -> Result<git2::Repository, git2::Error> {
417		let url = url.as_ref();
418		let into = into.as_ref();
419
420		let git_config = git2::Config::open_default()?;
421		let mut repo_builder = git2::build::RepoBuilder::new();
422		let mut fetch_options = git2::FetchOptions::new();
423		let mut remote_callbacks = git2::RemoteCallbacks::new();
424
425		remote_callbacks.credentials(self.credentials(&git_config));
426		fetch_options.remote_callbacks(remote_callbacks);
427		repo_builder.fetch_options(fetch_options);
428
429		repo_builder.clone(url, into)
430	}
431
432
433	/// Fetch from a remote using the git authenticator.
434	///
435	/// If you need more control over the fetch options,
436	/// use [`Self::credentials()`] with [`git2::Remote::fetch()`].
437	pub fn fetch(&self, repo: &git2::Repository, remote: &mut git2::Remote, refspecs: &[&str], reflog_msg: Option<&str>) -> Result<(), git2::Error> {
438		let git_config = repo.config()?;
439		let mut fetch_options = git2::FetchOptions::new();
440		let mut remote_callbacks = git2::RemoteCallbacks::new();
441
442		remote_callbacks.credentials(self.credentials(&git_config));
443		fetch_options.remote_callbacks(remote_callbacks);
444		remote.fetch(refspecs, Some(&mut fetch_options), reflog_msg)
445	}
446
447	/// Download and index the packfile from a remote using the git authenticator.
448	///
449	/// If you need more control over the download options,
450	/// use [`Self::credentials()`] with [`git2::Remote::download()`].
451	///
452	/// This function does not update the remote tracking branches.
453	/// Consider using [`Self::fetch()`] if that is what you want.
454	pub fn download(&self, repo: &git2::Repository, remote: &mut git2::Remote, refspecs: &[&str]) -> Result<(), git2::Error> {
455		let git_config = repo.config()?;
456		let mut fetch_options = git2::FetchOptions::new();
457		let mut remote_callbacks = git2::RemoteCallbacks::new();
458
459		remote_callbacks.credentials(self.credentials(&git_config));
460		fetch_options.remote_callbacks(remote_callbacks);
461		remote.download(refspecs, Some(&mut fetch_options))
462	}
463
464	/// Push to a remote using the git authenticator.
465	///
466	/// If you need more control over the push options,
467	/// use [`Self::credentials()`] with [`git2::Remote::push()`].
468	pub fn push(&self, repo: &git2::Repository, remote: &mut git2::Remote, refspecs: &[&str]) -> Result<(), git2::Error> {
469		let git_config = repo.config()?;
470		let mut push_options = git2::PushOptions::new();
471		let mut remote_callbacks = git2::RemoteCallbacks::new();
472
473		remote_callbacks.credentials(self.credentials(&git_config));
474		push_options.remote_callbacks(remote_callbacks);
475
476		remote.push(refspecs, Some(&mut push_options))
477	}
478
479	/// Get the configured username for a URL.
480	fn get_username(&self, url: &str) -> Option<&str> {
481		if let Some(domain) = domain_from_url(url) {
482			if let Some(username) = self.usernames.get(domain) {
483				return Some(username);
484			}
485		}
486		self.usernames.get("*").map(|x| x.as_str())
487	}
488
489	/// Get the configured plaintext credentials for a URL.
490	fn get_plaintext_credentials(&self, url: &str) -> Option<&PlaintextCredentials> {
491		if let Some(domain) = domain_from_url(url) {
492			if let Some(credentials) = self.plaintext_credentials.get(domain) {
493				return Some(credentials);
494			}
495		}
496		self.plaintext_credentials.get("*")
497	}
498}
499
500fn make_credentials_callback<'a>(
501	authenticator: &'a GitAuthenticator,
502	git_config: &'a git2::Config,
503) -> impl 'a + FnMut(&str, Option<&str>, git2::CredentialType) -> Result<git2::Cred, git2::Error> {
504	let mut try_cred_helper = authenticator.try_cred_helper;
505	let mut try_password_prompt = authenticator.try_password_prompt;
506	let mut try_ssh_agent = authenticator.try_ssh_agent;
507	let mut ssh_keys = authenticator.ssh_keys.iter();
508	let mut prompter = authenticator.prompter.clone();
509
510	move |url: &str, username: Option<&str>, allowed: git2::CredentialType| {
511		trace!("credentials callback called with url: {url:?}, username: {username:?}, allowed_credentials: {allowed:?}");
512
513		// If git2 is asking for a username, we got an SSH url without username specified.
514		// After we supply a username, it will ask for the real credentials.
515		//
516		// Sadly, we can not switch usernames during an authentication session,
517		// so to try different usernames, we need to retry the git operation multiple times.
518		// If this happens, we'll bail and go into stage 2.
519		if allowed.contains(git2::CredentialType::USERNAME) {
520			if let Some(username) = authenticator.get_username(url) {
521				debug!("credentials_callback: returning username: {username:?}");
522				match git2::Cred::username(username) {
523					Ok(x) => return Ok(x),
524					Err(e) => {
525						debug!("credentials_callback: failed to wrap username: {e}");
526						return Err(e);
527					},
528				}
529			}
530		}
531
532		// Try public key authentication.
533		if allowed.contains(git2::CredentialType::SSH_KEY) {
534			if let Some(username) = username {
535				if try_ssh_agent {
536					try_ssh_agent = false;
537					debug!("credentials_callback: trying ssh_key_from_agent with username: {username:?}");
538					match git2::Cred::ssh_key_from_agent(username) {
539						Ok(x) => return Ok(x),
540						Err(e) => debug!("credentials_callback: failed to use SSH agent: {e}"),
541					}
542				}
543
544				#[allow(clippy::while_let_on_iterator)] // Incorrect lint: we're not consuming the iterator.
545				while let Some(key) = ssh_keys.next() {
546					debug!("credentials_callback: trying ssh key, username: {username:?}, private key: {:?}", key.private_key);
547					let prompter = Some(prompter.as_prompter_mut())
548						.filter(|_| authenticator.prompt_ssh_key_password);
549					match key.to_credentials(username, prompter, git_config) {
550						Ok(x) => return Ok(x),
551						Err(e) => debug!("credentials_callback: failed to use SSH key from file {:?}: {e}", key.private_key),
552					}
553				}
554			}
555		}
556
557		// Sometimes libgit2 will ask for a username/password in plaintext.
558		if allowed.contains(git2::CredentialType::USER_PASS_PLAINTEXT) {
559			// Try provided plaintext credentials first.
560			if let Some(credentials) = authenticator.get_plaintext_credentials(url) {
561				debug!("credentials_callback: trying plain text credentials with username: {:?}", credentials.username);
562				match credentials.to_credentials() {
563					Ok(x) => return Ok(x),
564					Err(e) => {
565						debug!("credentials_callback: failed to wrap plain text credentials: {e}");
566						return Err(e);
567					},
568				}
569			}
570
571			// Try the git credential helper.
572			if try_cred_helper {
573				try_cred_helper = false;
574				debug!("credentials_callback: trying credential_helper");
575				match git2::Cred::credential_helper(git_config, url, username) {
576					Ok(x) => return Ok(x),
577					Err(e) => debug!("credentials_callback: failed to use credential helper: {e}"),
578				}
579			}
580
581			// Prompt the user on the terminal.
582			if try_password_prompt > 0 {
583				try_password_prompt -= 1;
584				let credentials = PlaintextCredentials::prompt(
585					prompter.as_prompter_mut(),
586					username,
587					url,
588					git_config
589				);
590				if let Some(credentials) = credentials {
591					return credentials.to_credentials();
592				}
593			}
594		}
595
596		Err(git2::Error::from_str("all authentication attempts failed"))
597	}
598}
599
600#[derive(Debug, Clone)]
601struct PrivateKeyFile {
602	private_key: PathBuf,
603	public_key: Option<PathBuf>,
604	password: Option<String>,
605}
606
607impl PrivateKeyFile {
608	fn to_credentials(&self, username: &str, prompter: Option<&mut dyn Prompter>, git_config: &git2::Config) -> Result<git2::Cred, git2::Error> {
609		if let Some(password) = &self.password {
610			git2::Cred::ssh_key(username, self.public_key.as_deref(), &self.private_key, Some(password))
611		} else if let Some(prompter) = prompter {
612			let password = match ssh_key::analyze_ssh_key_file(&self.private_key) {
613				Err(e) => {
614					warn!("Failed to analyze SSH key: {}: {}", self.private_key.display(), e);
615					None
616				},
617				Ok(key_info) => {
618					if key_info.format == ssh_key::KeyFormat::Unknown {
619						warn!("Unknown key format for key: {}", self.private_key.display());
620					}
621					if key_info.encrypted {
622						prompter.prompt_ssh_key_passphrase(&self.private_key, git_config)
623					} else {
624						None
625					}
626				},
627			};
628			git2::Cred::ssh_key(username, self.public_key.as_deref(), &self.private_key, password.as_deref())
629		} else {
630			git2::Cred::ssh_key(username, self.public_key.as_deref(), &self.private_key, None)
631		}
632	}
633}
634
635#[derive(Debug, Clone)]
636struct PlaintextCredentials {
637	username: String,
638	password: String,
639}
640
641impl PlaintextCredentials {
642	fn prompt(prompter: &mut dyn Prompter, username: Option<&str>, url: &str, git_config: &git2::Config) -> Option<Self> {
643		if let Some(username) = username {
644			let password = prompter.prompt_password(username, url, git_config)?;
645			Some(Self {
646				username: username.into(),
647				password,
648			})
649		} else {
650			let (username, password) = prompter.prompt_username_password(url, git_config)?;
651			Some(Self {
652				username,
653				password,
654			})
655		}
656	}
657
658	fn to_credentials(&self) -> Result<git2::Cred, git2::Error> {
659		git2::Cred::userpass_plaintext(&self.username, &self.password)
660	}
661}
662
663fn get_pub_key_path(priv_key_path: &Path) -> Option<PathBuf> {
664	let name = priv_key_path.file_name()?;
665	let name = name.to_str()?;
666	let pub_key_path = priv_key_path.with_file_name(format!("{name}.pub"));
667	if pub_key_path.is_file() {
668		Some(pub_key_path)
669	} else {
670		None
671	}
672}
673
674fn domain_from_url(url: &str) -> Option<&str> {
675	// We support:
676	// Relative paths
677	// Real URLs: scheme://[user[:pass]@]host/path
678	// SSH URLs: [user@]host:path.
679
680	// If there is no colon: URL is a relative path and there is no domain (or need for credentials).
681	let (head, tail) = url.split_once(':')?;
682
683	// Real URL
684	if let Some(tail) = tail.strip_prefix("//") {
685		let (_credentials, tail) = tail.split_once('@').unwrap_or(("", tail));
686		let (host, _path) = tail.split_once('/').unwrap_or((tail, ""));
687		Some(host)
688	// SSH "URL"
689	} else {
690		let (_credentials, host) = head.split_once('@').unwrap_or(("", head));
691		Some(host)
692	}
693}
694
695#[cfg(test)]
696mod test {
697	use super::*;
698	use assert2::assert;
699
700	#[test]
701	fn test_domain_from_url() {
702		assert!(let Some("host") = domain_from_url("user@host:path"));
703		assert!(let Some("host") = domain_from_url("host:path"));
704		assert!(let Some("host") = domain_from_url("host:path@with:stuff"));
705
706		assert!(let Some("host") = domain_from_url("ssh://user:pass@host/path"));
707		assert!(let Some("host") = domain_from_url("ssh://user@host/path"));
708		assert!(let Some("host") = domain_from_url("ssh://host/path"));
709
710		assert!(let None = domain_from_url("some/relative/path"));
711		assert!(let None = domain_from_url("some/relative/path@with-at-sign"));
712	}
713
714	#[test]
715	fn test_that_authenticator_is_send() {
716		let authenticator = GitAuthenticator::new();
717		let thread = std::thread::spawn(move || {
718			drop(authenticator);
719		});
720		thread.join().unwrap();
721	}
722}