fingerprintd/implementations/agent.cpp

156 lines
6.3 KiB
C++
Raw Normal View History

// 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;
}