use chrono::prelude::*; use gdkx11::X11Window; use gethostname::gethostname; use glib::{ControlFlow, GString, clone}; use gtk::{prelude::*, Button, Entry, Label, Plug, Window}; use log::{debug, error, warn}; use std::{io::{self, Write}, sync::mpsc, thread, time::Duration, ffi::CString}; use bscreensaver_util::{init_logging, dialog::DialogExitStatus, settings::Configuration, desktop::NewLoginCommand}; const PAM_SERVICE_NAME: &str = "xscreensaver"; const DIALOG_UPDATE_INTERVAL: Duration = Duration::from_millis(100); const DIALOG_TIMEOUT: Duration = Duration::from_secs(60); #[derive(Debug)] enum GtkConverseOp { ShowInfo(String), ShowError(String), ShowPrompt(String, mpsc::Sender), ShowBlindPrompt(String, mpsc::Sender), } struct Authenticator { username: String, sender: glib::Sender, } impl Authenticator { pub fn new>(username: S, op_sender: glib::Sender) -> Self { Self { username: username.as_ref().to_string(), sender: op_sender, } } pub fn authenticate(mut self) -> anyhow::Result<()> { let username = std::mem::take(&mut self.username); let mut context = pam_client::Context::new(PAM_SERVICE_NAME, Some(username.as_str()), self)?; context.authenticate(pam_client::Flag::NONE)?; context.acct_mgmt(pam_client::Flag::NONE)?; let _ = context.open_session(pam_client::Flag::NONE)?; Ok(()) } } impl pam_client::ConversationHandler for Authenticator { fn text_info(&mut self, msg: &std::ffi::CStr) { let msg = msg.to_str().unwrap_or("(OS sent us a non-UTF-8 message)").trim(); let _ = self.sender.send(GtkConverseOp::ShowInfo(msg.to_string())); thread::sleep(Duration::from_secs(2)); } fn error_msg(&mut self, msg: &std::ffi::CStr) { let msg = msg.to_str().unwrap_or("(OS sent us a non-UTF-8 message)").trim(); let _ = self.sender.send(GtkConverseOp::ShowError(msg.to_string())); thread::sleep(Duration::from_secs(2)); } fn prompt_echo_on(&mut self, msg: &std::ffi::CStr) -> Result { let msg = msg.to_str().unwrap_or("Password:").trim(); let (tx, rx) = mpsc::channel(); self.sender.send(GtkConverseOp::ShowPrompt(msg.to_string(), tx)).map_err(|_| pam_client::ErrorCode::ABORT)?; let result = rx.recv().map_err(|_| pam_client::ErrorCode::ABORT)?; CString::new(result).map_err(|_| pam_client::ErrorCode::CRED_ERR) } fn prompt_echo_off(&mut self, msg: &std::ffi::CStr) -> Result { let msg = msg.to_str().unwrap_or("Password:").trim(); let (tx, rx) = mpsc::channel(); self.sender.send(GtkConverseOp::ShowBlindPrompt(msg.to_string(), tx)).map_err(|_| pam_client::ErrorCode::ABORT)?; let result = rx.recv().map_err(|_| pam_client::ErrorCode::ABORT)?; CString::new(result).map_err(|_| pam_client::ErrorCode::CRED_ERR) } } fn main() -> anyhow::Result<()> { init_logging("BSCREENSAVER_DIALOG_GTK3_LOG"); glib::log_set_default_handler(glib::rust_log_handler); // Can't use the rust version as it requires gtk_init() to be // called first, but the underlying C function requires that // it hasn't. let backends = CString::new("x11").unwrap(); unsafe { gtk::gdk::ffi::gdk_set_allowed_backends(backends.as_ptr()); }; let config = Configuration::load()?; let new_login_command = if config.new_login_command == NewLoginCommand::Disabled { None } else { Some(config.new_login_command.clone()) }; let standalone = std::env::var("BSCREENSAVER_DIALOG_STANDALONE").is_ok(); gtk::init()?; let top_sg = gtk::SizeGroup::builder() .mode(gtk::SizeGroupMode::Horizontal) .build(); let label_sg = gtk::SizeGroup::builder() .mode(gtk::SizeGroupMode::Horizontal) .build(); let entry_sg = gtk::SizeGroup::builder() .mode(gtk::SizeGroupMode::Horizontal) .build(); let header = gtk::HeaderBar::builder() .title("Unlock Screen") .show_close_button(true) .build(); let top_vbox = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .spacing(0) .build(); let dialog = if standalone { let win = Window::builder() .type_(gtk::WindowType::Toplevel) .title("Unlock Screen") .modal(true) .build(); win.set_titlebar(Some(&header)); win.upcast::() } else { let plug = Plug::builder() .type_(gtk::WindowType::Toplevel) .title("Unlock Screen") .modal(true) .build(); plug.connect_embedded(|_| debug!("DIALOG EMBEDDED")); plug.connect_embedded_notify(|_| debug!("DIALOG EMBEDDED (notify)")); plug.connect_realize(|plug| { let plug_window = match plug.window().unwrap().downcast::() { Err(err) => { error!("Failed to find XID of unlock dialog window: {}", err); DialogExitStatus::OtherError.exit(); }, Ok(w) => w, }; let xid = plug_window.xid() as u32; let xid_buf: [u8; 4] = [ ((xid >> 24) & 0xff) as u8, ((xid >> 16) & 0xff) as u8, ((xid >> 8) & 0xff) as u8, (xid & 0xff) as u8, ]; let out = io::stdout(); let mut out_locked = out.lock(); if let Err(err) = out_locked.write_all(&xid_buf).and_then(|_| out_locked.flush()) { error!("Failed to write XID to stdout: {}", err); DialogExitStatus::OtherError.exit(); }; }); // Walking the header's widget tree, finding the close button, and connecting // to the 'clicked' signal strangely doesn't work either, so let's just // disable it for now. header.set_show_close_button(false); top_vbox.pack_start(&header, true, false, 0); plug.upcast::() }; // I don't know why, but this doesn't work when we're a GktPlug, despite // an examination of the gtk source suggesting that the header should send // a delete-event to the toplevel (which should be the plug) when the close // button is clicked. For some reason, though, we never get the delete-event. dialog.connect_delete_event(|_, _| DialogExitStatus::AuthCanceled.exit()); dialog.connect_realize(|_| debug!("DIALOG REALIZED")); dialog.connect_map(|_| debug!("DIALOG MAPPED")); dialog.connect_unmap(|_| debug!("DIALOG UNMAPPED")); dialog.connect_unrealize(|_| debug!("DIALOG_UNREALIZED")); dialog.add(&top_vbox); let top_hbox = gtk::Box::builder() .orientation(gtk::Orientation::Horizontal) .border_width(48) .spacing(8) .build(); top_vbox.pack_start(&top_hbox, true, true, 0); let vbox = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .spacing(4) .build(); top_sg.add_widget(&vbox); top_hbox.pack_start(&vbox, true, true, 0); let attrs = gtk::pango::AttrList::new(); attrs.insert(gtk::pango::AttrFloat::new_scale(gtk::pango::SCALE_XX_LARGE)); let mut bold_desc = gtk::pango::FontDescription::new(); bold_desc.set_weight(gtk::pango::Weight::Bold); attrs.insert(gtk::pango::AttrFontDesc::new(&bold_desc)); let label = gtk::Label::builder() .label(gethostname().to_str().unwrap_or("(unknown hostname)")) .xalign(0.5) .yalign(0.5) .attributes(&attrs) .build(); vbox.pack_start(&label, false, false, 0); let attrs = gtk::pango::AttrList::new(); attrs.insert(gtk::pango::AttrFloat::new_scale(gtk::pango::SCALE_LARGE)); let label = gtk::Label::builder() .xalign(0.5) .yalign(0.5) .attributes(&attrs) .build(); set_time_label(&label); vbox.pack_start(&label, false, false, 0); glib::timeout_add_seconds_local(1, move || { set_time_label(&label); ControlFlow::Continue }); let sep = gtk::Separator::builder() .orientation(gtk::Orientation::Vertical) .build(); top_hbox.pack_start(&sep, true, false, 0); let vbox = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .spacing(0) .margin(0) .build(); top_sg.add_widget(&vbox); top_hbox.pack_start(&vbox, true, true, 0); let auth_vbox = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .spacing(4) .build(); vbox.pack_start(&auth_vbox, true, true, 2); let button_hbox = gtk::Box::builder() .orientation(gtk::Orientation::Horizontal) .spacing(8) .build(); vbox.pack_end(&button_hbox, false, false, 4); if let Some(new_login_command) = new_login_command { let button = Button::builder() .label("New Login") .build(); button.connect_clicked(clone!(@strong new_login_command => move |_| { thread::spawn(clone!(@strong new_login_command => move || { if let Err(err) = new_login_command.run() { warn!("Failed to run new login command: {}", err); } else { DialogExitStatus::AuthCanceled.exit(); } })); })); button_hbox.pack_start(&button, false, true, 8); } let accept_button_box = gtk::Box::builder() .orientation(gtk::Orientation::Horizontal) .spacing(0) .margin(0) .build(); button_hbox.pack_end(&accept_button_box, false, true, 8); let timer = gtk::ProgressBar::builder() .orientation(gtk::Orientation::Horizontal) .fraction(1.0) .show_text(false) .can_focus(false) .margin(2) .build(); top_vbox.pack_end(&timer, false, false, 0); let delta = (DIALOG_UPDATE_INTERVAL.as_millis() as f64) / (DIALOG_TIMEOUT.as_millis() as f64); glib::timeout_add_local(DIALOG_UPDATE_INTERVAL, clone!(@weak timer => @default-return ControlFlow::Break, move || { let new_fraction = timer.fraction() - delta; if new_fraction <= 0.0 { DialogExitStatus::AuthTimedOut.exit(); } timer.set_fraction(new_fraction); ControlFlow::Continue })); let (tx, rx) = glib::MainContext::channel(glib::Priority::default()); let username = bscreensaver_util::get_username()?; rx.attach(None, clone!(@weak dialog, @weak auth_vbox, @weak accept_button_box, @weak timer, @strong username, => @default-return ControlFlow::Break, move |op| { for child in auth_vbox.children() { auth_vbox.remove(&child); } for child in accept_button_box.children() { accept_button_box.remove(&child); } debug!("PAM saysL {:?}", op); let input_purpose = match op { GtkConverseOp::ShowBlindPrompt(_, _) => gtk::InputPurpose::Password, _ => gtk::InputPurpose::FreeForm, }; match op { GtkConverseOp::ShowInfo(msg) | GtkConverseOp::ShowError(msg) => { let attrs = gtk::pango::AttrList::new(); attrs.insert(gtk::pango::AttrFloat::new_scale(gtk::pango::SCALE_LARGE)); let label = gtk::Label::builder() .label(&msg) .xalign(0.0) .width_chars(40) .wrap(true) .wrap_mode(gtk::pango::WrapMode::Word) .attributes(&attrs) .build(); auth_vbox.pack_start(&label, false, false, 0); }, GtkConverseOp::ShowPrompt(msg, result_sender) | GtkConverseOp::ShowBlindPrompt(msg, result_sender) => { let hbox = gtk::Box::builder() .orientation(gtk::Orientation::Horizontal) .spacing(8) .build(); auth_vbox.pack_start(&hbox, true, true, 2); let label = Label::builder() .label("Username:") .xalign(0.0) .build(); label_sg.add_widget(&label); hbox.pack_start(&label, false, true, 8); let username_box = Entry::builder() .text(&username) .sensitive(false) .build(); entry_sg.add_widget(&username_box); hbox.pack_start(&username_box, true, true, 8); let hbox = gtk::Box::builder() .orientation(gtk::Orientation::Horizontal) .spacing(8) .build(); auth_vbox.pack_start(&hbox, true, true, 0); let label = Label::builder() .label(&msg) .xalign(0.0) .build(); label_sg.add_widget(&label); hbox.pack_start(&label, false, true, 8); let input_box = Entry::builder() .visibility(false) .input_purpose(input_purpose) .activates_default(true) .width_chars(25) .build(); entry_sg.add_widget(&input_box); hbox.pack_start(&input_box, true, true, 8); input_box.connect_key_press_event(clone!(@strong timer => move |_, ev| { let new_fraction = timer.fraction() + 0.05; timer.set_fraction(if new_fraction >= 1.0 { 1.0 } else { new_fraction }); if ev.keyval().name() == Some(GString::from("Escape")) { DialogExitStatus::AuthCanceled.exit(); } glib::Propagation::Stop })); input_box.grab_focus(); let accept_button = Button::builder() .label("Unlock") .build(); accept_button.connect_clicked(clone!(@strong input_box => move |accept_button| { accept_button.set_sensitive(false); input_box.set_sensitive(false); if let Err(err) = result_sender.send(input_box.text().to_string()) { warn!("Failed to send result to main thread: {}", err); } })); accept_button_box.pack_start(&accept_button, true, true, 0); accept_button.set_can_default(true); accept_button.set_has_default(true); } }; dialog.show_all(); ControlFlow::Continue })); thread::spawn(move || { let authenticator = Authenticator::new(&username, tx.clone()); if let Err(err) = authenticator.authenticate() { error!("PAM: {}", err); if tx.send(GtkConverseOp::ShowError("Authentication Failed".to_string())).is_ok() { thread::sleep(Duration::from_secs(2)); } DialogExitStatus::AuthFailed.exit(); } else { debug!("PAM success"); DialogExitStatus::AuthSucceeded.exit(); } }); gtk::main(); // We never call gtk::main_quit(), so we should never get here. If // we do, it'd be a bad bug, because we'd exit with status 0, which // would be interpreted as "auth success". DialogExitStatus::OtherError.exit(); } fn set_time_label(label: >k::Label) { let now = Local::now().format("%Y-%m-%d %H:%M:%S").to_string(); label.set_label(&now); }