349 lines
12 KiB
Rust

use chrono::prelude::*;
use gdkx11::X11Window;
use gethostname::gethostname;
use glib::GString;
use gtk::{prelude::*, Button, Entry, Label, Plug, Window};
use log::{debug, error, warn};
use std::{io::{self, Write}, process::{exit, Command}, rc::Rc, thread, time::Duration};
use bscreensaver_util::{init_logging, settings::Configuration};
const DIALOG_UPDATE_INTERVAL: Duration = Duration::from_millis(100);
const DIALOG_TIMEOUT: Duration = Duration::from_secs(60);
fn main() -> anyhow::Result<()> {
init_logging("BSCREENSAVER_DIALOG_GTK3_LOG");
glib::log_set_default_handler(glib::rust_log_handler);
let mut config = Configuration::load()?;
let new_login_command = config.new_login_command
.take()
.map(|nlc| shell_words::split(&nlc))
.transpose()?;
let standalone = std::env::var("BSCREENSAVER_DIALOG_STANDALONE").is_ok();
gtk::init()?;
let (tx, rx) = glib::MainContext::sync_channel(glib::PRIORITY_DEFAULT, 1);
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);
exit(2);
},
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);
exit(2);
};
});
// 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(|_, _| exit(1));
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);
glib::Continue(true)
});
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 auth_failed_label = gtk::Label::builder()
.label("Authentication Failed!")
.xalign(0.5)
.yalign(0.5)
.attributes(&attrs)
.opacity(0.0)
.build();
vbox.pack_start(&auth_failed_label, false, false, 0);
rx.attach(None, move |exit_status| {
if exit_status != 0 {
auth_failed_label.set_opacity(1.0);
glib::timeout_add_seconds_local_once(2, move || exit(exit_status));
} else {
exit(exit_status);
}
glib::Continue(true)
});
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(4)
.build();
top_sg.add_widget(&vbox);
top_hbox.pack_start(&vbox, true, true, 0);
let hbox = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.spacing(8)
.build();
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 = bscreensaver_util::get_username()?;
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();
vbox.pack_start(&hbox, true, true, 0);
let label = Label::builder()
.label("Password:")
.xalign(0.0)
.build();
label_sg.add_widget(&label);
hbox.pack_start(&label, false, true, 8);
let password_box = Rc::new(Entry::builder()
.visibility(false)
.input_purpose(gtk::InputPurpose::Password)
.activates_default(true)
.width_chars(25)
.build());
entry_sg.add_widget(&*password_box);
hbox.pack_start(&*password_box, true, true, 8);
password_box.connect_key_press_event(|_, ev| {
if ev.keyval().name() == Some(GString::from("Escape")) {
exit(1);
}
gtk::Inhibit(false)
});
password_box.grab_focus();
let hbox = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.spacing(8)
.build();
vbox.pack_start(&hbox, true, true, 2);
if let Some(new_login_command) = new_login_command {
let new_login_button = Button::builder()
.label("New Login")
.build();
new_login_button.connect_clicked(move |_| {
let cmd = &new_login_command[0];
let empty = Vec::<String>::new();
let args = if new_login_command.len() > 1 { &new_login_command[1..] } else { &empty };
if let Err(err) = Command::new(cmd).args(args).spawn() {
warn!("Failed to run new login command: {}", err);
} else {
exit(1);
}
});
hbox.pack_start(&new_login_button, false, true, 8);
}
let unlock_button = Button::builder()
.label("Unlock")
.build();
{
let password_box = Rc::clone(&password_box);
unlock_button.connect_clicked(move |unlock_button| {
unlock_button.set_sensitive(false);
password_box.set_sensitive(false);
let username = username.clone();
let password = password_box.text().to_string();
let tx = tx.clone();
thread::spawn(move || {
let status = if authenticate(&username, &password) { 0 } else { -1 };
if let Err(err) = tx.send(status) {
error!("Failed to send exit status to main thread: {}", err);
exit(2);
}
});
});
}
hbox.pack_end(&unlock_button, false, true, 8);
unlock_button.set_can_default(true);
unlock_button.set_has_default(true);
let timer = Rc::new(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 timer = Rc::clone(&timer);
let delta = (DIALOG_UPDATE_INTERVAL.as_millis() as f64) / (DIALOG_TIMEOUT.as_millis() as f64);
glib::timeout_add_local(DIALOG_UPDATE_INTERVAL, move || {
let new_fraction = timer.fraction() - delta;
if new_fraction <= 0.0 {
exit(1);
}
timer.set_fraction(new_fraction);
Continue(true)
});
}
{
let timer = Rc::clone(&timer);
password_box.connect_key_press_event(move |_, _| {
let new_fraction = timer.fraction() + 0.05;
timer.set_fraction(if new_fraction >= 1.0 { 1.0 } else { new_fraction });
Inhibit(false)
});
}
dialog.show_all();
gtk::main();
Ok(())
}
fn set_time_label(label: &gtk::Label) {
let now = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
label.set_label(&now);
}
fn authenticate(username: &String, password: &String) -> bool {
let mut authenticator = match pam::Authenticator::with_password("xscreensaver") {
Err(err) => {
error!("[PAM] {}", err);
return false;
},
Ok(authenticator) => authenticator,
};
authenticator.get_handler().set_credentials(username, password);
if let Err(err) = authenticator.authenticate() {
error!("[PAM] {}", err);
return false;
}
if let Err(err) = authenticator.open_session() {
error!("[PAM] {}", err);
false
} else {
true
}
}