420 lines
16 KiB
Rust

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<String>),
ShowBlindPrompt(String, mpsc::Sender<String>),
}
struct Authenticator {
username: String,
sender: glib::Sender<GtkConverseOp>,
}
impl Authenticator {
pub fn new<S: AsRef<str>>(username: S, op_sender: glib::Sender<GtkConverseOp>) -> 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<CString, pam_client::ErrorCode> {
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<CString, pam_client::ErrorCode> {
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::<gtk::Window>()
} 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::<X11Window>() {
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::<gtk::Window>()
};
// 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: &gtk::Label) {
let now = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
label.set_label(&now);
}