Unify shell opacity and add native authentication and system controls
This commit is contained in:
1 parent
8832a9bf2e
commit
c8e89fb758
35 files changed
+1674
-133
No files matched your search
@@ -0,0 +1,172 @@
|
||||
mod prompt;
|
||||
mod session;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{
|
||||
Arc, Mutex,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
};
|
||||
use zbus::zvariant::{OwnedValue, Value};
|
||||
|
||||
const AGENT_PATH: &str = "/dev/ferese/PolkitAgent";
|
||||
const AUTHORITY: &str = "org.freedesktop.PolicyKit1";
|
||||
const AUTHORITY_PATH: &str = "/org/freedesktop/PolicyKit1/Authority";
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
enum PromptEvent {
|
||||
Start { message: String, user: String },
|
||||
Request { prompt: String, echo: bool },
|
||||
Info { text: String },
|
||||
Error { text: String },
|
||||
Response { value: String },
|
||||
Cancel,
|
||||
}
|
||||
|
||||
type Identity = (String, HashMap<String, OwnedValue>);
|
||||
|
||||
#[derive(Default)]
|
||||
struct Agent {
|
||||
active: Mutex<HashMap<String, Arc<AtomicBool>>>,
|
||||
}
|
||||
|
||||
#[zbus::interface(name = "org.freedesktop.PolicyKit1.AuthenticationAgent")]
|
||||
impl Agent {
|
||||
async fn begin_authentication(
|
||||
&self,
|
||||
_action_id: String,
|
||||
message: String,
|
||||
_icon_name: String,
|
||||
_details: HashMap<String, String>,
|
||||
cookie: String,
|
||||
identities: Vec<Identity>,
|
||||
) -> zbus::fdo::Result<()> {
|
||||
let uid = choose_identity(&identities).ok_or_else(|| {
|
||||
zbus::fdo::Error::Failed("No supported authentication identity".into())
|
||||
})?;
|
||||
let cancelled = Arc::new(AtomicBool::new(false));
|
||||
|
||||
{
|
||||
let mut active = self.active.lock().unwrap();
|
||||
if active.contains_key(&cookie) {
|
||||
return Err(zbus::fdo::Error::Failed("Duplicate request".into()));
|
||||
}
|
||||
active.insert(cookie.clone(), cancelled.clone());
|
||||
}
|
||||
|
||||
let result = tokio::task::spawn_blocking({
|
||||
let cookie = cookie.clone();
|
||||
move || {
|
||||
session::authenticate(uid, message.chars().take(240).collect(), cookie, cancelled)
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(|error| zbus::fdo::Error::Failed(error.to_string()))?;
|
||||
|
||||
self.active.lock().unwrap().remove(&cookie);
|
||||
match result {
|
||||
Ok(true) => Ok(()),
|
||||
Ok(false) => Err(zbus::fdo::Error::Failed("Authentication cancelled".into())),
|
||||
Err(error) => Err(zbus::fdo::Error::Failed(error)),
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel_authentication(&self, cookie: String) {
|
||||
if let Some(cancelled) = self.active.lock().unwrap().get(&cookie) {
|
||||
cancelled.store(true, Ordering::Release);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn choose_identity(identities: &[Identity]) -> Option<u32> {
|
||||
let available: Vec<_> = identities
|
||||
.iter()
|
||||
.filter(|(kind, _)| kind == "unix-user")
|
||||
.filter_map(|(_, details)| {
|
||||
details
|
||||
.get("uid")
|
||||
.and_then(|uid| u32::try_from(uid.clone()).ok())
|
||||
.filter(|uid| *uid <= i32::MAX as u32)
|
||||
})
|
||||
.collect();
|
||||
let current = unsafe { libc::geteuid() };
|
||||
|
||||
available
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|uid| *uid == current)
|
||||
.or_else(|| available.first().copied())
|
||||
}
|
||||
|
||||
fn session_subject() -> Result<(String, HashMap<String, Value<'static>>), String> {
|
||||
let id = std::env::var("XDG_SESSION_ID")
|
||||
.map_err(|_| "XDG_SESSION_ID is required for polkit registration".to_owned())?;
|
||||
|
||||
if id.is_empty() {
|
||||
return Err("XDG_SESSION_ID is empty".into());
|
||||
}
|
||||
|
||||
let mut details = HashMap::new();
|
||||
details.insert("session-id".to_owned(), Value::from(id));
|
||||
|
||||
Ok(("unix-session".into(), details))
|
||||
}
|
||||
|
||||
async fn run_agent() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let subject = session_subject()?;
|
||||
let connection = zbus::connection::Builder::system()?
|
||||
.serve_at(AGENT_PATH, Agent::default())?
|
||||
.build()
|
||||
.await?;
|
||||
let authority = zbus::Proxy::new(&connection, AUTHORITY, AUTHORITY_PATH, AUTHORITY).await?;
|
||||
let locale = std::env::var("LANG").unwrap_or_default();
|
||||
|
||||
authority
|
||||
.call::<_, _, ()>(
|
||||
"RegisterAuthenticationAgent",
|
||||
&(subject, locale, AGENT_PATH),
|
||||
)
|
||||
.await?;
|
||||
std::future::pending::<()>().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
unsafe {
|
||||
libc::prctl(libc::PR_SET_DUMPABLE, 0);
|
||||
}
|
||||
|
||||
match std::env::args().nth(1).as_deref() {
|
||||
None => tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()?
|
||||
.block_on(run_agent()),
|
||||
Some("--prompt") => prompt::run(),
|
||||
Some("--check") => session::check().map_err(Into::into),
|
||||
_ => Err("Usage: ferese-polkit-agent [--check]".into()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn chooses_current_identity_when_available() {
|
||||
let current = unsafe { libc::geteuid() };
|
||||
let identity = |uid: u32| {
|
||||
(
|
||||
"unix-user".to_owned(),
|
||||
HashMap::from([("uid".to_owned(), OwnedValue::from(uid))]),
|
||||
)
|
||||
};
|
||||
assert_eq!(
|
||||
choose_identity(&[identity(0), identity(current)]),
|
||||
Some(current)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,489 @@
|
||||
use crate::PromptEvent;
|
||||
use cosmic::{
|
||||
Element,
|
||||
app::{Core, Settings, Task},
|
||||
iced::{Alignment, Background, Border, Color, Length, Size, Subscription, window},
|
||||
theme,
|
||||
widget::{self, button, column, container, row, text},
|
||||
};
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
io::{BufRead, BufReader, Write},
|
||||
sync::{Arc, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
use zeroize::{Zeroize, Zeroizing};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Appearance {
|
||||
surface: Color,
|
||||
translucent: bool,
|
||||
text: Color,
|
||||
muted: Color,
|
||||
accent: Color,
|
||||
radius: f32,
|
||||
font: cosmic::font::Font,
|
||||
}
|
||||
|
||||
impl Appearance {
|
||||
fn load() -> Self {
|
||||
let document = ferese_config::config_path()
|
||||
.and_then(|path| std::fs::read_to_string(path).ok())
|
||||
.and_then(|source| ferese_config::Document::parse(&source).ok());
|
||||
let setting = |key: &str, fallback: &str| {
|
||||
document
|
||||
.as_ref()
|
||||
.and_then(|doc| doc.get(key))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or(fallback)
|
||||
.to_owned()
|
||||
};
|
||||
let color = |key, fallback| parse_color(&setting(key, fallback), fallback);
|
||||
let family: &'static str =
|
||||
Box::leak(setting("theme.typography.font_family", "Inter").into_boxed_str());
|
||||
let translucent = setting("theme.material.style", "solid") == "translucent";
|
||||
let shell_opacity = document
|
||||
.as_ref()
|
||||
.and_then(|doc| doc.get("theme.material.opacity"))
|
||||
.and_then(|value| value.as_f64())
|
||||
.unwrap_or(ferese_config::DEFAULT_MATERIAL_OPACITY)
|
||||
.clamp(0., 1.) as f32;
|
||||
let mut surface = color("theme.colors.surface_base", "#111821");
|
||||
surface.a = if translucent { shell_opacity } else { 1. };
|
||||
|
||||
Self {
|
||||
surface,
|
||||
translucent,
|
||||
text: color("theme.colors.text_primary", "#F4F7FB"),
|
||||
muted: color("theme.colors.text_muted", "#8793A2"),
|
||||
accent: color("theme.colors.accent", "#3D7BE6"),
|
||||
radius: document
|
||||
.as_ref()
|
||||
.and_then(|doc| doc.get("theme.geometry.shell_radius"))
|
||||
.and_then(|value| value.as_f64())
|
||||
.unwrap_or(14.)
|
||||
.clamp(8., 36.) as f32,
|
||||
font: cosmic::font::Font {
|
||||
family: cosmic::iced::font::Family::Name(family),
|
||||
..cosmic::font::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn theme(&self) -> cosmic::Theme {
|
||||
use cosmic::cosmic_theme::{ThemeBuilder, palette::Srgba};
|
||||
|
||||
let rgba = |color: Color| Srgba::new(color.r, color.g, color.b, color.a);
|
||||
let builder = if self.surface.r + self.surface.g + self.surface.b > 1.5 {
|
||||
ThemeBuilder::light()
|
||||
} else {
|
||||
ThemeBuilder::dark()
|
||||
};
|
||||
|
||||
let mut corners = cosmic::cosmic_theme::CornerRadii::default();
|
||||
corners.radius_xs = [self.radius.min(4.); 4];
|
||||
corners.radius_s = [self.radius.min(8.); 4];
|
||||
corners.radius_m = [self.radius; 4];
|
||||
corners.radius_l = [self.radius; 4];
|
||||
corners.radius_xl = [self.radius; 4];
|
||||
|
||||
cosmic::Theme::custom(Arc::new(
|
||||
builder
|
||||
.corner_radii(corners)
|
||||
.bg_color(rgba(Color {
|
||||
a: if self.translucent { 0. } else { 1. },
|
||||
..self.surface
|
||||
}))
|
||||
.primary_container_bg(rgba(self.surface))
|
||||
.text_tint(rgba(self.text).color)
|
||||
.accent(rgba(self.accent).color)
|
||||
.build(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_color(input: &str, fallback: &str) -> Color {
|
||||
let parse = |source: &str| {
|
||||
let value = u32::from_str_radix(source.strip_prefix('#')?, 16).ok()?;
|
||||
(source.len() == 7)
|
||||
.then(|| Color::from_rgb8((value >> 16) as u8, (value >> 8) as u8, value as u8))
|
||||
};
|
||||
parse(input)
|
||||
.or_else(|| parse(fallback))
|
||||
.unwrap_or(Color::BLACK)
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Message {
|
||||
Tick,
|
||||
Input(String),
|
||||
Submit,
|
||||
Cancel,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Message {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("AuthenticationPromptMessage")
|
||||
}
|
||||
}
|
||||
|
||||
struct Prompt {
|
||||
core: Core,
|
||||
appearance: Appearance,
|
||||
incoming: Arc<Mutex<VecDeque<PromptEvent>>>,
|
||||
description: String,
|
||||
user: String,
|
||||
question: String,
|
||||
echo: bool,
|
||||
answer: Zeroizing<String>,
|
||||
waiting: bool,
|
||||
error: Option<String>,
|
||||
info: Option<String>,
|
||||
height: f32,
|
||||
}
|
||||
|
||||
fn content_height(description: &str, status: Option<&str>) -> f32 {
|
||||
let lines: usize = description
|
||||
.lines()
|
||||
.map(|line| line.chars().count().div_ceil(48).max(1))
|
||||
.sum();
|
||||
let status_height = status.map_or(0, |message| {
|
||||
14 + 18 * message.chars().count().div_ceil(48).clamp(1, 3)
|
||||
});
|
||||
(214 + 18 * lines.saturating_sub(1) + status_height).min(360) as f32
|
||||
}
|
||||
|
||||
fn input_style(palette: &Appearance) -> theme::TextInput {
|
||||
let text = palette.text;
|
||||
let muted = palette.muted;
|
||||
let accent = palette.accent;
|
||||
let radius = palette.radius.min(10.);
|
||||
let appearance = move |focused: bool| widget::text_input::Appearance {
|
||||
background: Color::from_rgba(text.r, text.g, text.b, 0.045).into(),
|
||||
border_radius: radius.into(),
|
||||
border_width: 1.,
|
||||
border_offset: None,
|
||||
border_color: if focused {
|
||||
accent.scale_alpha(0.82)
|
||||
} else {
|
||||
muted.scale_alpha(0.26)
|
||||
},
|
||||
icon_color: Some(muted),
|
||||
text_color: Some(text),
|
||||
placeholder_color: muted,
|
||||
selected_text_color: text,
|
||||
selected_fill: accent.scale_alpha(0.35),
|
||||
label_color: text,
|
||||
};
|
||||
theme::TextInput::Custom {
|
||||
active: Box::new(move |_| appearance(false)),
|
||||
hovered: Box::new(move |_| appearance(true)),
|
||||
focused: Box::new(move |_| appearance(true)),
|
||||
error: Box::new(move |_| appearance(false)),
|
||||
disabled: Box::new(move |_| appearance(false)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let stdin = std::io::stdin();
|
||||
let mut lines = BufReader::new(stdin).lines();
|
||||
let Some(Ok(first)) = lines.next() else {
|
||||
return Err("Missing authentication request".into());
|
||||
};
|
||||
let PromptEvent::Start { message, user } = serde_json::from_str(&first)? else {
|
||||
return Err("Invalid authentication request".into());
|
||||
};
|
||||
let incoming = Arc::new(Mutex::new(VecDeque::new()));
|
||||
let queue = incoming.clone();
|
||||
std::thread::spawn(move || {
|
||||
for line in lines {
|
||||
let Ok(line) = line else { break };
|
||||
if let Ok(event) = serde_json::from_str(&line) {
|
||||
queue.lock().unwrap().push_back(event);
|
||||
}
|
||||
}
|
||||
queue.lock().unwrap().push_back(PromptEvent::Cancel);
|
||||
});
|
||||
let appearance = Appearance::load();
|
||||
let height = content_height(&message, None);
|
||||
cosmic::app::run::<Prompt>(
|
||||
Settings::default()
|
||||
.size(Size::new(434., height))
|
||||
.client_decorations(false)
|
||||
.transparent(true)
|
||||
.theme(appearance.theme())
|
||||
.default_font(appearance.font)
|
||||
.default_text_size(14.)
|
||||
.antialiasing(true),
|
||||
(incoming, message, user, appearance),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reply(mut event: PromptEvent) {
|
||||
if let Ok(mut line) = serde_json::to_string(&event) {
|
||||
let mut stdout = std::io::stdout().lock();
|
||||
let _ = writeln!(stdout, "{line}");
|
||||
let _ = stdout.flush();
|
||||
line.zeroize();
|
||||
}
|
||||
if let PromptEvent::Response { value } = &mut event {
|
||||
value.zeroize();
|
||||
}
|
||||
}
|
||||
|
||||
fn focus() -> Task<Message> {
|
||||
widget::text_input::focus(widget::Id::new("auth-response"))
|
||||
}
|
||||
|
||||
impl Prompt {
|
||||
fn resize_to_content(&mut self) -> Task<Message> {
|
||||
let height = content_height(
|
||||
&self.description,
|
||||
self.error.as_deref().or(self.info.as_deref()),
|
||||
);
|
||||
if (height - self.height).abs() < 1.0 {
|
||||
return Task::none();
|
||||
}
|
||||
self.height = height;
|
||||
self.core
|
||||
.main_window_id()
|
||||
.map(|id| window::resize(id, Size::new(434., height)))
|
||||
.unwrap_or_else(Task::none)
|
||||
}
|
||||
}
|
||||
|
||||
impl cosmic::Application for Prompt {
|
||||
type Executor = cosmic::executor::Default;
|
||||
type Flags = (
|
||||
Arc<Mutex<VecDeque<PromptEvent>>>,
|
||||
String,
|
||||
String,
|
||||
Appearance,
|
||||
);
|
||||
type Message = Message;
|
||||
const APP_ID: &'static str = "dev.ferese.Authentication";
|
||||
|
||||
fn core(&self) -> &Core {
|
||||
&self.core
|
||||
}
|
||||
|
||||
fn core_mut(&mut self) -> &mut Core {
|
||||
&mut self.core
|
||||
}
|
||||
|
||||
fn init(
|
||||
mut core: Core,
|
||||
(incoming, description, user, appearance): Self::Flags,
|
||||
) -> (Self, Task<Message>) {
|
||||
core.window.show_headerbar = false;
|
||||
let height = content_height(&description, None);
|
||||
let bounds = core
|
||||
.main_window_id()
|
||||
.map(|id| window::set_max_size(id, Some(Size::new(434., 360.))))
|
||||
.unwrap_or_else(Task::none);
|
||||
(
|
||||
Self {
|
||||
core,
|
||||
appearance,
|
||||
incoming,
|
||||
description,
|
||||
user,
|
||||
question: "Waiting for authentication…".into(),
|
||||
echo: false,
|
||||
answer: Zeroizing::new(String::new()),
|
||||
waiting: true,
|
||||
error: None,
|
||||
info: None,
|
||||
height,
|
||||
},
|
||||
Task::batch([focus(), bounds]),
|
||||
)
|
||||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Message> {
|
||||
cosmic::iced::time::every(Duration::from_millis(30)).map(|_| Message::Tick)
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) -> Task<Message> {
|
||||
match message {
|
||||
Message::Tick => {
|
||||
let events: Vec<_> = self.incoming.lock().unwrap().drain(..).collect();
|
||||
let mut refocus = false;
|
||||
for event in events {
|
||||
match event {
|
||||
PromptEvent::Request { prompt, echo } => {
|
||||
self.answer.zeroize();
|
||||
self.question = prompt;
|
||||
self.echo = echo;
|
||||
self.waiting = false;
|
||||
refocus = true;
|
||||
}
|
||||
PromptEvent::Info { text } => self.info = Some(text),
|
||||
PromptEvent::Error { text } => self.error = Some(text),
|
||||
PromptEvent::Cancel => return cosmic::iced::exit(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let resize = self.resize_to_content();
|
||||
if refocus {
|
||||
return Task::batch([resize, focus()]);
|
||||
}
|
||||
return resize;
|
||||
}
|
||||
Message::Input(mut value) => {
|
||||
if !self.waiting && value.len() <= 4096 {
|
||||
self.answer.zeroize();
|
||||
*self.answer = std::mem::take(&mut value);
|
||||
self.error = None;
|
||||
}
|
||||
value.zeroize();
|
||||
}
|
||||
Message::Submit if !self.waiting && !self.answer.is_empty() => {
|
||||
let value = std::mem::take(&mut *self.answer);
|
||||
reply(PromptEvent::Response { value });
|
||||
self.waiting = true;
|
||||
}
|
||||
Message::Cancel => {
|
||||
self.answer.zeroize();
|
||||
reply(PromptEvent::Cancel);
|
||||
return cosmic::iced::exit();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
fn on_escape(&mut self) -> Task<Message> {
|
||||
self.update(Message::Cancel)
|
||||
}
|
||||
|
||||
fn view(&self) -> Element<'_, Message> {
|
||||
let palette = &self.appearance;
|
||||
let mut input = widget::text_input(&self.question, self.answer.as_str())
|
||||
.id(widget::Id::new("auth-response"))
|
||||
.font(palette.font)
|
||||
.padding([7, 12])
|
||||
.style(input_style(palette));
|
||||
if !self.echo {
|
||||
input = input.password();
|
||||
}
|
||||
if !self.waiting {
|
||||
input = input.on_input(Message::Input).on_submit(|mut value| {
|
||||
value.zeroize();
|
||||
Message::Submit
|
||||
});
|
||||
}
|
||||
let action = button::custom(text("Authenticate").font(palette.font).size(13))
|
||||
.class(theme::Button::Suggested)
|
||||
.height(Length::Fixed(36.))
|
||||
.padding([8, 16])
|
||||
.on_press_maybe((!self.waiting && !self.answer.is_empty()).then_some(Message::Submit));
|
||||
let lock_svg = std::str::from_utf8(include_bytes!(
|
||||
"../../ferese-shell/assets/icons/status/lock.svg"
|
||||
))
|
||||
.expect("embedded lock icon is valid UTF-8")
|
||||
.replace(
|
||||
"currentColor",
|
||||
&format!(
|
||||
"#{:02x}{:02x}{:02x}",
|
||||
(palette.accent.r * 255.) as u8,
|
||||
(palette.accent.g * 255.) as u8,
|
||||
(palette.accent.b * 255.) as u8,
|
||||
),
|
||||
);
|
||||
let lock_icon = widget::icon::from_svg_bytes(lock_svg.into_bytes())
|
||||
.symbolic(false)
|
||||
.icon()
|
||||
.size(20);
|
||||
let heading = row![
|
||||
container(lock_icon)
|
||||
.width(40)
|
||||
.height(40)
|
||||
.center_x(40)
|
||||
.center_y(40)
|
||||
.class(theme::Container::custom(move |_| {
|
||||
widget::container::Style {
|
||||
background: Some(Background::Color(palette.accent.scale_alpha(0.13))),
|
||||
border: Border {
|
||||
radius: 12.0.into(),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
})),
|
||||
column![
|
||||
text("Authentication required").font(palette.font).size(18),
|
||||
text(format!("Confirm as {}", self.user))
|
||||
.font(palette.font)
|
||||
.size(12)
|
||||
.class(theme::Text::Color(palette.muted)),
|
||||
]
|
||||
.spacing(2),
|
||||
]
|
||||
.spacing(12)
|
||||
.align_y(Alignment::Center);
|
||||
let mut body = column![
|
||||
heading,
|
||||
text(&self.description).font(palette.font).size(13),
|
||||
input
|
||||
]
|
||||
.spacing(14)
|
||||
.width(Length::Fill);
|
||||
if let Some(error) = &self.error {
|
||||
body = body.push(
|
||||
text(error)
|
||||
.font(palette.font)
|
||||
.size(12)
|
||||
.class(theme::Text::Color(Color::from_rgb8(235, 98, 98))),
|
||||
);
|
||||
} else if let Some(info) = &self.info {
|
||||
body = body.push(
|
||||
text(info)
|
||||
.font(palette.font)
|
||||
.size(12)
|
||||
.class(theme::Text::Color(palette.muted)),
|
||||
);
|
||||
}
|
||||
body = body.push(
|
||||
row![
|
||||
button::custom(text("Cancel").font(palette.font).size(13))
|
||||
.class(theme::Button::Text)
|
||||
.padding([8, 14])
|
||||
.on_press(Message::Cancel),
|
||||
action
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(Alignment::Center),
|
||||
);
|
||||
container(body)
|
||||
.padding(20)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.class(theme::Container::custom(move |_| {
|
||||
widget::container::Style {
|
||||
background: Some(Background::Color(palette.surface)),
|
||||
text_color: Some(palette.text),
|
||||
border: Border {
|
||||
radius: palette.radius.into(),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}))
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::content_height;
|
||||
|
||||
#[test]
|
||||
fn dialog_grows_for_long_messages_and_caps_its_height() {
|
||||
assert_eq!(content_height("Short request", None), 214.);
|
||||
assert!(content_height(&"Long message ".repeat(20), None) > 214.);
|
||||
assert_eq!(content_height(&"Long message ".repeat(100), None), 360.);
|
||||
assert!(content_height("Short request", Some("Password not accepted")) > 214.);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
use crate::PromptEvent;
|
||||
use glib::gobject_ffi;
|
||||
use libloading::Library;
|
||||
use std::{
|
||||
ffi::{CStr, CString, c_char, c_int, c_void},
|
||||
io::{BufRead, BufReader, BufWriter, Write},
|
||||
process::{Child, ChildStdin, Command, Stdio},
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
mpsc::{self, Sender},
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
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<Self, String> {
|
||||
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<ChildStdin>, 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<ChildStdin>, mpsc::Receiver<PromptEvent>), 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::null())
|
||||
.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<SessionEvent>) };
|
||||
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<SessionEvent>) };
|
||||
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<SessionEvent>) };
|
||||
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<SessionEvent>) };
|
||||
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<ChildStdin>,
|
||||
input: &mpsc::Receiver<PromptEvent>,
|
||||
cancelled: &AtomicBool,
|
||||
child: &mut Child,
|
||||
) -> Result<Option<bool>, 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(
|
||||
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(
|
||||
info as unsafe extern "C" fn(Object, *const c_char, *mut c_void),
|
||||
)),
|
||||
signal_data,
|
||||
);
|
||||
connect(
|
||||
session,
|
||||
c"show-error",
|
||||
Some(std::mem::transmute(
|
||||
error as unsafe extern "C" fn(Object, *const c_char, *mut c_void),
|
||||
)),
|
||||
signal_data,
|
||||
);
|
||||
connect(
|
||||
session,
|
||||
c"completed",
|
||||
Some(std::mem::transmute(
|
||||
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<SessionEvent>));
|
||||
}
|
||||
Ok(outcome.filter(|_| !stopped))
|
||||
}
|
||||
|
||||
pub fn authenticate(
|
||||
uid: u32,
|
||||
message: String,
|
||||
cookie: String,
|
||||
cancelled: Arc<AtomicBool>,
|
||||
) -> Result<bool, String> {
|
||||
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())?
|
||||
}
|
||||
Reference in new issue
Block a user