difftreelog
feat remowt-askpass, remowt-editor
in: trunk
5 files changed
cmds/remowt-agent/Cargo.tomldiffbeforeafterboth--- a/cmds/remowt-agent/Cargo.toml
+++ b/cmds/remowt-agent/Cargo.toml
@@ -4,22 +4,32 @@
edition = "2021"
[dependencies]
-anyhow = "1.0.86"
+anyhow.workspace = true
bifrostlink.workspace = true
bifrostlink-ports.workspace = true
-clap = { version = "4.5.13", features = ["derive"] }
-futures = "0.3.30"
-futures-util = "0.3.30"
-nix = "0.29.0"
-polkit-shared = { version = "0.1.0", path = "../../crates/polkit-shared" }
-rand = "0.8.5"
-remowt-link-shared = { version = "0.1.0", path = "../../crates/remowt-link-shared" }
-serde = { version = "1.0.204", features = ["derive"] }
-tokio = { version = "1.39.2", features = ["rt-multi-thread", "fs", "macros"] }
-tokio-util = { version = "0.7.11", features = ["codec"] }
-tracing = "0.1.40"
-tracing-subscriber = "0.3.18"
-ui-prompt = { version = "0.1.0", path = "../../crates/ui-prompt" }
-uuid = { version = "1.10.0", features = ["v4"] }
-zbus = { version = "4.4.0", features = ["tokio"] }
-zbus_polkit = { version = "4.0.0", features = ["tokio"] }
+clap = { workspace = true, features = ["derive"] }
+futures.workspace = true
+futures-util.workspace = true
+nix.workspace = true
+polkit-shared.workspace = true
+rand.workspace = true
+remowt-link-shared.workspace = true
+remowt-pty.workspace = true
+serde = { workspace = true, features = ["derive"] }
+tempfile.workspace = true
+tokio = { workspace = true, features = [
+ "rt",
+ "fs",
+ "macros",
+ "net",
+ "io-util",
+ "time",
+ "process",
+] }
+tokio-util = { workspace = true, features = ["codec"] }
+tracing.workspace = true
+tracing-subscriber.workspace = true
+ui-prompt.workspace = true
+uuid = { workspace = true, features = ["v4"] }
+zbus = { workspace = true, features = ["tokio"] }
+zbus_polkit = { workspace = true, features = ["tokio"] }
cmds/remowt-agent/src/askpass.rsdiffbeforeafterboth--- /dev/null
+++ b/cmds/remowt-agent/src/askpass.rs
@@ -0,0 +1,50 @@
+use std::borrow::Cow;
+use std::io::Write as _;
+
+use anyhow::Context as _;
+use ui_prompt::bifrost::PromptEndpointsClient;
+use ui_prompt::dbus::{DbusPrompterInterface, DbusPrompterProxy};
+use ui_prompt::Source;
+use zbus::Connection;
+
+use remowt_link_shared::BifConfig;
+
+const BUS_NAME: &str = "lach.RemowtAskpass";
+const PROMPTER_PATH: &str = "/lach/Askpass";
+
+pub async fn serve(
+ conn: &Connection,
+ prompter: PromptEndpointsClient<BifConfig>,
+) -> anyhow::Result<()> {
+ conn.object_server()
+ .at(PROMPTER_PATH, DbusPrompterInterface(prompter))
+ .await?;
+ conn.request_name(BUS_NAME).await?;
+ Ok(())
+}
+
+pub async fn ask(prompt: &str, description: String) -> anyhow::Result<()> {
+ let conn = Connection::session()
+ .await
+ .context("connecting to the session bus (DBUS_SESSION_BUS_ADDRESS)")?;
+ let proxy = DbusPrompterProxy::builder(&conn)
+ .destination(BUS_NAME)?
+ .path(PROMPTER_PATH)?
+ .build()
+ .await?;
+
+ let password = proxy
+ .prompt_text(
+ false,
+ prompt,
+ &description,
+ &[Source(Cow::Borrowed("remowt-askpass"))],
+ )
+ .await?;
+
+ let mut out = std::io::stdout().lock();
+ out.write_all(password.as_bytes())?;
+ out.write_all(b"\n")?;
+ out.flush()?;
+ Ok(())
+}
cmds/remowt-agent/src/bus.rsdiffbeforeafterboth--- /dev/null
+++ b/cmds/remowt-agent/src/bus.rs
@@ -0,0 +1,40 @@
+use std::process::Stdio;
+
+use anyhow::Context as _;
+use futures::StreamExt as _;
+use tokio::process::{Child, Command};
+use tokio_util::codec::{FramedRead, LinesCodec};
+use zbus::Connection;
+
+pub struct PrivateBus {
+ pub address: String,
+ pub conn: Connection,
+ _child: Child,
+}
+
+pub async fn spawn() -> anyhow::Result<PrivateBus> {
+ let mut child = Command::new("dbus-daemon")
+ .args(["--session", "--nofork", "--print-address"])
+ .stdout(Stdio::piped())
+ .kill_on_drop(true)
+ .spawn()
+ .context("spawning dbus-daemon for the private bus")?;
+
+ let stdout = child.stdout.take().expect("piped");
+ let address = FramedRead::new(stdout, LinesCodec::new())
+ .next()
+ .await
+ .context("dbus-daemon exited before printing its address")?
+ .context("reading dbus-daemon address")?;
+
+ let conn = zbus::connection::Builder::address(address.as_str())?
+ .build()
+ .await
+ .context("connecting to the private bus")?;
+
+ Ok(PrivateBus {
+ address,
+ conn,
+ _child: child,
+ })
+}
cmds/remowt-agent/src/editor.rsdiffbeforeafterboth1use std::env::{current_dir, temp_dir};2use std::path::Path;3use std::time::Duration;4use std::{fs, io};56use anyhow::{bail, Context as _};7use nix::libc;8use remowt_link_shared::editor::EditorEndpointsClient;9use tokio::process::Command;10use zbus::{fdo, interface, proxy, Connection};1112use remowt_link_shared::BifConfig;1314const BUS_NAME: &str = "lach.RemowtEditor";15const SERVICE_PATH: &str = "/lach/Editor";1617pub struct EditorService {18 editor: EditorEndpointsClient<BifConfig>,19}2021#[interface(name = "lach.RemowtEditor")]22impl EditorService {23 /// Attach the User's GUI to the nvim server at `socket_path` (on the remote),24 /// blocking until the user is done.25 async fn edit(&self, socket_path: String) -> fdo::Result<()> {26 self.editor27 .open_editor(socket_path)28 .await29 .map_err(|e| fdo::Error::Failed(format!("requesting editor on the User: {e}")))?30 .map_err(|e| fdo::Error::Failed(format!("editor failed: {e}")))?;31 Ok(())32 }33}3435pub async fn serve(36 conn: &Connection,37 editor: EditorEndpointsClient<BifConfig>,38) -> anyhow::Result<()> {39 conn.object_server()40 .at(SERVICE_PATH, EditorService { editor })41 .await?;42 conn.request_name(BUS_NAME).await?;43 Ok(())44}4546#[proxy(interface = "lach.RemowtEditor")]47trait RemowtEditor {48 async fn edit(&self, socket_path: &str) -> fdo::Result<()>;49}5051pub async fn edit(path: String) -> anyhow::Result<()> {52 let path = Path::new(&path);53 let abs = if path.is_absolute() {54 path.to_path_buf()55 } else {56 current_dir()?.join(path)57 };5859 let sock = temp_dir().join(format!("remowt-nvim-{}.sock", uuid::Uuid::new_v4()));60 let sock_str = sock61 .to_str()62 .context("temp socket path is not utf-8")?63 .to_owned();6465 let mut child = Command::new("nvim");66 child67 .arg("--headless")68 .arg("--listen")69 .arg(&sock)70 .arg("--")71 .arg(&abs)72 .kill_on_drop(true);73 // SAFETY: only an async-signal-safe `prctl` call.74 unsafe {75 child.pre_exec(|| {76 if libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL as libc::c_ulong) != 0 {77 return Err(io::Error::last_os_error());78 }79 Ok(())80 });81 }82 let mut child = child.spawn().context("spawning nvim")?;8384 wait_for_socket(&sock)85 .await86 .context("nvim did not start its server")?;8788 let conn = Connection::session()89 .await90 .context("connecting to the session bus (DBUS_SESSION_BUS_ADDRESS)")?;91 let proxy = RemowtEditorProxy::builder(&conn)92 .destination(BUS_NAME)?93 .path(SERVICE_PATH)?94 .build()95 .await?;96 let result = proxy.edit(&sock_str).await;9798 if tokio::time::timeout(Duration::from_secs(2), child.wait())99 .await100 .is_err()101 {102 let _ = child.kill().await;103 }104 let _ = fs::remove_file(&sock);105106 result?;107 Ok(())108}109110/// Poll for `path` to appear (nvim creating its listen socket), up to ~10s.111async fn wait_for_socket(path: &Path) -> anyhow::Result<()> {112 for _ in 0..200 {113 if tokio::fs::try_exists(path).await.unwrap_or(false) {114 return Ok(());115 }116 tokio::time::sleep(Duration::from_millis(50)).await;117 }118 bail!("timed out waiting for {}", path.display())119}cmds/remowt-agent/src/main.rsdiffbeforeafterboth--- a/cmds/remowt-agent/src/main.rs
+++ b/cmds/remowt-agent/src/main.rs
@@ -1,29 +1,36 @@
use std::borrow::Cow;
use std::collections::{BTreeMap, HashMap};
-use std::future;
-use std::io::{stdout, Write};
+use std::fs::Permissions;
+use std::future::pending;
+use std::os::unix::fs::PermissionsExt as _;
use std::path::PathBuf;
use std::sync::{Arc, Mutex, OnceLock};
-use bifrostlink::{AddressT, Rpc};
+use bifrostlink::declarative::RemoteEndpoints;
+use bifrostlink::Rpc;
+use bifrostlink_ports::stdio::from_stdio;
use bifrostlink_ports::unix_socket::from_socket;
use clap::Parser;
use polkit_shared::{emphasize, BackendRequest, Identity, PidDisplay};
-use remowt_link_shared::Address;
-use tokio::io::{AsyncReadExt, AsyncWriteExt};
+use remowt_link_shared::editor::EditorEndpointsClient;
+use remowt_link_shared::{Address, BifConfig, Fs, Pty, Systemd};
+use tokio::fs;
use tokio::net::UnixStream;
-use tokio::runtime::Runtime;
+use tokio::runtime::Builder;
use tokio::task::AbortHandle;
use tracing::{info, trace};
-use ui_prompt::rofi::RofiPrompter;
+use ui_prompt::bifrost::PromptEndpointsClient;
use ui_prompt::{PrependSourcePrompter, Prompter, Source};
use zbus::fdo;
use zbus::zvariant::{OwnedValue, Str};
use zbus::{interface, proxy, Connection};
use zbus_polkit::policykit1::Subject;
-use self::helper::{Helper, SuidHelper};
+use self::helper::{Helper, SocketHelper, SuidHelper};
+pub mod askpass;
+pub mod bus;
+pub mod editor;
pub mod helper;
struct CancelTaskOnDrop {
@@ -44,23 +51,26 @@
}
}
-struct Agent<H> {
+struct Agent<H, P> {
tasks: Arc<Mutex<HashMap<String, AbortHandle>>>,
helper: H,
+ prompter: P,
}
-impl<H> Agent<H> {
- fn new(helper: H) -> Self {
+impl<H, P> Agent<H, P> {
+ fn new(helper: H, prompter: P) -> Self {
Agent {
tasks: Arc::new(Mutex::new(HashMap::new())),
helper,
+ prompter,
}
}
}
#[interface(name = "org.freedesktop.PolicyKit1.AuthenticationAgent")]
-impl<H> Agent<H>
+impl<H, P> Agent<H, P>
where
H: Helper + Clone + Send + Sync + 'static,
+ P: Prompter + Clone + Send + Sync + 'static,
{
/// BeginAuthentication method
#[allow(clippy::too_many_arguments)]
@@ -68,7 +78,7 @@
&self,
action_id: String,
message: String,
- icon_name: String,
+ _icon_name: String,
mut details: BTreeMap<String, String>,
cookie: String,
identities: Vec<Identity>,
@@ -78,6 +88,7 @@
let _cancel_guard = Arc::new(OnceLock::new());
let task = {
let helper = self.helper.clone();
+ let prompter = self.prompter.clone();
let cookie = cookie.clone();
let _cancel_guard = _cancel_guard.clone();
tokio::task::spawn(async move {
@@ -103,7 +114,7 @@
let mut prompter = PrependSourcePrompter {
source: vec![Source(Cow::Borrowed("polkit agent"))],
description: description.clone(),
- prompter: RofiPrompter,
+ prompter,
};
let identity_displays: Vec<String> =
@@ -194,76 +205,171 @@
#[derive(Parser)]
enum Opts {
- Agent,
AskPass {
+ prompt: String,
description: String,
},
+ Editor {
+ /// Argument to nvim
+ path: String,
+ },
RealAgent {
#[arg(long)]
- path: PathBuf,
+ path: Option<PathBuf>,
+ /// Expect own address to be AgentPrivileged, skip installing polkit agent
+ #[arg(long)]
+ privileged: bool,
},
}
fn main() -> anyhow::Result<()> {
- tracing_subscriber::fmt::init();
+ // Log to stderr: `privileged-agent` uses stdout as the bifrost transport,
+ // so anything written there would corrupt the stream.
+ tracing_subscriber::fmt()
+ .with_writer(std::io::stderr)
+ .init();
let opts = Opts::parse();
- let runtime = Runtime::new()?;
+ let runtime = Builder::new_current_thread().enable_all().build()?;
match opts {
- Opts::Agent => {
- // TODO: Setup env, directories with various things...
- runtime.block_on(main_agent())
- }
- Opts::AskPass { description } => runtime.block_on(main_askpass(description)),
- Opts::RealAgent { path } => runtime.block_on(main_real_agent(path)),
+ Opts::AskPass {
+ prompt,
+ description,
+ } => runtime.block_on(askpass::ask(&prompt, description)),
+ Opts::Editor { path } => runtime.block_on(editor::edit(path)),
+ Opts::RealAgent { path, privileged } => runtime.block_on(main_real_agent(path, privileged)),
}
}
-async fn main_real_agent(path: PathBuf) -> anyhow::Result<()> {
- let mut stream = UnixStream::connect(path).await?;
- stream.write_all(b"REMOWT_HELLO\0").await?;
- let mut buf = [0u8; 12];
- stream.read_exact(&mut buf).await?;
- assert_eq!(&buf, b"REMOWT_EHLO\0");
- let port = from_socket(stream);
- let rpc = Rpc::<Address, remowt_link_shared::Error>::new(Address::Agent);
- rpc.add_direct(Address::User, port, bifrostlink::Rtt(0));
- Ok(())
+async fn main_real_agent(path: Option<PathBuf>, privileged: bool) -> anyhow::Result<()> {
+ let address = if privileged {
+ Address::AgentPrivileged
+ } else {
+ Address::Agent
+ };
+ let mut rpc = Rpc::<BifConfig>::new(address);
+
+ Fs::new().register_endpoints(&mut rpc);
+ Systemd.register_endpoints(&mut rpc);
+ Pty::new().register_endpoints(&mut rpc);
+
+ let user_prompter = PromptEndpointsClient::wrap(rpc.remote(Address::User));
+ let editor_client = EditorEndpointsClient::wrap(rpc.remote(Address::User));
+
+ let bus = bus::spawn().await?;
+ askpass::serve(&bus.conn, user_prompter.clone()).await?;
+ editor::serve(&bus.conn, editor_client).await?;
+
+ let helpers = tempfile::Builder::new().prefix("remowt-path.").tempdir()?;
+ let exe = std::env::current_exe()?;
+ let askpass_helper = helpers.path().join("remowt-askpass");
+ let editor_helper = helpers.path().join("remowt-editor");
+ {
+ let script = format!(
+ "#!/bin/sh\nexec {} ask-pass \"password\" \"$1\"\n",
+ sh_quote(&exe.to_string_lossy())
+ );
+ fs::write(&askpass_helper, script).await?;
+ fs::set_permissions(&askpass_helper, Permissions::from_mode(0o755)).await?;
+ }
+ {
+ let script = format!(
+ "#!/bin/sh\nexec {} editor \"$1\"\n",
+ sh_quote(&exe.to_string_lossy())
+ );
+ fs::write(&editor_helper, script).await?;
+ fs::set_permissions(&editor_helper, Permissions::from_mode(0o755)).await?;
+ }
+
+ // Safety: Hoping tokio own threads won't read any of those...
+ unsafe {
+ prepend_path(helpers.path());
+ std::env::set_var("SUDO_ASKPASS", &askpass_helper);
+ std::env::set_var("SSH_ASKPASS", &askpass_helper);
+ std::env::set_var("SSH_ASKPASS_REQUIRE", "force");
+ std::env::set_var("EDITOR", &editor_helper);
+ std::env::set_var("VISUAL", &editor_helper);
+ std::env::set_var("DBUS_SESSION_BUS_ADDRESS", &bus.address);
+ }
+
+ let port = match path {
+ Some(path) => from_socket(UnixStream::connect(path).await?),
+ None => from_stdio(),
+ };
+ rpc.add_direct(Address::User, port, bifrostlink::Rtt(0));
+
+ let polkit_conn = if !privileged {
+ // The unprivileged agent doubles as a polkit authentication agent so
+ // `run0` (e.g. our own elevation) routes its prompt to the User over
+ // bifrost instead of failing on a tty-less session.
+ let conn = Connection::system().await?;
+ let helper = SocketHelper {
+ fallback: SuidHelper,
+ };
+ register_auth_agent(&conn, Agent::new(helper, user_prompter)).await?;
+ Some(conn)
+ } else {
+ None
+ };
+
+ let _keep_alive = (bus, helpers, polkit_conn);
+ pending().await
}
-async fn main_agent() -> anyhow::Result<()> {
- trace!("started");
- let conn = Connection::system().await?;
+async fn register_auth_agent<H, P>(conn: &Connection, agent: Agent<H, P>) -> anyhow::Result<()>
+where
+ H: Helper + Clone + Send + Sync + 'static,
+ P: Prompter + Clone + Send + Sync + 'static,
+{
+ let proxy = zbus_polkit::policykit1::AuthorityProxy::new(conn).await?;
+ conn.object_server().at(OBJ_PATH, agent).await?;
- let proxy = zbus_polkit::policykit1::AuthorityProxy::new(&conn).await?;
- conn.object_server()
- .at(OBJ_PATH, Agent::new(SuidHelper))
+ let subject = auth_agent_subject()?;
+ proxy
+ .register_authentication_agent(&subject, "C", OBJ_PATH)
.await?;
+ info!(kind = subject.subject_kind, "registered polkit agent");
+ Ok(())
+}
- let session_id = std::env::var("XDG_SESSION_ID")?;
+fn auth_agent_subject() -> anyhow::Result<Subject> {
let mut details = HashMap::new();
- let val: OwnedValue = {
- let wrapped: Str<'_> = session_id.into();
- wrapped.into()
- };
- details.insert("session-id".to_string(), val);
- proxy
- .register_authentication_agent(
- &Subject {
- subject_kind: "unix-session".to_string(),
- subject_details: details,
- },
- "C",
- OBJ_PATH,
- )
- .await?;
- future::pending().await
+ if let Ok(session_id) = std::env::var("XDG_SESSION_ID") {
+ let val: OwnedValue = Str::from(session_id).into();
+ details.insert("session-id".to_string(), val);
+ return Ok(Subject {
+ subject_kind: "unix-session".to_string(),
+ subject_details: details,
+ });
+ }
+
+ details.insert("pid".to_string(), OwnedValue::from(std::process::id()));
+ Ok(Subject {
+ subject_kind: "unix-process".to_string(),
+ subject_details: details,
+ })
+}
+
+fn sh_quote(s: &str) -> String {
+ format!("'{}'", s.replace('\'', "'\\''"))
}
-async fn main_askpass(description: String) -> anyhow::Result<()> {
- let password = RofiPrompter
- .prompt_text(false, &description, "SSH password request", &[])
- .await?;
- stdout().lock().write_all(password.as_bytes())?;
- future::pending().await
+/// Prepend `dir` to the process `PATH`.
+///
+/// # SAFETY
+///
+/// Same as `set_var`
+unsafe fn prepend_path(dir: &std::path::Path) {
+ let value = match std::env::var_os("PATH") {
+ Some(existing) => {
+ let mut v = dir.as_os_str().to_owned();
+ v.push(":");
+ v.push(existing);
+ v
+ }
+ None => dir.as_os_str().to_owned(),
+ };
+ unsafe {
+ std::env::set_var("PATH", value);
+ }
}