#![warn(clippy::all, clippy::pedantic)]
#![allow(clippy::used_underscore_binding)]
#![allow(clippy::non_ascii_literal)]
#![allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
#![allow(clippy::option_if_let_else)]
use std::os::unix::ffi::OsStrExt as _;
use std::path::PathBuf;
use clap::{crate_name, Args, Command, FromArgMatches as _, Parser};
use log::{debug, error, info};
use tokio::io::unix::AsyncFd;
use tokio::signal::unix::{signal, SignalKind};
use tokio::time::Instant;
use x11rb::connection::{Connection as _, RequestConnection as _};
use x11rb::protocol::xproto::{
self, ColormapWrapper, ConnectionExt as _, CursorWrapper, Window, WindowWrapper,
};
use x11rb::{atom_manager, properties};
use x11rb::protocol::randr::{self, ConnectionExt as _};
use x11rb::protocol::render::{self, ConnectionExt as _, PictType};
use x11rb::wrapper::ConnectionExt as _;
use x11rb::xcb_ffi::XCBConnection;
mod backbuffer;
mod config;
mod dialog;
mod errors;
mod event;
mod keyboard;
mod secret;
use errors::{Context as _, Result};
use secret::Passphrase;
pub const CLASS: &str = "SshAskpass";
pub const NAME: &str = crate_name!();
include!(concat!(env!("XASKPASS_BUILD_HEADER_DIR"), "/icon.rs"));
atom_manager! {
pub AtomCollection: AtomCollectionCookie {
WM_PROTOCOLS,
WM_DELETE_WINDOW,
_NET_WM_ICON,
_NET_WM_ICON_NAME,
_NET_WM_NAME,
_NET_WM_PID,
_NET_WM_WINDOW_TYPE,
_NET_WM_WINDOW_TYPE_DIALOG,
_NET_WM_STATE,
_NET_WM_STATE_ABOVE,
UTF8_STRING,
CLIPBOARD,
XSEL_DATA,
INCR,
}
}
pub type XId = u32;
pub type Connection = XCBConnection;
fn get_deadline(conn: &Connection, window: Window) -> Result<u128> {
let has_randr = conn
.extension_information(randr::X11_EXTENSION_NAME)?
.is_some();
let mut min_cycle_deadline = None;
if has_randr {
let (major, minor) = randr::X11_XML_VERSION;
let version_cookie = conn.randr_query_version(major, minor)?;
if log::log_enabled!(log::Level::Debug) {
let version = version_cookie.reply()?;
debug!(
"randr version {}.{}",
version.major_version, version.minor_version
);
}
let screen_resources = conn.randr_get_screen_resources(window)?.reply()?;
for crtc in screen_resources.crtcs {
let crtc_info = conn
.randr_get_crtc_info(crtc, screen_resources.config_timestamp)?
.reply()?;
if crtc_info.mode == x11rb::NONE {
continue;
}
for mode in &screen_resources.modes {
if mode.id == crtc_info.mode {
if mode.dot_clock != 0 {
let vblank_time = (f64::from(mode.htotal) * f64::from(mode.vtotal))
/ (f64::from(mode.dot_clock) / 1_000_000.0);
debug!(
"crtc {}: vblank_time: {}μs, dot_clock: {}, htotal: {}, vtotal: {}",
crtc, vblank_time, mode.dot_clock, mode.htotal, mode.vtotal
);
if let Some(min) = min_cycle_deadline {
if vblank_time < min {
min_cycle_deadline = Some(vblank_time);
}
} else {
min_cycle_deadline = Some(vblank_time);
}
}
break;
}
}
}
}
Ok(min_cycle_deadline.map_or(8000, |f| (f / 2.0).floor() as u128))
}
fn choose_visual(conn: &Connection, screen_num: usize) -> Result<(u8, xproto::Visualid)> {
let depth = 32;
let screen = &conn.setup().roots[screen_num];
let has_render = conn
.extension_information(render::X11_EXTENSION_NAME)?
.is_some();
if has_render {
let (major, minor) = render::X11_XML_VERSION;
let version = conn.render_query_version(major, minor)?;
if log::log_enabled!(log::Level::Debug) {
let version = version.reply()?;
debug!(
"render version {}.{}",
version.major_version, version.minor_version
);
}
let formats = conn.render_query_pict_formats()?.reply()?;
let format = formats
.formats
.iter()
.filter(|info| (info.type_, info.depth) == (PictType::DIRECT, depth))
.filter(|info| {
let d = info.direct;
(d.red_mask, d.green_mask, d.blue_mask, d.alpha_mask) == (0xff, 0xff, 0xff, 0xff)
})
.find(|info| {
let d = info.direct;
(d.red_shift, d.green_shift, d.blue_shift, d.alpha_shift) == (16, 8, 0, 24)
});
if let Some(format) = format {
debug!("found pict format");
if let Some(visual) = formats.screens[screen_num]
.depths
.iter()
.flat_map(|d| &d.visuals)
.find(|v| v.format == format.id)
{
debug!("found visual");
return Ok((format.depth, visual.visual));
}
}
}
Ok((screen.root_depth, screen.root_visual))
}
fn find_xcb_visualtype(conn: &Connection, visual_id: u32) -> Option<xproto::Visualtype> {
for root in &conn.setup().roots {
for depth in &root.allowed_depths {
for visual in &depth.visuals {
if visual.visual_id == visual_id {
debug!("visual class: {:?}", visual.class);
return Some(*visual);
}
}
}
}
None
}
#[allow(clippy::too_many_lines)]
async fn run_xcontext(
config: config::Config,
opts: Opts,
startup_time: Instant,
) -> Result<Option<Passphrase>> {
let (conn, screen_num) = XCBConnection::connect(None).context("X11 connect")?;
let xfd = AsyncFd::new(conn).context("asyncfd failed")?;
let conn = xfd.get_ref();
debug!("connected X server");
let atoms = AtomCollection::new(conn)?;
conn.prefetch_extension_information(x11rb::protocol::present::X11_EXTENSION_NAME)?;
conn.prefetch_extension_information(x11rb::protocol::xkb::X11_EXTENSION_NAME)?;
conn.prefetch_extension_information(x11rb::protocol::render::X11_EXTENSION_NAME)?;
conn.prefetch_extension_information(x11rb::protocol::randr::X11_EXTENSION_NAME)?;
conn.flush()?;
let setup = conn.setup();
let screen = setup.roots.get(screen_num).expect("unknown screen");
let (depth, visualid) = if config.depth == 32 {
choose_visual(conn, screen_num)?
} else {
(screen.root_depth, screen.root_visual)
};
debug!("window depth: {}", depth);
let compositor_atom = if depth == 32 {
conn.prefetch_extension_information(x11rb::protocol::xfixes::X11_EXTENSION_NAME)?;
let compositor_atom = format!("_NET_WM_CM_S{}", screen_num);
Some(conn.intern_atom(false, compositor_atom.as_bytes())?)
} else {
None
};
let visual_type = find_xcb_visualtype(conn, visualid).unwrap();
let surface = backbuffer::XcbSurface::new(conn, screen.root, depth, &visual_type, 1, 1)?;
let backbuffer = backbuffer::Backbuffer::new(conn, screen.root, surface)?;
conn.flush()?;
let mut dialog = dialog::Dialog::new(
config.dialog,
screen,
&backbuffer.cr,
opts.label.as_deref(),
opts.debug,
)?;
let (window_width, window_height) = dialog.window_size(&backbuffer.cr);
debug!("window width: {}, height: {}", window_width, window_height);
let colormap = if visual_type.visual_id == screen.root_visual {
None
} else {
debug!("visualId requires a new colormap");
let colormap = ColormapWrapper::create_colormap(
conn,
xproto::ColormapAlloc::NONE,
screen.root,
visual_type.visual_id,
)?;
Some(colormap)
};
let window_wrapper = WindowWrapper::create_window(
conn,
depth,
screen.root,
0, 0, window_width,
window_height,
0, xproto::WindowClass::INPUT_OUTPUT,
visual_type.visual_id,
&xproto::CreateWindowAux::new()
.event_mask(
xproto::EventMask::EXPOSURE
| xproto::EventMask::KEY_PRESS
| xproto::EventMask::STRUCTURE_NOTIFY
| xproto::EventMask::BUTTON_PRESS
| xproto::EventMask::BUTTON_RELEASE
| xproto::EventMask::POINTER_MOTION
| xproto::EventMask::FOCUS_CHANGE,
)
.background_pixmap(xproto::PixmapEnum::NONE)
.border_pixel(screen.black_pixel)
.colormap(
colormap
.as_ref()
.map_or(screen.default_colormap, ColormapWrapper::colormap),
),
)?;
let window = window_wrapper.window();
let atoms = atoms.reply()?;
let hostname = std::env::var_os("HOSTNAME").unwrap_or_else(gethostname::gethostname);
let mut title = config.title;
if config.show_hostname {
title.push('@');
title.push_str(&hostname.to_string_lossy());
}
conn.change_property8(
xproto::PropMode::REPLACE,
window,
xproto::AtomEnum::WM_NAME,
xproto::AtomEnum::STRING,
title.as_bytes(),
)?;
conn.change_property8(
xproto::PropMode::REPLACE,
window,
xproto::AtomEnum::WM_ICON_NAME,
xproto::AtomEnum::STRING,
title.as_bytes(),
)?;
conn.change_property8(
xproto::PropMode::REPLACE,
window,
atoms._NET_WM_NAME,
atoms.UTF8_STRING,
title.as_bytes(),
)?;
conn.change_property8(
xproto::PropMode::REPLACE,
window,
atoms._NET_WM_ICON_NAME,
atoms.UTF8_STRING,
title.as_bytes(),
)?;
conn.change_property8(
xproto::PropMode::REPLACE,
window,
xproto::AtomEnum::WM_CLASS,
xproto::AtomEnum::STRING,
[opts.name.as_bytes(), CLASS.as_bytes()]
.join(&b'\0')
.as_slice(),
)?;
conn.change_property8(
xproto::PropMode::REPLACE,
window,
xproto::AtomEnum::WM_CLIENT_MACHINE,
xproto::AtomEnum::STRING,
hostname.as_bytes(),
)?;
conn.change_property32(
xproto::PropMode::REPLACE,
window,
atoms._NET_WM_PID,
xproto::AtomEnum::CARDINAL,
&[std::process::id()],
)?;
conn.change_property32(
xproto::PropMode::REPLACE,
window,
atoms._NET_WM_WINDOW_TYPE,
xproto::AtomEnum::ATOM,
&[atoms._NET_WM_WINDOW_TYPE_DIALOG],
)?;
conn.change_property32(
xproto::PropMode::REPLACE,
window,
atoms._NET_WM_STATE,
xproto::AtomEnum::ATOM,
&[atoms._NET_WM_STATE_ABOVE],
)?;
conn.change_property32(
xproto::PropMode::REPLACE,
window,
atoms.WM_PROTOCOLS,
xproto::AtomEnum::ATOM,
&[atoms.WM_DELETE_WINDOW],
)?;
let wm_hints = properties::WmHints {
input: Some(true),
initial_state: Some(properties::WmHintsState::Normal),
..properties::WmHints::default()
};
wm_hints.set(conn, window)?;
for (width, height, data) in ICONS {
let mut icon_data = Vec::with_capacity(8 + data.len());
icon_data.extend_from_slice(&width.to_ne_bytes());
icon_data.extend_from_slice(&height.to_ne_bytes());
icon_data.extend_from_slice(data);
conn.change_property(
xproto::PropMode::APPEND,
window,
atoms._NET_WM_ICON,
xproto::AtomEnum::CARDINAL,
32,
(icon_data.len() / 4).try_into().unwrap(),
&icon_data,
)?;
}
let mut size_hints = properties::WmSizeHints {
size: Some((
properties::WmSizeHintsSpecification::ProgramSpecified,
window_width.into(),
window_height.into(),
)),
min_size: Some((window_width.into(), window_height.into())),
..properties::WmSizeHints::default()
};
if !config.resizable {
debug!("trying to disable resizing");
size_hints.max_size = Some((window_width.into(), window_height.into()));
}
size_hints.set_normal_hints(conn, window)?;
debug!("map window");
conn.map_window(window)?;
debug!("flush");
conn.flush()?;
let (transparency, compositor_atom) = if let Some(compositor_atom) = compositor_atom {
let compositor_atom = compositor_atom.reply()?.atom;
let selection = conn.get_selection_owner(compositor_atom)?;
(
selection.reply()?.owner != x11rb::NONE,
Some(compositor_atom),
)
} else {
(false, None)
};
let resource_db;
let cursor_handle = if dialog.uses_cursor() {
debug!("loading x11 resources");
resource_db = x11rb::resource_manager::new_from_default(conn)?;
debug!("initializing cursor handle");
Some(x11rb::cursor::Handle::new(conn, screen_num, &resource_db)?)
} else {
None
};
debug!("compositor detected: {}", transparency);
dialog.set_transparency(transparency);
debug!("keyboard init");
let keyboard = keyboard::Keyboard::new(conn)?;
dialog.set_keyboard(&keyboard);
let input_cursor = if let Some(cursor_handle) = cursor_handle {
debug!("cursor init");
let cursor_handle = cursor_handle.reply()?;
Some(CursorWrapper::for_cursor(
conn,
cursor_handle.load_cursor(conn, "xterm").unwrap(),
))
} else {
None
};
let cycle_deadline = get_deadline(conn, window)?;
debug!("cycle_deadline: {}μs", cycle_deadline);
debug!("dialog init");
let mut backbuffer = backbuffer.reply()?;
backbuffer.init(window, &mut dialog)?;
let mut xcontext = event::XContext::new(event::Config {
keyboard,
xfd: &xfd,
backbuffer,
window: window_wrapper,
atoms,
width: window_width,
height: window_height,
grab_keyboard: config.grab_keyboard,
startup_time,
input_cursor,
compositor_atom,
debug: opts.debug,
cycle_deadline,
})?;
debug!("init took {}ms", startup_time.elapsed().as_millis());
xcontext.run_events(dialog).await
}
#[derive(Parser)]
#[clap(
version = env!("XASKPASS_BUILD_FULL_VERSION"),
about,
)]
struct Opts {
#[clap(long, default_value = NAME)]
name: String,
#[clap(short, long)]
quiet: bool,
#[clap(short, long, action = clap::ArgAction::Count)]
verbose: u8,
#[clap(short, long)]
config: Option<PathBuf>,
#[clap(short, long)]
debug: bool,
label: Option<String>,
#[clap(long)]
gen_config: bool,
}
fn run() -> i32 {
let startup_time = Instant::now();
let cfg_loader = config::Loader::new();
let mut help = format!(
"CONFIGURATION FILE:\n defaults: {}{}.toml",
cfg_loader.xdg_dirs.get_config_home().display(),
NAME,
);
for d in cfg_loader.xdg_dirs.get_config_dirs() {
help.push_str(&format!(",\n {}/{}.toml", d.display(), NAME));
}
let app = Command::new(NAME).after_help(help);
let app = Opts::augment_args(app);
let opts = Opts::from_arg_matches(&app.get_matches()).expect("from_arg_matches");
let mut log = stderrlog::new();
log.quiet(opts.quiet)
.verbosity(usize::from(opts.verbose) + 2);
if opts.debug {
log.timestamp(stderrlog::Timestamp::Millisecond)
.show_module_names(true);
}
log.init().unwrap();
debug!(
"{} {} is starting",
opts.name,
env!("XASKPASS_BUILD_FULL_VERSION")
);
match run_logged(&cfg_loader, opts, startup_time) {
Ok(ret) => ret,
Err(err) => {
error!("{}", err);
2
}
}
}
fn run_logged(cfg_loader: &config::Loader, opts: Opts, startup_time: Instant) -> Result<i32> {
if opts.gen_config {
let cfg = config::Config::default();
config::Loader::print(&cfg)?;
return Ok(0);
}
debug!("load config");
let config = if let Some(ref path) = opts.config {
config::Loader::load_path(path)?
} else {
cfg_loader.load()?
};
debug!("config loaded");
dialog::setlocale();
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_io()
.enable_time()
.build()
.expect("tokio runtime build failed");
let _runtime_guard = runtime.enter();
let mut sigint = signal(SignalKind::interrupt()).unwrap();
let mut sighup = signal(SignalKind::hangup()).unwrap();
let mut sigterm = signal(SignalKind::terminate()).unwrap();
let mut mainret = 1;
runtime.block_on(async {
tokio::select! {
_ = sigint.recv() => {
info!("got sigint");
}
_ = sighup.recv() => {
info!("got sighup");
}
_ = sigterm.recv() => {
info!("got sigterm");
}
ret = run_xcontext(config, opts, startup_time) => {
match ret? {
Some(pass) => {
pass.write_stdout().unwrap();
mainret = 0;
}
None => {
debug!("cancelled");
},
}
}
}
Ok(()) as Result<()>
})?;
debug!("exit");
Ok(mainret)
}
fn main() {
std::process::exit(run());
}
#[test]
fn verify_app() {
let app = Command::new(NAME);
let app = Opts::augment_args(app);
app.debug_assert();
}