156 lines
6.3 KiB
C++
156 lines
6.3 KiB
C++
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
||
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||
|
|
|
||
|
|
// lint-disable-file no-char-pointer
|
||
|
|
/*
|
||
|
|
fingerprintd-agent — run something in YOUR session when a finger matches.
|
||
|
|
|
||
|
|
The daemon is root. It has no session bus, no display and no business starting
|
||
|
|
your applications, so it does not try: it announces every matched finger on the
|
||
|
|
system bus and stops there.
|
||
|
|
|
||
|
|
net.catcrafts.Fingerprintd1.FingerMatched(s finger, u uid)
|
||
|
|
on /net/reactivated/Fprint/Device/0
|
||
|
|
|
||
|
|
This is the other half. It runs as you, inside your session, with your
|
||
|
|
environment, and maps fingers to commands from a file you own:
|
||
|
|
|
||
|
|
~/.config/fingerprintd/fingers.conf
|
||
|
|
right-ring-finger kde-open camera
|
||
|
|
left-index-finger plasma-settings
|
||
|
|
|
||
|
|
Different trust domain from /etc/fingerprintd/actions.conf, deliberately. That
|
||
|
|
file is a root shell and is guarded like one. This one runs your commands as
|
||
|
|
you, so it is an ordinary dotfile: no ownership check, relative commands fine,
|
||
|
|
re-read when it changes so editing it does not need a restart.
|
||
|
|
*/
|
||
|
|
// stdio.h before the module import, for the same reason main.cpp does it:
|
||
|
|
// setvbuf and its constants come from the C header, and the std module does
|
||
|
|
// not re-export the macro.
|
||
|
|
#include <stdio.h>
|
||
|
|
#include <gio/gio.h>
|
||
|
|
#include <unistd.h>
|
||
|
|
|
||
|
|
import std;
|
||
|
|
import Fingerprintd;
|
||
|
|
|
||
|
|
namespace {
|
||
|
|
|
||
|
|
constexpr const char* BusName = "net.reactivated.Fprint";
|
||
|
|
constexpr const char* DevicePath = "/net/reactivated/Fprint/Device/0";
|
||
|
|
constexpr const char* Iface = "net.catcrafts.Fingerprintd1";
|
||
|
|
constexpr const char* Signal = "FingerMatched";
|
||
|
|
|
||
|
|
std::string ConfigPath() {
|
||
|
|
const char* xdg = std::getenv("XDG_CONFIG_HOME");
|
||
|
|
if (xdg && *xdg) return std::string(xdg) + "/fingerprintd/fingers.conf";
|
||
|
|
const char* home = std::getenv("HOME");
|
||
|
|
return std::string(home ? home : ".") + "/.config/fingerprintd/fingers.conf";
|
||
|
|
}
|
||
|
|
|
||
|
|
struct Config {
|
||
|
|
std::filesystem::file_time_type stamp{};
|
||
|
|
std::map<std::string, std::string> byFinger;
|
||
|
|
|
||
|
|
// Re-read when the file's mtime moves, so editing the map takes effect
|
||
|
|
// without restarting the agent. Cheap: one stat per matched finger,
|
||
|
|
// and a finger match is a human-scale event.
|
||
|
|
void Refresh(const std::string& path) {
|
||
|
|
std::error_code ec;
|
||
|
|
auto now = std::filesystem::last_write_time(path, ec);
|
||
|
|
if (ec) { byFinger.clear(); return; }
|
||
|
|
if (now == stamp && !byFinger.empty()) return;
|
||
|
|
stamp = now;
|
||
|
|
byFinger.clear();
|
||
|
|
|
||
|
|
std::ifstream f(path);
|
||
|
|
std::string line;
|
||
|
|
while (std::getline(f, line)) {
|
||
|
|
std::string_view v(line);
|
||
|
|
while (!v.empty() && (v.front() == ' ' || v.front() == '\t')) v.remove_prefix(1);
|
||
|
|
while (!v.empty() && (v.back() == ' ' || v.back() == '\t' || v.back() == '\r'))
|
||
|
|
v.remove_suffix(1);
|
||
|
|
if (v.empty() || v.front() == '#') continue;
|
||
|
|
std::size_t sp = v.find_first_of(" \t");
|
||
|
|
if (sp == std::string_view::npos) continue;
|
||
|
|
std::string name(v.substr(0, sp));
|
||
|
|
std::string_view cmd = v.substr(sp);
|
||
|
|
while (!cmd.empty() && (cmd.front() == ' ' || cmd.front() == '\t'))
|
||
|
|
cmd.remove_prefix(1);
|
||
|
|
if (cmd.empty()) continue;
|
||
|
|
// Validated against the same vocabulary the daemon uses, so a
|
||
|
|
// typo is reported here rather than silently never firing.
|
||
|
|
if (!fingerprintd::store::FingerFromName(name)) {
|
||
|
|
std::println(std::cerr, "{}: not a finger name: {}", path, name);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
byFinger[name] = std::string(cmd);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
Config g_config;
|
||
|
|
std::string g_configPath;
|
||
|
|
std::uint32_t g_me = 0;
|
||
|
|
|
||
|
|
void OnFingerMatched(GDBusConnection*, const gchar*, const gchar*, const gchar*,
|
||
|
|
const gchar*, GVariant* params, gpointer) {
|
||
|
|
const gchar* finger = nullptr;
|
||
|
|
guint32 uid = 0;
|
||
|
|
g_variant_get(params, "(&su)", &finger, &uid);
|
||
|
|
if (!finger) return;
|
||
|
|
|
||
|
|
// The signal is visible to every local user. Someone else's finger is
|
||
|
|
// not your trigger.
|
||
|
|
if (uid != g_me) return;
|
||
|
|
|
||
|
|
g_config.Refresh(g_configPath);
|
||
|
|
auto it = g_config.byFinger.find(finger);
|
||
|
|
if (it == g_config.byFinger.end()) return;
|
||
|
|
|
||
|
|
std::println("{} -> {}", finger, it->second);
|
||
|
|
GError* err = nullptr;
|
||
|
|
// Async: a slow application must not wedge the agent, and the agent is
|
||
|
|
// not the thing that decides whether the unlock succeeded -- that has
|
||
|
|
// already happened by the time this signal arrives.
|
||
|
|
if (!g_spawn_command_line_async(it->second.c_str(), &err)) {
|
||
|
|
std::println(std::cerr, " failed: {}", err ? err->message : "unknown");
|
||
|
|
if (err) g_error_free(err);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
int main(int argc, char** argv) {
|
||
|
|
::setvbuf(stdout, nullptr, _IOLBF, 0);
|
||
|
|
std::span<char*> args(argv, static_cast<std::size_t>(argc));
|
||
|
|
for (std::string_view a : args.subspan(1)) {
|
||
|
|
if (a == "--version") { std::println("fingerprintd-agent 0.2.2"); return 0; }
|
||
|
|
if (a.starts_with("--config=")) g_configPath = a.substr(9);
|
||
|
|
}
|
||
|
|
// Root has a session bus about as often as it has a display. Refusing is
|
||
|
|
// clearer than starting and never firing.
|
||
|
|
if (::geteuid() == 0) {
|
||
|
|
std::println(std::cerr, "fingerprintd-agent: run as your user, not root");
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
if (g_configPath.empty()) g_configPath = ConfigPath();
|
||
|
|
g_me = static_cast<std::uint32_t>(::getuid());
|
||
|
|
|
||
|
|
GError* err = nullptr;
|
||
|
|
GDBusConnection* conn = g_bus_get_sync(G_BUS_TYPE_SYSTEM, nullptr, &err);
|
||
|
|
if (!conn) {
|
||
|
|
std::println(std::cerr, "system bus: {}", err ? err->message : "unavailable");
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
g_dbus_connection_signal_subscribe(
|
||
|
|
conn, BusName, Iface, Signal, DevicePath, nullptr,
|
||
|
|
G_DBUS_SIGNAL_FLAGS_NONE, OnFingerMatched, nullptr, nullptr);
|
||
|
|
|
||
|
|
g_config.Refresh(g_configPath);
|
||
|
|
std::println("fingerprintd-agent: uid {}, {} finger(s) mapped from {}",
|
||
|
|
g_me, g_config.byFinger.size(), g_configPath);
|
||
|
|
|
||
|
|
GMainLoop* loop = g_main_loop_new(nullptr, FALSE);
|
||
|
|
g_main_loop_run(loop);
|
||
|
|
return 0;
|
||
|
|
}
|