use std::ffi::{CStr, CString, c_char, c_int, c_void}; use std::io::{BufRead, BufReader, BufWriter, Write}; use std::process::{Child, ChildStdin, Command, Stdio}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc::Sender; use std::sync::{Arc, mpsc}; use std::time::Duration; use glib::gobject_ffi; use libloading::Library; use zeroize::Zeroize; use crate::PromptEvent; enum SessionEvent { Request(String, bool), Info(String), Error(String), Completed(bool), } type Object = *mut c_void; struct Api { _agent: Library, _polkit: Library, user_new: unsafe extern "C" fn(c_int) -> Object, session_new: unsafe extern "C" fn(Object, *const c_char) -> Object, initiate: unsafe extern "C" fn(Object), response: unsafe extern "C" fn(Object, *const c_char), cancel: unsafe extern "C" fn(Object), } impl Api { fn load() -> Result { unsafe { let agent = Library::new("libpolkit-agent-1.so.0").map_err(|e| e.to_string())?; let polkit = Library::new("libpolkit-gobject-1.so.0").map_err(|e| e.to_string())?; let user_new = *polkit.get(b"polkit_unix_user_new\0").map_err(|e| e.to_string())?; let session_new = *agent.get(b"polkit_agent_session_new\0").map_err(|e| e.to_string())?; let initiate = *agent .get(b"polkit_agent_session_initiate\0") .map_err(|e| e.to_string())?; let response = *agent .get(b"polkit_agent_session_response\0") .map_err(|e| e.to_string())?; let cancel = *agent.get(b"polkit_agent_session_cancel\0").map_err(|e| e.to_string())?; Ok(Self { _agent: agent, _polkit: polkit, user_new, session_new, initiate, response, cancel, }) } } } pub fn check() -> Result<(), String> { Api::load().map(|_| ()) } fn username(uid: u32) -> String { let output = Command::new("getent").args(["passwd", &uid.to_string()]).output(); output .ok() .filter(|output| output.status.success()) .and_then(|output| String::from_utf8(output.stdout).ok()) .and_then(|entry| entry.split(':').next().map(str::to_owned)) .filter(|name| !name.is_empty()) .unwrap_or_else(|| uid.to_string()) } fn send(writer: &mut BufWriter, event: &PromptEvent) -> Result<(), String> { serde_json::to_writer(&mut *writer, event).map_err(|e| e.to_string())?; writer.write_all(b"\n").map_err(|e| e.to_string())?; writer.flush().map_err(|e| e.to_string()) } fn prompt_process( message: String, user: String, ) -> Result<(Child, BufWriter, mpsc::Receiver), String> { let mut child = Command::new(std::env::current_exe().map_err(|e| e.to_string())?) .arg("--prompt") .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::inherit()) .spawn() .map_err(|e| e.to_string())?; let stdin = child.stdin.take().ok_or("Prompt has no input")?; let stdout = child.stdout.take().ok_or("Prompt has no output")?; let mut writer = BufWriter::new(stdin); send(&mut writer, &PromptEvent::Start { message, user })?; let (tx, rx) = mpsc::channel(); std::thread::spawn(move || { for line in BufReader::new(stdout).lines() { let Ok(mut line) = line else { break }; if let Ok(event) = serde_json::from_str(&line) { line.zeroize(); if tx.send(event).is_err() { break; } } else { line.zeroize(); } } }); Ok((child, writer, rx)) } unsafe extern "C" fn request(_session: Object, prompt: *const c_char, echo: c_int, data: *mut c_void) { let tx = unsafe { &*(data as *const Sender) }; let prompt = unsafe { CStr::from_ptr(prompt) }.to_string_lossy().into_owned(); let _ = tx.send(SessionEvent::Request(prompt, echo != 0)); } unsafe extern "C" fn info(_session: Object, text: *const c_char, data: *mut c_void) { let tx = unsafe { &*(data as *const Sender) }; let text = unsafe { CStr::from_ptr(text) }.to_string_lossy().into_owned(); let _ = tx.send(SessionEvent::Info(text)); } unsafe extern "C" fn error(_session: Object, text: *const c_char, data: *mut c_void) { let tx = unsafe { &*(data as *const Sender) }; let text = unsafe { CStr::from_ptr(text) }.to_string_lossy().into_owned(); let _ = tx.send(SessionEvent::Error(text)); } unsafe extern "C" fn completed(_session: Object, success: c_int, data: *mut c_void) { let tx = unsafe { &*(data as *const Sender) }; let _ = tx.send(SessionEvent::Completed(success != 0)); } unsafe fn connect(session: Object, name: &'static CStr, callback: gobject_ffi::GCallback, data: *mut c_void) { unsafe { gobject_ffi::g_signal_connect_data( session.cast(), name.as_ptr(), callback, data, None, gobject_ffi::G_CONNECT_DEFAULT, ); } } fn run_session( api: &Api, uid: u32, cookie: &CString, context: &glib::MainContext, writer: &mut BufWriter, input: &mpsc::Receiver, cancelled: &AtomicBool, child: &mut Child, ) -> Result, String> { let identity = unsafe { (api.user_new)(uid as c_int) }; if identity.is_null() { return Err("Could not create polkit identity".into()); } let session = unsafe { (api.session_new)(identity, cookie.as_ptr()) }; unsafe { gobject_ffi::g_object_unref(identity.cast()) }; if session.is_null() { return Err("Could not create polkit session".into()); } let (tx, rx) = mpsc::channel(); let signal_data = Box::into_raw(Box::new(tx)).cast(); unsafe { connect( session, c"request", Some(std::mem::transmute::< unsafe extern "C" fn(Object, *const c_char, c_int, *mut c_void), unsafe extern "C" fn(), >( request as unsafe extern "C" fn(Object, *const c_char, c_int, *mut c_void), )), signal_data, ); connect( session, c"show-info", Some(std::mem::transmute::< unsafe extern "C" fn(Object, *const c_char, *mut c_void), unsafe extern "C" fn(), >( info as unsafe extern "C" fn(Object, *const c_char, *mut c_void) )), signal_data, ); connect( session, c"show-error", Some(std::mem::transmute::< unsafe extern "C" fn(Object, *const c_char, *mut c_void), unsafe extern "C" fn(), >( error as unsafe extern "C" fn(Object, *const c_char, *mut c_void) )), signal_data, ); connect( session, c"completed", Some(std::mem::transmute::< unsafe extern "C" fn(Object, c_int, *mut c_void), unsafe extern "C" fn(), >( completed as unsafe extern "C" fn(Object, c_int, *mut c_void) )), signal_data, ); (api.initiate)(session); } let mut outcome = None; let mut stopped = false; while outcome.is_none() { while context.pending() { context.iteration(false); } while let Ok(event) = rx.try_recv() { match event { SessionEvent::Request(prompt, echo) => { send(writer, &PromptEvent::Request { prompt, echo })?; } SessionEvent::Info(text) => send(writer, &PromptEvent::Info { text })?, SessionEvent::Error(text) => send(writer, &PromptEvent::Error { text })?, SessionEvent::Completed(success) => outcome = Some(success), } } if outcome.is_some() { break; } while let Ok(event) = input.try_recv() { match event { PromptEvent::Response { mut value } => { if let Ok(response) = CString::new(value.as_str()) { unsafe { (api.response)(session, response.as_ptr()) }; } value.zeroize(); } PromptEvent::Cancel => cancelled.store(true, Ordering::Release), _ => {} } } if !stopped && (cancelled.load(Ordering::Acquire) || child.try_wait().map_err(|e| e.to_string())?.is_some()) { unsafe { (api.cancel)(session) }; stopped = true; } std::thread::sleep(Duration::from_millis(10)); } unsafe { gobject_ffi::g_object_unref(session.cast()); drop(Box::from_raw(signal_data as *mut Sender)); } Ok(outcome.filter(|_| !stopped)) } pub fn authenticate(uid: u32, message: String, cookie: String, cancelled: Arc) -> Result { let api = Api::load()?; let cookie = CString::new(cookie).map_err(|_| "Invalid authentication cookie")?; let context = glib::MainContext::new(); context .with_thread_default(|| { let (mut child, mut writer, input) = prompt_process(message, username(uid))?; let result = loop { match run_session( &api, uid, &cookie, &context, &mut writer, &input, &cancelled, &mut child, ) { Ok(Some(true)) => break Ok(true), Ok(Some(false)) if !cancelled.load(Ordering::Acquire) => { if let Err(error) = send( &mut writer, &PromptEvent::Error { text: "Password not accepted. Try again.".into(), }, ) { break Err(error); } } Ok(_) => break Ok(false), Err(error) => break Err(error), } }; let _ = child.kill(); let _ = child.wait(); result }) .map_err(|error| error.to_string())? }