diff --git a/Cargo.lock b/Cargo.lock index 00f732624..3b2c381e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1194,7 +1194,7 @@ checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" dependencies = [ "libc", "redox_users", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1217,7 +1217,7 @@ checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" dependencies = [ "libc", "redox_users", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1506,7 +1506,7 @@ dependencies = [ "cc", "lazy_static", "libc", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1574,7 +1574,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04412b8935272e3a9bae6f48c7bfff74c2911f60525404edfdd28e49884c3bfb" dependencies = [ "libc", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1887,7 +1887,7 @@ dependencies = [ "gobject-sys", "libc", "system-deps 6.1.1", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2008,6 +2008,8 @@ dependencies = [ "sysinfo", "thiserror", "tokio", + "winapi 0.3.9", + "windows-named-pipe", ] [[package]] @@ -2284,7 +2286,7 @@ checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" dependencies = [ "libc", "match_cfg", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2679,6 +2681,16 @@ dependencies = [ "treediff", ] +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + [[package]] name = "kqueue" version = "1.0.8" @@ -3135,7 +3147,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -3145,7 +3157,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" dependencies = [ "overload", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -3389,7 +3401,7 @@ checksum = "006e42d5b888366f1880eda20371fedde764ed2213dc8496f49622fa0c99cd5e" dependencies = [ "log", "serde", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -3499,7 +3511,7 @@ dependencies = [ "libc", "redox_syscall 0.2.16", "smallvec", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -3986,7 +3998,7 @@ dependencies = [ "raw-cpuid", "wasi 0.11.0+wasi-snapshot-preview1", "web-sys", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -5010,7 +5022,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" dependencies = [ "libc", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -5538,7 +5550,7 @@ dependencies = [ "serde", "tauri", "time", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -6107,7 +6119,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce65604324d3cce9b966701489fbd0cf318cb1f7bd9dd07ac9a4ee6fb791930d" dependencies = [ "tempfile", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -6479,6 +6491,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + [[package]] name = "winapi" version = "0.3.9" @@ -6489,6 +6507,12 @@ dependencies = [ "winapi-x86_64-pc-windows-gnu", ] +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" @@ -6501,7 +6525,7 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -6591,6 +6615,16 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ee5e275231f07c6e240d14f34e1b635bf1faa1c76c57cfd59a5cdb9848e4278" +[[package]] +name = "windows-named-pipe" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808ba65b3d86cc5465971ad08ee3850e197cc8d5719277611fabb27827c02388" +dependencies = [ + "kernel32-sys", + "winapi 0.2.8", +] + [[package]] name = "windows-sys" version = "0.42.0" @@ -7001,7 +7035,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2769203cd13a0c6015d515be729c526d041e9cf2c0cc478d57faee85f40c6dcd" dependencies = [ "nix 0.26.4", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -7044,7 +7078,7 @@ dependencies = [ "static_assertions", "tracing", "uds_windows", - "winapi", + "winapi 0.3.9", "xdg-home", "zbus_macros", "zbus_names", diff --git a/gitbutler-app/src/virtual_branches/virtual.rs b/gitbutler-app/src/virtual_branches/virtual.rs index d75ea74a3..243051e3b 100644 --- a/gitbutler-app/src/virtual_branches/virtual.rs +++ b/gitbutler-app/src/virtual_branches/virtual.rs @@ -2665,7 +2665,7 @@ pub fn push( with_force, credentials, None, - askpass, + askpass.clone(), )?; vbranch.upstream = Some(remote_branch.clone()); @@ -2673,6 +2673,11 @@ pub fn push( branch_writer .write(&mut vbranch) .context("failed to write target branch after push")?; + project_repository.fetch( + remote_branch.remote(), + credentials, + askpass.map(|(broker, _)| (broker, "modal".to_string())), + )?; Ok(()) } diff --git a/gitbutler-git/Cargo.toml b/gitbutler-git/Cargo.toml index 76186b9e7..38c64ef59 100644 --- a/gitbutler-git/Cargo.toml +++ b/gitbutler-git/Cargo.toml @@ -8,11 +8,11 @@ path = "src/lib.rs" [[bin]] name = "gitbutler-git-askpass" -path = "src/cli/bin/askpass.rs" +path = "src/bin/askpass.rs" [[bin]] name = "gitbutler-git-setsid" -path = "src/cli/bin/setsid.rs" +path = "src/bin/setsid.rs" [features] default = ["serde", "tokio"] @@ -29,3 +29,8 @@ sysinfo = "0.30.5" [target."cfg(unix)".dependencies] nix = { version = "0.27.1", features = ["process", "socket", "user"] } + +[target."cfg(windows)".dependencies] +winapi = { version = "0.3.9", features = ["winbase", "namedpipeapi"] } +# synchronous named pipes for the askpass utility +windows-named-pipe = "0.1.0" diff --git a/gitbutler-git/src/backend.rs b/gitbutler-git/src/backend.rs deleted file mode 100644 index 9b6db14ed..000000000 --- a/gitbutler-git/src/backend.rs +++ /dev/null @@ -1,2 +0,0 @@ -#[cfg(feature = "cli")] -pub mod cli; diff --git a/gitbutler-git/src/bin/askpass.rs b/gitbutler-git/src/bin/askpass.rs new file mode 100644 index 000000000..cf17d1989 --- /dev/null +++ b/gitbutler-git/src/bin/askpass.rs @@ -0,0 +1,49 @@ +#[cfg(unix)] +#[path = "askpass/unix.rs"] +mod unix; +#[cfg(windows)] +#[path = "askpass/windows.rs"] +mod windows; + +#[cfg(windows)] +use self::windows::UnixCompatibility; + +use std::io::{BufRead, BufReader, BufWriter, Write}; + +pub fn main() { + let pipe_name = std::env::var("GITBUTLER_ASKPASS_PIPE").expect("do not run this binary yourself; it's only meant to be run by GitButler (missing GITBUTLER_ASKPASS_PIPE env var)"); + let pipe_secret = std::env::var("GITBUTLER_ASKPASS_SECRET").expect("do not run this binary yourself; it's only meant to be run by GitButler (missing GITBUTLER_ASKPASS_SECRET env var)"); + let prompt = std::env::args().nth(1).expect("do not run this binary yourself; it's only meant to be run by GitButler (missing prompt arg)"); + + #[cfg(unix)] + let raw_stream = self::unix::establish(&pipe_name); + #[cfg(windows)] + let raw_stream = self::windows::establish(&pipe_name); + + let mut reader = BufReader::new(raw_stream.try_clone().unwrap()); + let mut writer = BufWriter::new(raw_stream.try_clone().unwrap()); + + // Write the secret. + writeln!(writer, "{pipe_secret}").expect("write(secret):"); + + // Write the prompt that Git gave us. + writeln!(writer, "{prompt}").expect("write(prompt):"); + + writer.flush().expect("flush():"); + + // Clear the timeout (it's now time for the user to provide a response) + raw_stream + .set_read_timeout(None) + .expect("set_read_timeout(None):"); + + // Wait for the response. + let mut password = String::new(); + let nread = reader.read_line(&mut password).expect("read_line():"); + if nread == 0 { + panic!("read_line() returned 0"); + } + + // Write the response back to Git. + // `password` already has a newline at the end. + write!(std::io::stdout(), "{password}").expect("write(password):"); +} diff --git a/gitbutler-git/src/bin/askpass/unix.rs b/gitbutler-git/src/bin/askpass/unix.rs new file mode 100644 index 000000000..cae096831 --- /dev/null +++ b/gitbutler-git/src/bin/askpass/unix.rs @@ -0,0 +1,5 @@ +use std::os::unix::net::UnixStream; + +pub fn establish(sock_path: &str) -> UnixStream { + UnixStream::connect(sock_path).expect("connect():") +} diff --git a/gitbutler-git/src/bin/askpass/windows.rs b/gitbutler-git/src/bin/askpass/windows.rs new file mode 100644 index 000000000..40313209c --- /dev/null +++ b/gitbutler-git/src/bin/askpass/windows.rs @@ -0,0 +1,59 @@ +use std::{ + io, + os::windows::io::{AsRawHandle, FromRawHandle}, + time::Duration, +}; +use windows_named_pipe::PipeStream; + +pub fn establish(sock_path: &str) -> PipeStream { + PipeStream::connect(sock_path).unwrap() +} + +/// There are some methods we need in order to run askpass correctly, +/// and those methods are not available out of the box on windows. +/// We stub them using this trait so we don't have to newtype +/// the pipestream itself (which would be extensive and un-DRY). +pub trait UnixCompatibility: Sized { + fn try_clone(&self) -> Option; + fn set_read_timeout(&self, timeout: Option) -> io::Result<()>; +} + +impl UnixCompatibility for PipeStream { + fn try_clone(&self) -> Option { + Some(unsafe { Self::from_raw_handle(self.as_raw_handle()) }) + } + + fn set_read_timeout(&self, timeout: Option) -> io::Result<()> { + // NOTE(qix-): Technically, this shouldn't work (and probably doesn't). + // NOTE(qix-): The documentation states: + // NOTE(qix-): + // NOTE(qix-): > This parameter must be NULL if . . . client and server + // NOTE(qix-): > processes are on the same computer. + // NOTE(qix-): + // NOTE(qix-): This is indeed the case here, but we try to make it work + // NOTE(qix-): anyway. + #[allow(unused_assignments)] + let mut timeout_ms: winapi::shared::minwindef::DWORD = 0; + let timeout_ptr: winapi::shared::minwindef::LPDWORD = if let Some(timeout) = timeout { + timeout_ms = timeout.as_millis() as winapi::shared::minwindef::DWORD; + &mut timeout_ms as *mut _ + } else { + std::ptr::null_mut() + }; + + let r = unsafe { + winapi::um::namedpipeapi::SetNamedPipeHandleState( + self.as_raw_handle(), + std::ptr::null_mut(), + std::ptr::null_mut(), + timeout_ptr, + ) + }; + + if r == 0 { + Err(io::Error::last_os_error()) + } else { + Ok(()) + } + } +} diff --git a/gitbutler-git/src/bin/setsid.rs b/gitbutler-git/src/bin/setsid.rs new file mode 100644 index 000000000..04ef18514 --- /dev/null +++ b/gitbutler-git/src/bin/setsid.rs @@ -0,0 +1,10 @@ +// NOTE(qix-): Cargo doesn't let us specify binaries based on the platform, +// NOTE(qix-): unfortunately. This utility is not used on Windows but is +// NOTE(qix-): build anyway. We'll address this at a later time. +// NOTE(qix-): +// NOTE(qix-): For now, we just stub out the main function on windows and panic. + +#[cfg(unix)] +include!("setsid/unix.rs"); +#[cfg(windows)] +include!("setsid/windows.rs"); diff --git a/gitbutler-git/src/cli/bin/setsid.rs b/gitbutler-git/src/bin/setsid/unix.rs similarity index 84% rename from gitbutler-git/src/cli/bin/setsid.rs rename to gitbutler-git/src/bin/setsid/unix.rs index 4359de923..8d6d02c12 100644 --- a/gitbutler-git/src/cli/bin/setsid.rs +++ b/gitbutler-git/src/bin/setsid/unix.rs @@ -1,16 +1,9 @@ -#[cfg(not(target_os = "windows"))] use nix::{ libc::{c_int, wait, EXIT_FAILURE, WEXITSTATUS, WIFEXITED, WIFSIGNALED, WTERMSIG}, unistd::{fork, setsid, ForkResult}, }; use std::{os::unix::process::CommandExt, process}; -#[cfg(target_os = "windows")] -pub fn main() { - panic!("This binary is only meant to be run on Unix-like systems. It exists on Windows only because Cargo cannot switch off bins based on target platform."); -} - -#[cfg(not(target_os = "windows"))] pub fn main() { let has_pipe_var = std::env::var("GITBUTLER_ASKPASS_PIPE") .map(|v| !v.is_empty()) diff --git a/gitbutler-git/src/bin/setsid/windows.rs b/gitbutler-git/src/bin/setsid/windows.rs new file mode 100644 index 000000000..9a41b9b89 --- /dev/null +++ b/gitbutler-git/src/bin/setsid/windows.rs @@ -0,0 +1,3 @@ +pub fn main() { + panic!("This binary is only meant to be run on Unix-like systems. It exists on Windows only because Cargo cannot switch off bins based on target platform."); +} diff --git a/gitbutler-git/src/cli.rs b/gitbutler-git/src/cli.rs deleted file mode 100644 index 0aceb3ff8..000000000 --- a/gitbutler-git/src/cli.rs +++ /dev/null @@ -1,17 +0,0 @@ -//! CLI-based (fork/exec) backend implementation, -//! executing the `git` command-line tool available -//! on `$PATH`. - -mod executor; -mod repository; - -#[cfg(unix)] -pub use self::executor::Uid; - -pub use self::{ - executor::{AskpassServer, FileStat, GitExecutor, Pid, Socket}, - repository::{fetch, push}, -}; - -#[cfg(feature = "tokio")] -pub use self::executor::tokio; diff --git a/gitbutler-git/src/cli/bin/askpass-unix.rs b/gitbutler-git/src/cli/bin/askpass-unix.rs deleted file mode 100644 index 747029430..000000000 --- a/gitbutler-git/src/cli/bin/askpass-unix.rs +++ /dev/null @@ -1,38 +0,0 @@ -use std::io::{BufRead, BufReader, BufWriter, Write}; -use std::os::unix::net::UnixStream; - -pub fn main(sock_path: &str, secret: &str, prompt: &str) { - let raw_stream = UnixStream::connect(sock_path).expect("connect():"); - - // Set a timer for 10s. - raw_stream - .set_read_timeout(Some(std::time::Duration::from_secs(10))) - .expect("set_read_timeout(Some):"); - - let mut reader = BufReader::new(raw_stream.try_clone().unwrap()); - let mut writer = BufWriter::new(raw_stream.try_clone().unwrap()); - - // Write the secret. - writeln!(writer, "{secret}").expect("write(secret):"); - - // Write the prompt that Git gave us. - writeln!(writer, "{prompt}").expect("write(prompt):"); - - writer.flush().expect("flush():"); - - // Clear the timeout (it's now time for the user to provide a response) - raw_stream - .set_read_timeout(None) - .expect("set_read_timeout(None):"); - - // Wait for the response. - let mut password = String::new(); - let nread = reader.read_line(&mut password).expect("read_line():"); - if nread == 0 { - panic!("read_line() returned 0"); - } - - // Write the response back to Git. - // `password` already has a newline at the end. - write!(std::io::stdout(), "{password}").expect("write(password):"); -} diff --git a/gitbutler-git/src/cli/bin/askpass.rs b/gitbutler-git/src/cli/bin/askpass.rs deleted file mode 100644 index 86027d0cf..000000000 --- a/gitbutler-git/src/cli/bin/askpass.rs +++ /dev/null @@ -1,15 +0,0 @@ -#[cfg(not(target_os = "windows"))] -#[path = "askpass-unix.rs"] -mod unix; - -#[cfg(target_os = "windows")] -compile_error!("Windows support is not yet implemented."); - -pub fn main() { - let pipe_name = std::env::var("GITBUTLER_ASKPASS_PIPE").expect("do not run this binary yourself; it's only meant to be run by GitButler (missing GITBUTLER_ASKPASS_PIPE env var)"); - let pipe_secret = std::env::var("GITBUTLER_ASKPASS_SECRET").expect("do not run this binary yourself; it's only meant to be run by GitButler (missing GITBUTLER_ASKPASS_SECRET env var)"); - let prompt = std::env::args().nth(1).expect("do not run this binary yourself; it's only meant to be run by GitButler (missing prompt arg)"); - - #[cfg(not(target_os = "windows"))] - unix::main(&pipe_name, &pipe_secret, &prompt); -} diff --git a/gitbutler-git/src/cli/executor/tokio.rs b/gitbutler-git/src/cli/executor/tokio.rs deleted file mode 100644 index 572f00b57..000000000 --- a/gitbutler-git/src/cli/executor/tokio.rs +++ /dev/null @@ -1,169 +0,0 @@ -//! A [Tokio](https://tokio.rs)-based [`super::GitExecutor`] implementation. - -#[cfg(unix)] -use std::os::unix::fs::{MetadataExt, PermissionsExt}; -use std::{collections::HashMap, fs::Permissions, path::Path, time::Duration}; -use tokio::process::Command; - -/// A [`super::GitExecutor`] implementation using the `git` command-line tool -/// via [`tokio::process::Command`]. -pub struct TokioExecutor; - -#[allow(unsafe_code)] -unsafe impl super::GitExecutor for TokioExecutor { - type Error = std::io::Error; - type ServerHandle = TokioAskpassServer; - - async fn execute_raw>( - &self, - args: &[&str], - cwd: P, - envs: Option>, - ) -> Result<(usize, String, String), Self::Error> { - let mut cmd = Command::new("git"); - - // Output the command being executed to stderr, for debugging purposes - // (only on test configs). - #[cfg(any(test, debug_assertions))] - { - let mut envs_str = String::new(); - if let Some(envs) = &envs { - for (key, value) in envs.iter() { - envs_str.push_str(&format!("{key}={value:?} ")); - } - } - let args_str = args - .iter() - .map(|s| format!("{s:?}")) - .collect::>() - .join(" "); - eprintln!("env {envs_str} git {args_str}"); - } - - cmd.kill_on_drop(true); - cmd.args(args); - cmd.current_dir(cwd); - - if let Some(envs) = envs { - cmd.envs(envs); - } - - let output = cmd.output().await?; - - #[cfg(any(test, debug_assertions))] - { - eprintln!( - "\n\n GIT STDOUT:\n\n{}\n\nGIT STDERR:\n\n{}\n\nGIT EXIT CODE: {}\n", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr), - output.status.code().unwrap_or(127) as usize - ); - } - - Ok(( - output.status.code().unwrap_or(127) as usize, - String::from_utf8_lossy(&output.stdout).trim().into(), - String::from_utf8_lossy(&output.stderr).trim().into(), - )) - } - - #[cfg(unix)] - async unsafe fn create_askpass_server(&self) -> Result { - let connection_string = - std::env::temp_dir().join(format!("gitbutler-askpass-{}", rand::random::())); - - let listener = tokio::net::UnixListener::bind(&connection_string)?; - - tokio::fs::set_permissions(&connection_string, Permissions::from_mode(0o0600)).await?; - - Ok(TokioAskpassServer { - server: Some(listener), - connection_string: connection_string.to_string_lossy().into(), - }) - } - - #[cfg(unix)] - async fn stat(&self, path: &str) -> Result { - let metadata = tokio::fs::symlink_metadata(path).await?; - - Ok(super::FileStat { - dev: metadata.dev(), - ino: metadata.ino(), - is_regular_file: metadata.is_file(), - }) - } -} - -#[cfg(unix)] -impl super::Socket for tokio::io::BufStream { - type Error = std::io::Error; - - fn pid(&self) -> Result { - self.get_ref() - .peer_cred() - .unwrap() - .pid() - .ok_or(std::io::Error::new( - std::io::ErrorKind::Other, - "no pid available for peer connection", - )) - } - - fn uid(&self) -> Result { - Ok(self.get_ref().peer_cred().unwrap().uid()) - } - - async fn read_line(&mut self) -> Result { - let mut buf = String::new(); - ::read_line(self, &mut buf).await?; - Ok(buf.trim_end_matches(|c| c == '\r' || c == '\n').into()) - } - - async fn write_line(&mut self, line: &str) -> Result<(), Self::Error> { - ::write_all(self, line.as_bytes()).await?; - ::write_all(self, b"\n").await?; - ::flush(self).await?; - Ok(()) - } -} - -/// A tokio-based [`super::AskpassServer`] implementation. -#[cfg(unix)] -pub struct TokioAskpassServer { - // Always Some until dropped. - server: Option, - connection_string: String, -} - -#[cfg(unix)] -impl super::AskpassServer for TokioAskpassServer { - type Error = std::io::Error; - #[cfg(unix)] - type SocketHandle = tokio::io::BufStream; - - async fn accept(&self, timeout: Option) -> Result { - let res = if let Some(timeout) = timeout { - tokio::time::timeout(timeout, self.server.as_ref().unwrap().accept()).await? - } else { - self.server.as_ref().unwrap().accept().await - }; - - res.map(|(s, _)| tokio::io::BufStream::new(s)) - } -} - -#[cfg(unix)] -impl core::fmt::Display for TokioAskpassServer { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - self.connection_string.fmt(f) - } -} - -#[cfg(unix)] -impl Drop for TokioAskpassServer { - fn drop(&mut self) { - drop(self.server.take()); - // best-effort - std::fs::remove_file(&self.connection_string).ok(); - } -} diff --git a/gitbutler-git/src/cli/executor.rs b/gitbutler-git/src/executor.rs similarity index 99% rename from gitbutler-git/src/cli/executor.rs rename to gitbutler-git/src/executor.rs index 0e3f022ae..c3b9a5b78 100644 --- a/gitbutler-git/src/cli/executor.rs +++ b/gitbutler-git/src/executor.rs @@ -142,7 +142,7 @@ pub unsafe trait GitExecutor { /// during askpass authentication. /// /// **Do not follow symbolic links.** - async fn stat(&self, path: &str) -> Result; + async fn stat>(&self, path: P) -> Result; } /// Stats for a file on the filesystem. diff --git a/gitbutler-git/src/executor/tokio.rs b/gitbutler-git/src/executor/tokio.rs new file mode 100644 index 000000000..45793ddee --- /dev/null +++ b/gitbutler-git/src/executor/tokio.rs @@ -0,0 +1,108 @@ +//! A [Tokio](https://tokio.rs)-based Git executor implementation. + +#[cfg(unix)] +mod unix; +#[cfg(windows)] +mod windows; + +use std::{collections::HashMap, path::Path}; +use tokio::process::Command; + +#[cfg(unix)] +pub use self::unix::TokioAskpassServer; +#[cfg(windows)] +pub use self::windows::TokioAskpassServer; + +/// A Git executor implementation using the `git` command-line tool +/// via [`tokio::process::Command`]. +pub struct TokioExecutor; + +#[allow(unsafe_code)] +unsafe impl super::GitExecutor for TokioExecutor { + type Error = std::io::Error; + type ServerHandle = TokioAskpassServer; + + async fn execute_raw>( + &self, + args: &[&str], + cwd: P, + envs: Option>, + ) -> Result<(usize, String, String), Self::Error> { + let mut cmd = Command::new({ + #[cfg(unix)] + { + "git" + } + #[cfg(windows)] + { + "git.exe" + } + }); + + // Output the command being executed to stderr, for debugging purposes + // (only on test configs). + #[cfg(any(test, debug_assertions))] + { + let mut envs_str = String::new(); + if let Some(envs) = &envs { + for (key, value) in envs.iter() { + envs_str.push_str(&format!("{key}={value:?} ")); + } + } + let args_str = args + .iter() + .map(|s| format!("{s:?}")) + .collect::>() + .join(" "); + eprintln!("env {envs_str} git {args_str}"); + } + + cmd.kill_on_drop(true); + cmd.args(args); + cmd.current_dir(cwd); + + if let Some(envs) = envs { + cmd.envs(envs); + } + + let output = cmd.output().await?; + + #[cfg(any(test, debug_assertions))] + { + eprintln!( + "\n\n GIT STDOUT:\n\n{}\n\nGIT STDERR:\n\n{}\n\nGIT EXIT CODE: {}\n", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + output.status.code().unwrap_or(127) as usize + ); + } + + Ok(( + output.status.code().unwrap_or(127) as usize, + String::from_utf8_lossy(&output.stdout).trim().into(), + String::from_utf8_lossy(&output.stderr).trim().into(), + )) + } + + async unsafe fn create_askpass_server(&self) -> Result { + #[cfg(unix)] + { + Self::ServerHandle::new().await + } + #[cfg(windows)] + { + Self::ServerHandle::new() + } + } + + async fn stat>(&self, path: P) -> Result { + #[cfg(unix)] + { + self::unix::stat(path).await + } + #[cfg(windows)] + { + self::windows::stat(path).await + } + } +} diff --git a/gitbutler-git/src/executor/tokio/unix.rs b/gitbutler-git/src/executor/tokio/unix.rs new file mode 100644 index 000000000..45be9655a --- /dev/null +++ b/gitbutler-git/src/executor/tokio/unix.rs @@ -0,0 +1,106 @@ +use crate::executor::{AskpassServer, FileStat, Pid, Socket, Uid}; +use std::{ + fs::Permissions, + os::unix::fs::{MetadataExt, PermissionsExt}, + path::Path, + time::Duration, +}; +use tokio::{ + io::{AsyncBufReadExt, AsyncWriteExt, BufStream}, + net::{UnixListener, UnixStream}, +}; + +impl Socket for BufStream { + type Error = std::io::Error; + + fn pid(&self) -> Result { + self.get_ref() + .peer_cred() + .unwrap() + .pid() + .ok_or(std::io::Error::new( + std::io::ErrorKind::Other, + "no pid available for peer connection", + )) + } + + fn uid(&self) -> Result { + Ok(self.get_ref().peer_cred().unwrap().uid()) + } + + async fn read_line(&mut self) -> Result { + let mut buf = String::new(); + ::read_line(self, &mut buf).await?; + Ok(buf.trim_end_matches(|c| c == '\r' || c == '\n').into()) + } + + async fn write_line(&mut self, line: &str) -> Result<(), Self::Error> { + ::write_all(self, line.as_bytes()).await?; + ::write_all(self, b"\n").await?; + ::flush(self).await?; + Ok(()) + } +} + +/// A tokio-based askpass server implementation. +pub struct TokioAskpassServer { + // Always Some until dropped. + server: Option, + connection_string: String, +} + +impl TokioAskpassServer { + pub(crate) async fn new() -> Result { + let connection_string = + std::env::temp_dir().join(format!("gitbutler-askpass-{}", rand::random::())); + + let listener = UnixListener::bind(&connection_string)?; + + tokio::fs::set_permissions(&connection_string, Permissions::from_mode(0o0600)).await?; + + Ok(TokioAskpassServer { + server: Some(listener), + connection_string: connection_string.to_string_lossy().into(), + }) + } +} + +impl AskpassServer for TokioAskpassServer { + type Error = std::io::Error; + type SocketHandle = BufStream; + + async fn accept(&self, timeout: Option) -> Result { + let res = if let Some(timeout) = timeout { + tokio::time::timeout(timeout, self.server.as_ref().unwrap().accept()).await? + } else { + self.server.as_ref().unwrap().accept().await + }; + + res.map(|(s, _)| BufStream::new(s)) + } +} + +impl core::fmt::Display for TokioAskpassServer { + #[inline] + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.connection_string.fmt(f) + } +} + +impl Drop for TokioAskpassServer { + fn drop(&mut self) { + drop(self.server.take()); + // best-effort + std::fs::remove_file(&self.connection_string).ok(); + } +} + +pub async fn stat>(path: P) -> Result { + let metadata = tokio::fs::symlink_metadata(path).await?; + + Ok(FileStat { + dev: metadata.dev(), + ino: metadata.ino(), + is_regular_file: metadata.is_file(), + }) +} diff --git a/gitbutler-git/src/executor/tokio/windows.rs b/gitbutler-git/src/executor/tokio/windows.rs new file mode 100644 index 000000000..60c63686b --- /dev/null +++ b/gitbutler-git/src/executor/tokio/windows.rs @@ -0,0 +1,131 @@ +use crate::executor::{AskpassServer, FileStat, Pid, Socket}; +use std::{ + cell::RefCell, + os::windows::{fs::MetadataExt, io::AsRawHandle}, + path::Path, + time::Duration, +}; +use tokio::{ + io::{AsyncBufReadExt, AsyncWriteExt, BufStream}, + net::windows::named_pipe::{NamedPipeServer, ServerOptions}, + sync::Mutex, +}; + +const ASKPASS_PIPE_PREFIX: &str = r"\\.\pipe\gitbutler-askpass-"; + +impl Socket for BufStream { + type Error = std::io::Error; + + fn pid(&self) -> Result { + let raw_handle = self.get_ref().as_raw_handle(); + let mut out_pid: winapi::shared::minwindef::ULONG = 0; + + #[allow(unsafe_code)] + let r = unsafe { + winapi::um::winbase::GetNamedPipeClientProcessId( + // We need the `as` here to make rustdoc shut up + // about winapi using different type defs for docs. + raw_handle as winapi::um::winnt::HANDLE, + &mut out_pid, + ) + }; + + if r == 0 { + Err(std::io::Error::last_os_error()) + } else { + Ok(Pid::from(out_pid)) + } + } + + async fn read_line(&mut self) -> Result { + let mut buf = String::new(); + ::read_line(self, &mut buf).await?; + Ok(buf.trim_end_matches(|c| c == '\r' || c == '\n').into()) + } + + async fn write_line(&mut self, line: &str) -> Result<(), Self::Error> { + ::write_all(self, line.as_bytes()).await?; + ::write_all(self, b"\n").await?; + ::flush(self).await?; + Ok(()) + } +} + +/// A server for the `askpass` protocol using Tokio. +pub struct TokioAskpassServer { + server: Mutex>, + connection_string: String, +} + +impl TokioAskpassServer { + pub(crate) fn new() -> Result { + let connection_string = format!("{ASKPASS_PIPE_PREFIX}{}", rand::random::()); + + let server = Mutex::new(RefCell::new( + ServerOptions::new() + .first_pipe_instance(true) + .create(&connection_string)?, + )); + + Ok(TokioAskpassServer { + server, + connection_string, + }) + } +} + +impl AskpassServer for TokioAskpassServer { + type Error = std::io::Error; + type SocketHandle = BufStream; + + // We can ignore clippy here since we locked the mutex. + #[allow(clippy::await_holding_refcell_ref)] + async fn accept(&self, timeout: Option) -> Result { + let server = self.server.lock().await; + + if let Some(timeout) = timeout { + tokio::time::timeout(timeout, server.borrow().connect()).await??; + } else { + server.borrow().connect().await?; + } + + // Windows is weird. The server becomes the peer connection, + // and before we use the new connection, we first create + // a new server to listen for the next connection. + let client = server.replace(ServerOptions::new().create(&self.connection_string)?); + + Ok(tokio::io::BufStream::new(client)) + } +} + +impl core::fmt::Display for TokioAskpassServer { + #[inline] + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.connection_string.fmt(f) + } +} + +impl Drop for TokioAskpassServer { + fn drop(&mut self) { + // Best effort + let _ = self.server.get_mut().get_mut().disconnect(); + } +} + +pub async fn stat>(path: P) -> Result { + let metadata = tokio::fs::symlink_metadata(path).await?; + + // NOTE(qix-): We can safely unwrap here since the docs say: + // NOTE(qix-): + // NOTE(qix-): > This will return `None`` if the Metadata instance was created + // NOTE(qix-): > from a call to `DirEntry::metadata`. If this `Metadata` was created + // NOTE(qix-): > by using `fs::metadata` or `File::metadata`, then this will return `Some`. + // NOTE(qix-): + // NOTE(qix-): Thus, since we're not using directory entries, these are guaranteed to + // NOTE(qix-): return `Some`. + Ok(FileStat { + dev: metadata.volume_serial_number().unwrap().into(), + ino: metadata.file_index().unwrap(), + is_regular_file: metadata.is_file(), + }) +} diff --git a/gitbutler-git/src/lib.rs b/gitbutler-git/src/lib.rs index 4559b631f..d7cfff62e 100644 --- a/gitbutler-git/src/lib.rs +++ b/gitbutler-git/src/lib.rs @@ -7,14 +7,19 @@ #![deny(missing_docs, unsafe_code)] #![allow(async_fn_in_trait)] #![cfg_attr(test, feature(async_closure))] +#![cfg_attr(windows, feature(windows_by_handle))] #![feature(impl_trait_in_assoc_type)] -mod cli; mod error; +pub(crate) mod executor; mod refspec; +mod repository; + +#[cfg(feature = "tokio")] +pub use self::executor::tokio; pub use self::{ - cli::*, error::Error, refspec::{Error as RefSpecError, RefSpec}, + repository::{fetch, push}, }; diff --git a/gitbutler-git/src/cli/repository.rs b/gitbutler-git/src/repository.rs similarity index 93% rename from gitbutler-git/src/cli/repository.rs rename to gitbutler-git/src/repository.rs index 0234466f5..bab201496 100644 --- a/gitbutler-git/src/cli/repository.rs +++ b/gitbutler-git/src/repository.rs @@ -88,11 +88,20 @@ where let path = path.parent().unwrap(); let askpath_path = path - .with_file_name("gitbutler-git-askpass") + .with_file_name({ + #[cfg(unix)] + { + "gitbutler-git-askpass" + } + #[cfg(windows)] + { + "gitbutler-git-askpass.exe" + } + }) .to_string_lossy() .into_owned(); - #[cfg(not(target_os = "windows"))] + #[cfg(unix)] let setsid_path = path .with_file_name("gitbutler-git-setsid") .to_string_lossy() @@ -103,7 +112,7 @@ where .await .map_err(Error::::Exec)?; - #[cfg(not(target_os = "windows"))] + #[cfg(unix)] let setsid_stat = executor .stat(&setsid_path) .await @@ -150,11 +159,11 @@ where format!( "{}{base_ssh_command} -o StrictHostKeyChecking=accept-new -o KbdInteractiveAuthentication=no{}", { - #[cfg(not(target_os = "windows"))] + #[cfg(unix)] { format!("'{setsid_path}' ") } - #[cfg(target_os = "windows")] + #[cfg(windows)] { "" } @@ -198,6 +207,9 @@ where // TODO(qix-): see if dropping sysinfo for a more bespoke implementation is worth it. let mut system = sysinfo::System::new(); system.refresh_processes(); + + // We can ignore clippy here since the type is different depending on the platform. + #[allow(clippy::useless_conversion)] let peer_path = system .process(sysinfo::Pid::from_u32(peer_pid.try_into().map_err(|_| Error::::NoSuchPid(peer_pid))?)) .and_then(|p| p.exe().map(|exe| exe.to_string_lossy().into_owned())) @@ -206,15 +218,28 @@ where // stat the askpass executable that is being invoked let peer_stat = executor.stat(&peer_path).await.map_err(Error::::Exec)?; - if peer_stat.ino == askpath_stat.ino { + let valid_executable = if peer_stat.ino == askpath_stat.ino { if peer_stat.dev != askpath_stat.dev { return Err(Error::::AskpassDeviceMismatch)?; } - } else if peer_stat.ino == setsid_stat.ino { + + true + } else { + false + }; + + #[cfg(unix)] + let valid_executable = valid_executable || if peer_stat.ino == setsid_stat.ino { if peer_stat.dev != setsid_stat.dev { return Err(Error::::AskpassDeviceMismatch)?; } + + true } else { + false + }; + + if !valid_executable { return Err(Error::::AskpassExecutableMismatch)?; }