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.rsdiffbeforeafterboth--- /dev/null
+++ b/cmds/remowt-agent/src/editor.rs
@@ -0,0 +1,119 @@
+use std::env::{current_dir, temp_dir};
+use std::path::Path;
+use std::time::Duration;
+use std::{fs, io};
+
+use anyhow::{bail, Context as _};
+use nix::libc;
+use remowt_link_shared::editor::EditorEndpointsClient;
+use tokio::process::Command;
+use zbus::{fdo, interface, proxy, Connection};
+
+use remowt_link_shared::BifConfig;
+
+const BUS_NAME: &str = "lach.RemowtEditor";
+const SERVICE_PATH: &str = "/lach/Editor";
+
+pub struct EditorService {
+ editor: EditorEndpointsClient<BifConfig>,
+}
+
+#[interface(name = "lach.RemowtEditor")]
+impl EditorService {
+ /// Attach the User's GUI to the nvim server at `socket_path` (on the remote),
+ /// blocking until the user is done.
+ async fn edit(&self, socket_path: String) -> fdo::Result<()> {
+ self.editor
+ .open_editor(socket_path)
+ .await
+ .map_err(|e| fdo::Error::Failed(format!("requesting editor on the User: {e}")))?
+ .map_err(|e| fdo::Error::Failed(format!("editor failed: {e}")))?;
+ Ok(())
+ }
+}
+
+pub async fn serve(
+ conn: &Connection,
+ editor: EditorEndpointsClient<BifConfig>,
+) -> anyhow::Result<()> {
+ conn.object_server()
+ .at(SERVICE_PATH, EditorService { editor })
+ .await?;
+ conn.request_name(BUS_NAME).await?;
+ Ok(())
+}
+
+#[proxy(interface = "lach.RemowtEditor")]
+trait RemowtEditor {
+ async fn edit(&self, socket_path: &str) -> fdo::Result<()>;
+}
+
+pub async fn edit(path: String) -> anyhow::Result<()> {
+ let path = Path::new(&path);
+ let abs = if path.is_absolute() {
+ path.to_path_buf()
+ } else {
+ current_dir()?.join(path)
+ };
+
+ let sock = temp_dir().join(format!("remowt-nvim-{}.sock", uuid::Uuid::new_v4()));
+ let sock_str = sock
+ .to_str()
+ .context("temp socket path is not utf-8")?
+ .to_owned();
+
+ let mut child = Command::new("nvim");
+ child
+ .arg("--headless")
+ .arg("--listen")
+ .arg(&sock)
+ .arg("--")
+ .arg(&abs)
+ .kill_on_drop(true);
+ // SAFETY: only an async-signal-safe `prctl` call.
+ unsafe {
+ child.pre_exec(|| {
+ if libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL as libc::c_ulong) != 0 {
+ return Err(io::Error::last_os_error());
+ }
+ Ok(())
+ });
+ }
+ let mut child = child.spawn().context("spawning nvim")?;
+
+ wait_for_socket(&sock)
+ .await
+ .context("nvim did not start its server")?;
+
+ let conn = Connection::session()
+ .await
+ .context("connecting to the session bus (DBUS_SESSION_BUS_ADDRESS)")?;
+ let proxy = RemowtEditorProxy::builder(&conn)
+ .destination(BUS_NAME)?
+ .path(SERVICE_PATH)?
+ .build()
+ .await?;
+ let result = proxy.edit(&sock_str).await;
+
+ if tokio::time::timeout(Duration::from_secs(2), child.wait())
+ .await
+ .is_err()
+ {
+ let _ = child.kill().await;
+ }
+ let _ = fs::remove_file(&sock);
+
+ result?;
+ Ok(())
+}
+
+/// Poll for `path` to appear (nvim creating its listen socket), up to ~10s.
+async fn wait_for_socket(path: &Path) -> anyhow::Result<()> {
+ for _ in 0..200 {
+ if tokio::fs::try_exists(path).await.unwrap_or(false) {
+ return Ok(());
+ }
+ tokio::time::sleep(Duration::from_millis(50)).await;
+ }
+ bail!("timed out waiting for {}", path.display())
+}
cmds/remowt-agent/src/main.rsdiffbeforeafterboth1use std::borrow::Cow;2use std::collections::{BTreeMap, HashMap};3use std::future;4use std::io::{stdout, Write};5use std::path::PathBuf;6use std::sync::{Arc, Mutex, OnceLock};78use bifrostlink::{AddressT, Rpc};9use bifrostlink_ports::unix_socket::from_socket;10use clap::Parser;11use polkit_shared::{emphasize, BackendRequest, Identity, PidDisplay};12use remowt_link_shared::Address;13use tokio::io::{AsyncReadExt, AsyncWriteExt};14use tokio::net::UnixStream;15use tokio::runtime::Runtime;16use tokio::task::AbortHandle;17use tracing::{info, trace};18use ui_prompt::rofi::RofiPrompter;19use ui_prompt::{PrependSourcePrompter, Prompter, Source};20use zbus::fdo;21use zbus::zvariant::{OwnedValue, Str};22use zbus::{interface, proxy, Connection};23use zbus_polkit::policykit1::Subject;2425use self::helper::{Helper, SuidHelper};2627pub mod helper;2829struct CancelTaskOnDrop {30 tasks: Arc<Mutex<HashMap<String, AbortHandle>>>,31 handle: String,32}33impl Drop for CancelTaskOnDrop {34 fn drop(&mut self) {35 info!("cancel on drop");36 if let Some(task) = self37 .tasks38 .lock()39 .expect("not poisoned")40 .remove(&self.handle)41 {42 task.abort();43 }44 }45}4647struct Agent<H> {48 tasks: Arc<Mutex<HashMap<String, AbortHandle>>>,49 helper: H,50}51impl<H> Agent<H> {52 fn new(helper: H) -> Self {53 Agent {54 tasks: Arc::new(Mutex::new(HashMap::new())),55 helper,56 }57 }58}5960#[interface(name = "org.freedesktop.PolicyKit1.AuthenticationAgent")]61impl<H> Agent<H>62where63 H: Helper + Clone + Send + Sync + 'static,64{65 /// BeginAuthentication method66 #[allow(clippy::too_many_arguments)]67 async fn begin_authentication(68 &self,69 action_id: String,70 message: String,71 icon_name: String,72 mut details: BTreeMap<String, String>,73 cookie: String,74 identities: Vec<Identity>,75 ) -> zbus::fdo::Result<()> {76 use std::fmt::Write;77 info!("begin auth");78 let _cancel_guard = Arc::new(OnceLock::new());79 let task = {80 let helper = self.helper.clone();81 let cookie = cookie.clone();82 let _cancel_guard = _cancel_guard.clone();83 tokio::task::spawn(async move {84 let _cancel_guard = _cancel_guard.clone();85 trace!("conversation task");86 let mut description = format!("{message}\n\n<b>Action id:</b> {action_id}",);87 if let Some(subject) = details.remove("polkit.caller-pid") {88 let _ = write!(description, "\n<b>Caller:</b> ");89 if let Ok(pid) = subject.parse::<u32>() {90 let _ = write!(description, "{}", PidDisplay(pid));91 } else {92 let _ = write!(description, "{}", emphasize("invalid pid"));93 }94 }95 if let Some(subject) = details.remove("polkit.subject-pid") {96 let _ = write!(description, "\n<b>Subject:</b> ");97 if let Ok(pid) = subject.parse::<u32>() {98 let _ = write!(description, "{}", PidDisplay(pid));99 } else {100 let _ = write!(description, "{}", emphasize("invalid pid"));101 }102 }103 let mut prompter = PrependSourcePrompter {104 source: vec![Source(Cow::Borrowed("polkit agent"))],105 description: description.clone(),106 prompter: RofiPrompter,107 };108109 let identity_displays: Vec<String> =110 identities.iter().map(|v| v.to_string()).collect();111 let identity_displays: Vec<&str> =112 identity_displays.iter().map(|v| v.as_str()).collect();113 info!("choose identity");114 let choosen_identity = match identity_displays.len() {115 0 => {116 return Err(fdo::Error::AuthFailed(117 "no identity to authenticate as".to_owned(),118 ))119 }120 1 => 0,121 _ => {122 prompter123 .prompt_enum(124 "Identity",125 "Select identity to use for polkit authorization",126 &identity_displays,127 &[],128 )129 .await?130 }131 };132 info!("identity chosen");133134 let _ = write!(135 description,136 "\n<b>Identity:</b> {}",137 identities[choosen_identity as usize]138 );139 prompter.description = description;140141 prompter.source.push(Source(Cow::Borrowed("polkit daemon")));142143 helper144 .help_me(145 &cookie,146 prompter,147 identities[choosen_identity as usize].clone(),148 )149 .await150 .map_err(|e| fdo::Error::Failed(e.to_string()))?;151 // let connection = Connection::system().await?;152 // let helper = PolkitHelperProxy::new(&connection).await?;153154 Ok(())155 })156 };157 self.tasks158 .lock()159 .unwrap()160 .insert(cookie.clone(), task.abort_handle());161 info!("abort handle stored");162 let _ = _cancel_guard.set(CancelTaskOnDrop {163 tasks: self.tasks.clone(),164 handle: cookie.clone(),165 });166167 let _ = task.await;168169 Ok(())170 }171172 /// CancelAuthentication method173 async fn cancel_authentication(&self, cookie: &str) -> zbus::fdo::Result<()> {174 info!("auth cancelled");175 if let Some(abort) = self.tasks.lock().unwrap().remove(cookie) {176 info!("abort handle found");177 abort.abort();178 }179 // debug!("Authentication cancled ! {cookie}");180 Ok(())181 }182}183184const OBJ_PATH: &str = "/org/freedesktop/PolicyKit1/AuthenticationAgent";185186#[proxy(187 interface = "lach.PolkitHelper",188 default_service = "lach.polkit.helper1",189 default_path = "/lach/PolkitHelper"190)]191trait PolkitHelper {192 fn init_conversation(&self, request: BackendRequest) -> zbus::Result<()>;193}194195#[derive(Parser)]196enum Opts {197 Agent,198 AskPass {199 description: String,200 },201 RealAgent {202 #[arg(long)]203 path: PathBuf,204 },205}206207fn main() -> anyhow::Result<()> {208 tracing_subscriber::fmt::init();209 let opts = Opts::parse();210211 let runtime = Runtime::new()?;212213 match opts {214 Opts::Agent => {215 // TODO: Setup env, directories with various things...216 runtime.block_on(main_agent())217 }218 Opts::AskPass { description } => runtime.block_on(main_askpass(description)),219 Opts::RealAgent { path } => runtime.block_on(main_real_agent(path)),220 }221}222async fn main_real_agent(path: PathBuf) -> anyhow::Result<()> {223 let mut stream = UnixStream::connect(path).await?;224 stream.write_all(b"REMOWT_HELLO\0").await?;225 let mut buf = [0u8; 12];226 stream.read_exact(&mut buf).await?;227 assert_eq!(&buf, b"REMOWT_EHLO\0");228 let port = from_socket(stream);229 let rpc = Rpc::<Address, remowt_link_shared::Error>::new(Address::Agent);230 rpc.add_direct(Address::User, port, bifrostlink::Rtt(0));231 Ok(())232}233234async fn main_agent() -> anyhow::Result<()> {235 trace!("started");236 let conn = Connection::system().await?;237238 let proxy = zbus_polkit::policykit1::AuthorityProxy::new(&conn).await?;239 conn.object_server()240 .at(OBJ_PATH, Agent::new(SuidHelper))241 .await?;242243 let session_id = std::env::var("XDG_SESSION_ID")?;244 let mut details = HashMap::new();245 let val: OwnedValue = {246 let wrapped: Str<'_> = session_id.into();247 wrapped.into()248 };249 details.insert("session-id".to_string(), val);250 proxy251 .register_authentication_agent(252 &Subject {253 subject_kind: "unix-session".to_string(),254 subject_details: details,255 },256 "C",257 OBJ_PATH,258 )259 .await?;260 future::pending().await261}262263async fn main_askpass(description: String) -> anyhow::Result<()> {264 let password = RofiPrompter265 .prompt_text(false, &description, "SSH password request", &[])266 .await?;267 stdout().lock().write_all(password.as_bytes())?;268 future::pending().await269}1use std::borrow::Cow;2use std::collections::{BTreeMap, HashMap};3use std::fs::Permissions;4use std::future::pending;5use std::os::unix::fs::PermissionsExt as _;6use std::path::PathBuf;7use std::sync::{Arc, Mutex, OnceLock};89use bifrostlink::declarative::RemoteEndpoints;10use bifrostlink::Rpc;11use bifrostlink_ports::stdio::from_stdio;12use bifrostlink_ports::unix_socket::from_socket;13use clap::Parser;14use polkit_shared::{emphasize, BackendRequest, Identity, PidDisplay};15use remowt_link_shared::editor::EditorEndpointsClient;16use remowt_link_shared::{Address, BifConfig, Fs, Pty, Systemd};17use tokio::fs;18use tokio::net::UnixStream;19use tokio::runtime::Builder;20use tokio::task::AbortHandle;21use tracing::{info, trace};22use ui_prompt::bifrost::PromptEndpointsClient;23use ui_prompt::{PrependSourcePrompter, Prompter, Source};24use zbus::fdo;25use zbus::zvariant::{OwnedValue, Str};26use zbus::{interface, proxy, Connection};27use zbus_polkit::policykit1::Subject;2829use self::helper::{Helper, SocketHelper, SuidHelper};3031pub mod askpass;32pub mod bus;33pub mod editor;34pub mod helper;3536struct CancelTaskOnDrop {37 tasks: Arc<Mutex<HashMap<String, AbortHandle>>>,38 handle: String,39}40impl Drop for CancelTaskOnDrop {41 fn drop(&mut self) {42 info!("cancel on drop");43 if let Some(task) = self44 .tasks45 .lock()46 .expect("not poisoned")47 .remove(&self.handle)48 {49 task.abort();50 }51 }52}5354struct Agent<H, P> {55 tasks: Arc<Mutex<HashMap<String, AbortHandle>>>,56 helper: H,57 prompter: P,58}59impl<H, P> Agent<H, P> {60 fn new(helper: H, prompter: P) -> Self {61 Agent {62 tasks: Arc::new(Mutex::new(HashMap::new())),63 helper,64 prompter,65 }66 }67}6869#[interface(name = "org.freedesktop.PolicyKit1.AuthenticationAgent")]70impl<H, P> Agent<H, P>71where72 H: Helper + Clone + Send + Sync + 'static,73 P: Prompter + Clone + Send + Sync + 'static,74{75 /// BeginAuthentication method76 #[allow(clippy::too_many_arguments)]77 async fn begin_authentication(78 &self,79 action_id: String,80 message: String,81 _icon_name: String,82 mut details: BTreeMap<String, String>,83 cookie: String,84 identities: Vec<Identity>,85 ) -> zbus::fdo::Result<()> {86 use std::fmt::Write;87 info!("begin auth");88 let _cancel_guard = Arc::new(OnceLock::new());89 let task = {90 let helper = self.helper.clone();91 let prompter = self.prompter.clone();92 let cookie = cookie.clone();93 let _cancel_guard = _cancel_guard.clone();94 tokio::task::spawn(async move {95 let _cancel_guard = _cancel_guard.clone();96 trace!("conversation task");97 let mut description = format!("{message}\n\n<b>Action id:</b> {action_id}",);98 if let Some(subject) = details.remove("polkit.caller-pid") {99 let _ = write!(description, "\n<b>Caller:</b> ");100 if let Ok(pid) = subject.parse::<u32>() {101 let _ = write!(description, "{}", PidDisplay(pid));102 } else {103 let _ = write!(description, "{}", emphasize("invalid pid"));104 }105 }106 if let Some(subject) = details.remove("polkit.subject-pid") {107 let _ = write!(description, "\n<b>Subject:</b> ");108 if let Ok(pid) = subject.parse::<u32>() {109 let _ = write!(description, "{}", PidDisplay(pid));110 } else {111 let _ = write!(description, "{}", emphasize("invalid pid"));112 }113 }114 let mut prompter = PrependSourcePrompter {115 source: vec![Source(Cow::Borrowed("polkit agent"))],116 description: description.clone(),117 prompter,118 };119120 let identity_displays: Vec<String> =121 identities.iter().map(|v| v.to_string()).collect();122 let identity_displays: Vec<&str> =123 identity_displays.iter().map(|v| v.as_str()).collect();124 info!("choose identity");125 let choosen_identity = match identity_displays.len() {126 0 => {127 return Err(fdo::Error::AuthFailed(128 "no identity to authenticate as".to_owned(),129 ))130 }131 1 => 0,132 _ => {133 prompter134 .prompt_enum(135 "Identity",136 "Select identity to use for polkit authorization",137 &identity_displays,138 &[],139 )140 .await?141 }142 };143 info!("identity chosen");144145 let _ = write!(146 description,147 "\n<b>Identity:</b> {}",148 identities[choosen_identity as usize]149 );150 prompter.description = description;151152 prompter.source.push(Source(Cow::Borrowed("polkit daemon")));153154 helper155 .help_me(156 &cookie,157 prompter,158 identities[choosen_identity as usize].clone(),159 )160 .await161 .map_err(|e| fdo::Error::Failed(e.to_string()))?;162 // let connection = Connection::system().await?;163 // let helper = PolkitHelperProxy::new(&connection).await?;164165 Ok(())166 })167 };168 self.tasks169 .lock()170 .unwrap()171 .insert(cookie.clone(), task.abort_handle());172 info!("abort handle stored");173 let _ = _cancel_guard.set(CancelTaskOnDrop {174 tasks: self.tasks.clone(),175 handle: cookie.clone(),176 });177178 let _ = task.await;179180 Ok(())181 }182183 /// CancelAuthentication method184 async fn cancel_authentication(&self, cookie: &str) -> zbus::fdo::Result<()> {185 info!("auth cancelled");186 if let Some(abort) = self.tasks.lock().unwrap().remove(cookie) {187 info!("abort handle found");188 abort.abort();189 }190 // debug!("Authentication cancled ! {cookie}");191 Ok(())192 }193}194195const OBJ_PATH: &str = "/org/freedesktop/PolicyKit1/AuthenticationAgent";196197#[proxy(198 interface = "lach.PolkitHelper",199 default_service = "lach.polkit.helper1",200 default_path = "/lach/PolkitHelper"201)]202trait PolkitHelper {203 fn init_conversation(&self, request: BackendRequest) -> zbus::Result<()>;204}205206#[derive(Parser)]207enum Opts {208 AskPass {209 prompt: String,210 description: String,211 },212 Editor {213 /// Argument to nvim214 path: String,215 },216 RealAgent {217 #[arg(long)]218 path: Option<PathBuf>,219 /// Expect own address to be AgentPrivileged, skip installing polkit agent220 #[arg(long)]221 privileged: bool,222 },223}224225fn main() -> anyhow::Result<()> {226 // Log to stderr: `privileged-agent` uses stdout as the bifrost transport,227 // so anything written there would corrupt the stream.228 tracing_subscriber::fmt()229 .with_writer(std::io::stderr)230 .init();231 let opts = Opts::parse();232233 let runtime = Builder::new_current_thread().enable_all().build()?;234235 match opts {236 Opts::AskPass {237 prompt,238 description,239 } => runtime.block_on(askpass::ask(&prompt, description)),240 Opts::Editor { path } => runtime.block_on(editor::edit(path)),241 Opts::RealAgent { path, privileged } => runtime.block_on(main_real_agent(path, privileged)),242 }243}244async fn main_real_agent(path: Option<PathBuf>, privileged: bool) -> anyhow::Result<()> {245 let address = if privileged {246 Address::AgentPrivileged247 } else {248 Address::Agent249 };250 let mut rpc = Rpc::<BifConfig>::new(address);251252 Fs::new().register_endpoints(&mut rpc);253 Systemd.register_endpoints(&mut rpc);254 Pty::new().register_endpoints(&mut rpc);255256 let user_prompter = PromptEndpointsClient::wrap(rpc.remote(Address::User));257 let editor_client = EditorEndpointsClient::wrap(rpc.remote(Address::User));258259 let bus = bus::spawn().await?;260 askpass::serve(&bus.conn, user_prompter.clone()).await?;261 editor::serve(&bus.conn, editor_client).await?;262263 let helpers = tempfile::Builder::new().prefix("remowt-path.").tempdir()?;264 let exe = std::env::current_exe()?;265 let askpass_helper = helpers.path().join("remowt-askpass");266 let editor_helper = helpers.path().join("remowt-editor");267 {268 let script = format!(269 "#!/bin/sh\nexec {} ask-pass \"password\" \"$1\"\n",270 sh_quote(&exe.to_string_lossy())271 );272 fs::write(&askpass_helper, script).await?;273 fs::set_permissions(&askpass_helper, Permissions::from_mode(0o755)).await?;274 }275 {276 let script = format!(277 "#!/bin/sh\nexec {} editor \"$1\"\n",278 sh_quote(&exe.to_string_lossy())279 );280 fs::write(&editor_helper, script).await?;281 fs::set_permissions(&editor_helper, Permissions::from_mode(0o755)).await?;282 }283284 // Safety: Hoping tokio own threads won't read any of those...285 unsafe {286 prepend_path(helpers.path());287 std::env::set_var("SUDO_ASKPASS", &askpass_helper);288 std::env::set_var("SSH_ASKPASS", &askpass_helper);289 std::env::set_var("SSH_ASKPASS_REQUIRE", "force");290 std::env::set_var("EDITOR", &editor_helper);291 std::env::set_var("VISUAL", &editor_helper);292 std::env::set_var("DBUS_SESSION_BUS_ADDRESS", &bus.address);293 }294295 let port = match path {296 Some(path) => from_socket(UnixStream::connect(path).await?),297 None => from_stdio(),298 };299 rpc.add_direct(Address::User, port, bifrostlink::Rtt(0));300301 let polkit_conn = if !privileged {302 // The unprivileged agent doubles as a polkit authentication agent so303 // `run0` (e.g. our own elevation) routes its prompt to the User over304 // bifrost instead of failing on a tty-less session.305 let conn = Connection::system().await?;306 let helper = SocketHelper {307 fallback: SuidHelper,308 };309 register_auth_agent(&conn, Agent::new(helper, user_prompter)).await?;310 Some(conn)311 } else {312 None313 };314315 let _keep_alive = (bus, helpers, polkit_conn);316 pending().await317}318319async fn register_auth_agent<H, P>(conn: &Connection, agent: Agent<H, P>) -> anyhow::Result<()>320where321 H: Helper + Clone + Send + Sync + 'static,322 P: Prompter + Clone + Send + Sync + 'static,323{324 let proxy = zbus_polkit::policykit1::AuthorityProxy::new(conn).await?;325 conn.object_server().at(OBJ_PATH, agent).await?;326327 let subject = auth_agent_subject()?;328 proxy329 .register_authentication_agent(&subject, "C", OBJ_PATH)330 .await?;331 info!(kind = subject.subject_kind, "registered polkit agent");332 Ok(())333}334335fn auth_agent_subject() -> anyhow::Result<Subject> {336 let mut details = HashMap::new();337 if let Ok(session_id) = std::env::var("XDG_SESSION_ID") {338 let val: OwnedValue = Str::from(session_id).into();339 details.insert("session-id".to_string(), val);340 return Ok(Subject {341 subject_kind: "unix-session".to_string(),342 subject_details: details,343 });344 }345346 details.insert("pid".to_string(), OwnedValue::from(std::process::id()));347 Ok(Subject {348 subject_kind: "unix-process".to_string(),349 subject_details: details,350 })351}352353fn sh_quote(s: &str) -> String {354 format!("'{}'", s.replace('\'', "'\\''"))355}356357/// Prepend `dir` to the process `PATH`.358///359/// # SAFETY360///361/// Same as `set_var`362unsafe fn prepend_path(dir: &std::path::Path) {363 let value = match std::env::var_os("PATH") {364 Some(existing) => {365 let mut v = dir.as_os_str().to_owned();366 v.push(":");367 v.push(existing);368 v369 }370 None => dir.as_os_str().to_owned(),371 };372 unsafe {373 std::env::set_var("PATH", value);374 }375}