From b1aaafcd756ce7c23a45680c534be1de8b69dd75 Mon Sep 17 00:00:00 2001 From: Christian Bruckmayer <3799140+ChrisBr@users.noreply.github.com> Date: Mon, 18 Nov 2024 08:04:34 +0000 Subject: [PATCH] Use git credential helper (#1871) * Use git credential helper * Fix compilation Signed-off-by: Jiahao XU * Add back `gh auth token` in `gh_token::get` Signed-off-by: Jiahao XU * Fix confusing `.expect` msg Signed-off-by: Jiahao XU --------- Signed-off-by: Jiahao XU Co-authored-by: Jiahao XU --- crates/bin/src/gh_token.rs | 107 +++++++++++++++++++++++++++++-------- 1 file changed, 85 insertions(+), 22 deletions(-) diff --git a/crates/bin/src/gh_token.rs b/crates/bin/src/gh_token.rs index 837f9f95..9cc7adeb 100644 --- a/crates/bin/src/gh_token.rs +++ b/crates/bin/src/gh_token.rs @@ -4,33 +4,96 @@ use std::{ str, }; -use tokio::process::Command; -use zeroize::Zeroizing; +use tokio::{io::AsyncWriteExt, process::Command}; +use zeroize::{Zeroize, Zeroizing}; pub(super) async fn get() -> io::Result>> { - let Output { status, stdout, .. } = Command::new("gh") + let output = Command::new("gh") .args(["auth", "token"]) - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::null()) - .output() + .stdout_with_optional_input(None) .await?; - let stdout = Zeroizing::new(stdout); - - if !status.success() { - return Err(io::Error::new( - io::ErrorKind::Other, - format!("process exited with `{status}`"), - )); + if !output.is_empty() { + return Ok(output); } - let s = str::from_utf8(&stdout).map_err(|err| { - io::Error::new( - io::ErrorKind::InvalidData, - format!("Invalid output, expected utf8: {err}"), - ) - })?; - - Ok(Zeroizing::new(s.trim().into())) + Command::new("git") + .args(["credential", "fill"]) + .stdout_with_optional_input(Some("host=github.com\nprotocol=https".as_bytes())) + .await? + .lines() + .find_map(|line| { + line.trim() + .strip_prefix("password=") + .map(|token| Zeroizing::new(token.into())) + }) + .ok_or_else(|| { + io::Error::new( + io::ErrorKind::Other, + "Password not found in `git credential fill` output", + ) + }) +} + +trait CommandExt { + // Helper function to execute a command, optionally with input + async fn stdout_with_optional_input( + &mut self, + input: Option<&[u8]>, + ) -> io::Result>>; +} + +impl CommandExt for Command { + async fn stdout_with_optional_input( + &mut self, + input: Option<&[u8]>, + ) -> io::Result>> { + self.stdout(Stdio::piped()) + .stderr(Stdio::null()) + .stdin(if input.is_some() { + Stdio::piped() + } else { + Stdio::null() + }); + + let mut child = self.spawn()?; + + if let Some(input) = input { + child.stdin.take().unwrap().write_all(input).await?; + } + + let Output { status, stdout, .. } = child.wait_with_output().await?; + + if status.success() { + let s = String::from_utf8(stdout).map_err(|err| { + let msg = format!( + "Invalid output for `{:?}`, expected utf8: {err}", + self.as_std() + ); + + zeroize_and_drop(err.into_bytes()); + + io::Error::new(io::ErrorKind::InvalidData, msg) + })?; + + let trimmed = s.trim(); + + Ok(if trimmed.len() == s.len() { + Zeroizing::new(s.into_boxed_str()) + } else { + Zeroizing::new(trimmed.into()) + }) + } else { + zeroize_and_drop(stdout); + + Err(io::Error::new( + io::ErrorKind::Other, + format!("`{:?}` process exited with `{status}`", self.as_std()), + )) + } + } +} + +fn zeroize_and_drop(mut bytes: Vec) { + bytes.zeroize(); }