420 lines
16 KiB
Rust
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: >k::Label) {
|
|
let now = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
|
|
label.set_label(&now);
|
|
}
|