Merge branch 'master' into Convert-cloud-api-object-to-class

This commit is contained in:
Mattias Granlund 2024-03-21 20:20:14 +01:00 committed by GitHub
commit eb93713d69
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 575 additions and 278 deletions

68
Cargo.lock generated
View File

@ -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",

View File

@ -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(())
}

View File

@ -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"

View File

@ -1,2 +0,0 @@
#[cfg(feature = "cli")]
pub mod cli;

View File

@ -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):");
}

View File

@ -0,0 +1,5 @@
use std::os::unix::net::UnixStream;
pub fn establish(sock_path: &str) -> UnixStream {
UnixStream::connect(sock_path).expect("connect():")
}

View File

@ -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<Self>;
fn set_read_timeout(&self, timeout: Option<Duration>) -> io::Result<()>;
}
impl UnixCompatibility for PipeStream {
fn try_clone(&self) -> Option<Self> {
Some(unsafe { Self::from_raw_handle(self.as_raw_handle()) })
}
fn set_read_timeout(&self, timeout: Option<Duration>) -> 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(())
}
}
}

View File

@ -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");

View File

@ -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())

View File

@ -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.");
}

View File

@ -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;

View File

@ -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):");
}

View File

@ -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);
}

View File

@ -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<P: AsRef<Path>>(
&self,
args: &[&str],
cwd: P,
envs: Option<HashMap<String, String>>,
) -> 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::<Vec<_>>()
.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<Self::ServerHandle, Self::Error> {
let connection_string =
std::env::temp_dir().join(format!("gitbutler-askpass-{}", rand::random::<u64>()));
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<super::FileStat, Self::Error> {
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<tokio::net::UnixStream> {
type Error = std::io::Error;
fn pid(&self) -> Result<super::Pid, Self::Error> {
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<super::Uid, Self::Error> {
Ok(self.get_ref().peer_cred().unwrap().uid())
}
async fn read_line(&mut self) -> Result<String, Self::Error> {
let mut buf = String::new();
<Self as tokio::io::AsyncBufReadExt>::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> {
<Self as tokio::io::AsyncWriteExt>::write_all(self, line.as_bytes()).await?;
<Self as tokio::io::AsyncWriteExt>::write_all(self, b"\n").await?;
<Self as tokio::io::AsyncWriteExt>::flush(self).await?;
Ok(())
}
}
/// A tokio-based [`super::AskpassServer`] implementation.
#[cfg(unix)]
pub struct TokioAskpassServer {
// Always Some until dropped.
server: Option<tokio::net::UnixListener>,
connection_string: String,
}
#[cfg(unix)]
impl super::AskpassServer for TokioAskpassServer {
type Error = std::io::Error;
#[cfg(unix)]
type SocketHandle = tokio::io::BufStream<tokio::net::UnixStream>;
async fn accept(&self, timeout: Option<Duration>) -> Result<Self::SocketHandle, Self::Error> {
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();
}
}

View File

@ -142,7 +142,7 @@ pub unsafe trait GitExecutor {
/// during askpass authentication.
///
/// **Do not follow symbolic links.**
async fn stat(&self, path: &str) -> Result<FileStat, Self::Error>;
async fn stat<P: AsRef<Path>>(&self, path: P) -> Result<FileStat, Self::Error>;
}
/// Stats for a file on the filesystem.

View File

@ -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<P: AsRef<Path>>(
&self,
args: &[&str],
cwd: P,
envs: Option<HashMap<String, String>>,
) -> 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::<Vec<_>>()
.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<Self::ServerHandle, Self::Error> {
#[cfg(unix)]
{
Self::ServerHandle::new().await
}
#[cfg(windows)]
{
Self::ServerHandle::new()
}
}
async fn stat<P: AsRef<Path>>(&self, path: P) -> Result<super::FileStat, Self::Error> {
#[cfg(unix)]
{
self::unix::stat(path).await
}
#[cfg(windows)]
{
self::windows::stat(path).await
}
}
}

View File

@ -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<UnixStream> {
type Error = std::io::Error;
fn pid(&self) -> Result<Pid, Self::Error> {
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<Uid, Self::Error> {
Ok(self.get_ref().peer_cred().unwrap().uid())
}
async fn read_line(&mut self) -> Result<String, Self::Error> {
let mut buf = String::new();
<Self as AsyncBufReadExt>::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> {
<Self as AsyncWriteExt>::write_all(self, line.as_bytes()).await?;
<Self as AsyncWriteExt>::write_all(self, b"\n").await?;
<Self as AsyncWriteExt>::flush(self).await?;
Ok(())
}
}
/// A tokio-based askpass server implementation.
pub struct TokioAskpassServer {
// Always Some until dropped.
server: Option<UnixListener>,
connection_string: String,
}
impl TokioAskpassServer {
pub(crate) async fn new() -> Result<Self, std::io::Error> {
let connection_string =
std::env::temp_dir().join(format!("gitbutler-askpass-{}", rand::random::<u64>()));
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<UnixStream>;
async fn accept(&self, timeout: Option<Duration>) -> Result<Self::SocketHandle, Self::Error> {
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<P: AsRef<Path>>(path: P) -> Result<FileStat, std::io::Error> {
let metadata = tokio::fs::symlink_metadata(path).await?;
Ok(FileStat {
dev: metadata.dev(),
ino: metadata.ino(),
is_regular_file: metadata.is_file(),
})
}

View File

@ -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<NamedPipeServer> {
type Error = std::io::Error;
fn pid(&self) -> Result<Pid, Self::Error> {
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<String, Self::Error> {
let mut buf = String::new();
<Self as AsyncBufReadExt>::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> {
<Self as AsyncWriteExt>::write_all(self, line.as_bytes()).await?;
<Self as AsyncWriteExt>::write_all(self, b"\n").await?;
<Self as AsyncWriteExt>::flush(self).await?;
Ok(())
}
}
/// A server for the `askpass` protocol using Tokio.
pub struct TokioAskpassServer {
server: Mutex<RefCell<NamedPipeServer>>,
connection_string: String,
}
impl TokioAskpassServer {
pub(crate) fn new() -> Result<Self, std::io::Error> {
let connection_string = format!("{ASKPASS_PIPE_PREFIX}{}", rand::random::<u64>());
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<NamedPipeServer>;
// We can ignore clippy here since we locked the mutex.
#[allow(clippy::await_holding_refcell_ref)]
async fn accept(&self, timeout: Option<Duration>) -> Result<Self::SocketHandle, Self::Error> {
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<P: AsRef<Path>>(path: P) -> Result<FileStat, std::io::Error> {
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(),
})
}

View File

@ -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},
};

View File

@ -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::<E>::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::<E>::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::<E>::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::<E>::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::<E>::AskpassDeviceMismatch)?;
}
true
} else {
false
};
if !valid_executable {
return Err(Error::<E>::AskpassExecutableMismatch)?;
}