mirror of
https://github.com/troglobit/finit.git
synced 2026-09-30 13:02:37 +07:00
All three org.finit signals were invisible to generated proxies, and org.freedesktop.DBus was missing from the standard interfaces even though Hello, AddMatch, and RemoveMatch are answered. Add a link_signal_t table to the vtable, emitted like methods and properties, declare the Manager1 and Cond1 signals, and complete the static XML with PropertiesChanged and org.freedesktop.DBus. Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
547 lines
16 KiB
C
547 lines
16 KiB
C
/* libink — built-in implementations of the well-known
|
|
* org.freedesktop.DBus.* interfaces (Hello, Peer, Introspectable).
|
|
*
|
|
* These run before object-tree lookup in the dispatcher; returning
|
|
* 0 means "handled, reply sent"; <0 means "not a built-in, fall
|
|
* through to user-registered handlers".
|
|
*
|
|
* Copyright (c) 2026 Joachim Wiberg <troglobit@gmail.com>
|
|
* SPDX-License-Identifier: MIT
|
|
*/
|
|
|
|
#include <errno.h>
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <sys/types.h>
|
|
|
|
#include "internal.h"
|
|
|
|
/* ---------- helpers ---------- */
|
|
|
|
static int member_is(const struct link_msg *m, const char *iface, const char *member)
|
|
{
|
|
if (!m->member || strcmp(m->member, member) != 0)
|
|
return 0;
|
|
if (m->interface && strcmp(m->interface, iface) != 0)
|
|
return 0;
|
|
return 1;
|
|
}
|
|
|
|
static int send_string_reply(link_connection_t *conn, const struct link_msg *req,
|
|
const char *s)
|
|
{
|
|
struct link_writer w;
|
|
ssize_t blen;
|
|
|
|
__w_init(&w, conn->txbuf, sizeof(conn->txbuf));
|
|
__w_string(&w, s);
|
|
blen = __w_finish(&w);
|
|
if (blen < 0) {
|
|
errno = EMSGSIZE;
|
|
return -1;
|
|
}
|
|
return __send_method_return(conn, req, "s", conn->txbuf, (size_t)blen);
|
|
}
|
|
|
|
/* ---------- Hello ---------- */
|
|
|
|
static int handle_hello(link_connection_t *conn, const struct link_msg *m)
|
|
{
|
|
if (!conn->unique_name[0]) {
|
|
uint32_t n = ++conn->server->next_unique_id;
|
|
|
|
snprintf(conn->unique_name, sizeof(conn->unique_name),
|
|
":1.%u", n);
|
|
}
|
|
return send_string_reply(conn, m, conn->unique_name);
|
|
}
|
|
|
|
/* ---------- Ping / GetMachineId ---------- */
|
|
|
|
static int handle_ping(link_connection_t *conn, const struct link_msg *m)
|
|
{
|
|
return __send_method_return(conn, m, NULL, NULL, 0);
|
|
}
|
|
|
|
static int handle_get_machine_id(link_connection_t *conn, const struct link_msg *m)
|
|
{
|
|
/* D-Bus mandates a 32-char hex machine-id. Use the per-server
|
|
* GUID-style identifier we already generate for each connection,
|
|
* promoted to a per-server constant on first call. Good enough
|
|
* for the brokerless case where clients use this only as a
|
|
* sanity hint. */
|
|
static char machine_id[33];
|
|
|
|
if (!machine_id[0])
|
|
__auth_generate_guid(machine_id);
|
|
return send_string_reply(conn, m, machine_id);
|
|
}
|
|
|
|
/* ---------- Introspect ---------- */
|
|
|
|
struct xbuf {
|
|
char *buf;
|
|
size_t cap;
|
|
size_t off;
|
|
int err;
|
|
};
|
|
|
|
static void xprintf(struct xbuf *x, const char *fmt, ...)
|
|
{
|
|
va_list ap;
|
|
int n;
|
|
|
|
if (x->err)
|
|
return;
|
|
va_start(ap, fmt);
|
|
n = vsnprintf(x->buf + x->off, x->cap - x->off, fmt, ap);
|
|
va_end(ap);
|
|
if (n < 0 || (size_t)n >= x->cap - x->off) {
|
|
x->err = 1;
|
|
return;
|
|
}
|
|
x->off += (size_t)n;
|
|
}
|
|
|
|
/*
|
|
* Advance past one single complete type in a D-Bus signature:
|
|
* a basic type code, 'a' + element type, or a bracketed group.
|
|
* Signatures come from our own vtables, so trust them; an
|
|
* unterminated group just stops at NUL.
|
|
*/
|
|
static const char *sig_next(const char *p)
|
|
{
|
|
while (*p == 'a') /* array prefixes, then element type */
|
|
p++;
|
|
if (*p == '(' || *p == '{') {
|
|
char close = *p == '(' ? ')' : '}';
|
|
|
|
for (p++; *p && *p != close; p = sig_next(p))
|
|
;
|
|
}
|
|
return *p ? p + 1 : p; /* NUL: unterminated group, stop here */
|
|
}
|
|
|
|
static void emit_args(struct xbuf *x, const char *sig, const char *dir)
|
|
{
|
|
const char *p, *e;
|
|
|
|
for (p = sig; p && *p; p = e) {
|
|
e = sig_next(p);
|
|
xprintf(x, " <arg type=\"%.*s\" direction=\"%s\"/>\n",
|
|
(int)(e - p), p, dir);
|
|
}
|
|
}
|
|
|
|
/* Emit a single <method> stanza for one method definition. */
|
|
static void emit_method(struct xbuf *x, const link_method_t *m)
|
|
{
|
|
xprintf(x, " <method name=\"%s\">\n", m->name);
|
|
emit_args(x, m->in_sig, "in");
|
|
emit_args(x, m->out_sig, "out");
|
|
xprintf(x, " </method>\n");
|
|
}
|
|
|
|
static void emit_property(struct xbuf *x, const link_property_t *p)
|
|
{
|
|
/* Setters are not implemented, so every property advertises
|
|
* access="read" today. When Properties.Set lands, switch on
|
|
* a writable flag. */
|
|
xprintf(x, " <property name=\"%s\" type=\"%s\" access=\"read\"/>\n",
|
|
p->name, p->sig ? p->sig : "s");
|
|
}
|
|
|
|
static void emit_signal(struct xbuf *x, const link_signal_t *s)
|
|
{
|
|
xprintf(x, " <signal name=\"%s\">\n", s->name);
|
|
emit_args(x, s->sig, "out");
|
|
xprintf(x, " </signal>\n");
|
|
}
|
|
|
|
static const char STANDARD_INTERFACES_XML[] =
|
|
" <interface name=\"org.freedesktop.DBus.Introspectable\">\n"
|
|
" <method name=\"Introspect\">\n"
|
|
" <arg name=\"data\" type=\"s\" direction=\"out\"/>\n"
|
|
" </method>\n"
|
|
" </interface>\n"
|
|
" <interface name=\"org.freedesktop.DBus.Peer\">\n"
|
|
" <method name=\"Ping\"/>\n"
|
|
" <method name=\"GetMachineId\">\n"
|
|
" <arg name=\"machine_uuid\" type=\"s\" direction=\"out\"/>\n"
|
|
" </method>\n"
|
|
" </interface>\n"
|
|
" <interface name=\"org.freedesktop.DBus.Properties\">\n"
|
|
" <method name=\"Get\">\n"
|
|
" <arg name=\"interface\" type=\"s\" direction=\"in\"/>\n"
|
|
" <arg name=\"property\" type=\"s\" direction=\"in\"/>\n"
|
|
" <arg name=\"value\" type=\"v\" direction=\"out\"/>\n"
|
|
" </method>\n"
|
|
" <method name=\"GetAll\">\n"
|
|
" <arg name=\"interface\" type=\"s\" direction=\"in\"/>\n"
|
|
" <arg name=\"props\" type=\"a{sv}\" direction=\"out\"/>\n"
|
|
" </method>\n"
|
|
" <signal name=\"PropertiesChanged\">\n"
|
|
" <arg name=\"interface\" type=\"s\"/>\n"
|
|
" <arg name=\"changed\" type=\"a{sv}\"/>\n"
|
|
" <arg name=\"invalidated\" type=\"as\"/>\n"
|
|
" </signal>\n"
|
|
" </interface>\n";
|
|
|
|
/*
|
|
* Hello/AddMatch/RemoveMatch are answered on the canonical
|
|
* /org/freedesktop/DBus object only, so declare them only there.
|
|
*/
|
|
static const char DBUS_DRIVER_XML[] =
|
|
" <interface name=\"org.freedesktop.DBus\">\n"
|
|
" <method name=\"Hello\">\n"
|
|
" <arg name=\"name\" type=\"s\" direction=\"out\"/>\n"
|
|
" </method>\n"
|
|
" <method name=\"AddMatch\">\n"
|
|
" <arg name=\"rule\" type=\"s\" direction=\"in\"/>\n"
|
|
" </method>\n"
|
|
" <method name=\"RemoveMatch\">\n"
|
|
" <arg name=\"rule\" type=\"s\" direction=\"in\"/>\n"
|
|
" </method>\n"
|
|
" </interface>\n";
|
|
|
|
/* Is `child` a path under `parent`? If so, write the first segment
|
|
* of the relative remainder into out (max outsz) and return 1. */
|
|
static int child_segment(const char *parent, const char *child,
|
|
char *out, size_t outsz)
|
|
{
|
|
size_t plen = strlen(parent);
|
|
const char *rest, *slash;
|
|
size_t seglen;
|
|
|
|
if (strncmp(parent, child, plen) != 0)
|
|
return 0;
|
|
/* Special case for "/" */
|
|
if (plen == 1 && parent[0] == '/')
|
|
rest = child + 1;
|
|
else if (child[plen] != '/')
|
|
return 0;
|
|
else
|
|
rest = child + plen + 1;
|
|
if (!*rest)
|
|
return 0;
|
|
|
|
slash = strchr(rest, '/');
|
|
seglen = slash ? (size_t)(slash - rest) : strlen(rest);
|
|
if (seglen + 1 > outsz)
|
|
return 0;
|
|
memcpy(out, rest, seglen);
|
|
out[seglen] = '\0';
|
|
return 1;
|
|
}
|
|
|
|
static int handle_introspect(link_connection_t *conn, const struct link_msg *m)
|
|
{
|
|
static char xml[8192]; /* static keeps the stack small in PID 1 */
|
|
struct xbuf x = { .buf = xml, .cap = sizeof(xml) };
|
|
struct link_object *o;
|
|
const char *path = m->path;
|
|
|
|
xprintf(&x,
|
|
"<!DOCTYPE node PUBLIC \"-//freedesktop//DTD D-BUS Object Introspection 1.0//EN\"\n"
|
|
" \"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd\">\n"
|
|
"<node>\n");
|
|
|
|
xprintf(&x, "%s", STANDARD_INTERFACES_XML);
|
|
if (!strcmp(path, "/org/freedesktop/DBus"))
|
|
xprintf(&x, "%s", DBUS_DRIVER_XML);
|
|
|
|
o = NULL;
|
|
{
|
|
struct link_object *p;
|
|
|
|
TAILQ_FOREACH(p, &conn->server->objects, link) {
|
|
if (strcmp(p->path, path) == 0) {
|
|
o = p;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (o) {
|
|
struct link_vtable_entry *e;
|
|
const link_method_t *meth;
|
|
|
|
TAILQ_FOREACH(e, &o->vtables, link) {
|
|
const link_property_t *prop;
|
|
const link_signal_t *sig;
|
|
|
|
xprintf(&x, " <interface name=\"%s\">\n",
|
|
e->vt->interface);
|
|
if (e->vt->methods)
|
|
for (meth = e->vt->methods; meth->name; meth++)
|
|
emit_method(&x, meth);
|
|
if (e->vt->properties)
|
|
for (prop = e->vt->properties; prop->name; prop++)
|
|
emit_property(&x, prop);
|
|
if (e->vt->signals)
|
|
for (sig = e->vt->signals; sig->name; sig++)
|
|
emit_signal(&x, sig);
|
|
xprintf(&x, " </interface>\n");
|
|
}
|
|
}
|
|
|
|
{
|
|
struct link_object *p;
|
|
char prev_seg[LINK_PATH_MAX] = { 0 };
|
|
char seg [LINK_PATH_MAX];
|
|
|
|
TAILQ_FOREACH(p, &conn->server->objects, link) {
|
|
if (!child_segment(path, p->path, seg, sizeof(seg)))
|
|
continue;
|
|
if (strcmp(prev_seg, seg) == 0)
|
|
continue;
|
|
xprintf(&x, " <node name=\"%s\"/>\n", seg);
|
|
memcpy(prev_seg, seg, sizeof(prev_seg));
|
|
}
|
|
}
|
|
|
|
xprintf(&x, "</node>\n");
|
|
|
|
if (x.err)
|
|
return __send_error(conn, m,
|
|
"org.freedesktop.DBus.Error.Failed",
|
|
"Introspection XML overflow");
|
|
|
|
return send_string_reply(conn, m, xml);
|
|
}
|
|
|
|
/* ---------- Properties.Get / GetAll ---------- */
|
|
|
|
/* Find the (object, vtable-entry) pair matching `path` and `interface`.
|
|
* Returns NULL if the path is unknown or the interface isn't exposed
|
|
* on it. */
|
|
static struct link_vtable_entry *
|
|
find_vtable(link_connection_t *conn, const char *path, const char *interface)
|
|
{
|
|
struct link_object *o;
|
|
|
|
if (!path || !interface)
|
|
return NULL;
|
|
TAILQ_FOREACH(o, &conn->server->objects, link) {
|
|
struct link_vtable_entry *e;
|
|
|
|
if (strcmp(o->path, path) != 0)
|
|
continue;
|
|
TAILQ_FOREACH(e, &o->vtables, link) {
|
|
if (strcmp(e->vt->interface, interface) == 0)
|
|
return e;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static int handle_properties_get(link_connection_t *conn, const struct link_msg *m)
|
|
{
|
|
const char *iface, *prop_name;
|
|
struct link_reader r;
|
|
struct link_writer w;
|
|
struct link_vtable_entry *e;
|
|
const link_property_t *p;
|
|
ssize_t blen;
|
|
|
|
if (!m->signature || strcmp(m->signature, "ss") != 0)
|
|
return __send_error(conn, m,
|
|
"org.freedesktop.DBus.Error.InvalidArgs",
|
|
"Properties.Get takes (interface, property)");
|
|
|
|
__r_init(&r, m->body, m->body_avail);
|
|
if (__r_string(&r, &iface) < 0 || __r_string(&r, &prop_name) < 0)
|
|
return __send_error(conn, m,
|
|
"org.freedesktop.DBus.Error.InvalidArgs",
|
|
"Malformed argument");
|
|
|
|
e = find_vtable(conn, m->path, iface);
|
|
if (!e || !e->vt->properties)
|
|
return __send_error(conn, m,
|
|
"org.freedesktop.DBus.Error.UnknownInterface",
|
|
"No such interface on this object");
|
|
|
|
for (p = e->vt->properties; p->name; p++) {
|
|
if (strcmp(p->name, prop_name) != 0)
|
|
continue;
|
|
if (!p->getter)
|
|
break;
|
|
__w_init(&w, conn->txbuf, sizeof(conn->txbuf));
|
|
__w_sig(&w, p->sig ? p->sig : "s");
|
|
if (p->getter(&w, e->userdata) != 0 || (blen = __w_finish(&w)) < 0)
|
|
return __send_error(conn, m,
|
|
"org.freedesktop.DBus.Error.Failed",
|
|
"Property getter failed");
|
|
return __send_method_return(conn, m, "v",
|
|
conn->txbuf, (size_t)blen);
|
|
}
|
|
|
|
return __send_error(conn, m,
|
|
"org.freedesktop.DBus.Error.UnknownProperty",
|
|
"No such property on this interface");
|
|
}
|
|
|
|
static int handle_properties_get_all(link_connection_t *conn, const struct link_msg *m)
|
|
{
|
|
const char *iface;
|
|
struct link_reader r;
|
|
struct link_writer w;
|
|
struct link_vtable_entry *e;
|
|
const link_property_t *p;
|
|
ssize_t blen;
|
|
|
|
if (!m->signature || strcmp(m->signature, "s") != 0)
|
|
return __send_error(conn, m,
|
|
"org.freedesktop.DBus.Error.InvalidArgs",
|
|
"Properties.GetAll takes one string");
|
|
|
|
__r_init(&r, m->body, m->body_avail);
|
|
if (__r_string(&r, &iface) < 0)
|
|
return __send_error(conn, m,
|
|
"org.freedesktop.DBus.Error.InvalidArgs",
|
|
"Malformed argument");
|
|
|
|
e = find_vtable(conn, m->path, iface);
|
|
if (!e)
|
|
return __send_error(conn, m,
|
|
"org.freedesktop.DBus.Error.UnknownInterface",
|
|
"No such interface on this object");
|
|
|
|
__w_init(&w, conn->txbuf, sizeof(conn->txbuf));
|
|
__w_array_begin(&w, '{');
|
|
if (e->vt->properties) {
|
|
for (p = e->vt->properties; p->name; p++) {
|
|
if (!p->getter)
|
|
continue;
|
|
__w_struct_begin(&w);
|
|
__w_string(&w, p->name);
|
|
__w_sig(&w, p->sig ? p->sig : "s");
|
|
if (p->getter(&w, e->userdata) != 0)
|
|
return __send_error(conn, m,
|
|
"org.freedesktop.DBus.Error.Failed",
|
|
"Property getter failed");
|
|
__w_struct_end(&w);
|
|
}
|
|
}
|
|
__w_array_end(&w);
|
|
|
|
blen = __w_finish(&w);
|
|
if (blen < 0)
|
|
return __send_error(conn, m,
|
|
"org.freedesktop.DBus.Error.Failed",
|
|
"Reply too large");
|
|
|
|
return __send_method_return(conn, m, "a{sv}",
|
|
conn->txbuf, (size_t)blen);
|
|
}
|
|
|
|
/* ---------- AddMatch / RemoveMatch ---------- */
|
|
|
|
static int handle_add_match(link_connection_t *conn, const struct link_msg *m)
|
|
{
|
|
const char *rule;
|
|
struct link_reader r;
|
|
|
|
if (!m->signature || strcmp(m->signature, "s") != 0)
|
|
return __send_error(conn, m,
|
|
"org.freedesktop.DBus.Error.InvalidArgs",
|
|
"AddMatch takes a single string");
|
|
|
|
__r_init(&r, m->body, m->body_avail);
|
|
if (__r_string(&r, &rule) < 0)
|
|
return __send_error(conn, m,
|
|
"org.freedesktop.DBus.Error.InvalidArgs",
|
|
"Malformed argument");
|
|
|
|
if (__match_add(conn, rule) < 0) {
|
|
if (errno == ENOSPC)
|
|
return __send_error(conn, m,
|
|
"org.freedesktop.DBus.Error.LimitsExceeded",
|
|
"Too many active match rules");
|
|
return __send_error(conn, m,
|
|
"org.freedesktop.DBus.Error.MatchRuleInvalid",
|
|
"Unrecognised key or malformed rule");
|
|
}
|
|
return __send_method_return(conn, m, NULL, NULL, 0);
|
|
}
|
|
|
|
static int handle_remove_match(link_connection_t *conn, const struct link_msg *m)
|
|
{
|
|
const char *rule;
|
|
struct link_reader r;
|
|
|
|
if (!m->signature || strcmp(m->signature, "s") != 0)
|
|
return __send_error(conn, m,
|
|
"org.freedesktop.DBus.Error.InvalidArgs",
|
|
"RemoveMatch takes a single string");
|
|
|
|
__r_init(&r, m->body, m->body_avail);
|
|
if (__r_string(&r, &rule) < 0)
|
|
return __send_error(conn, m,
|
|
"org.freedesktop.DBus.Error.InvalidArgs",
|
|
"Malformed argument");
|
|
|
|
if (__match_remove(conn, rule) < 0)
|
|
return __send_error(conn, m,
|
|
"org.freedesktop.DBus.Error.MatchRuleNotFound",
|
|
"No such match rule on this connection");
|
|
|
|
return __send_method_return(conn, m, NULL, NULL, 0);
|
|
}
|
|
|
|
/* ---------- entry point ---------- */
|
|
|
|
/* Returns 0 when a built-in handled the call and its reply went out,
|
|
* -1 when a built-in handled it but the send failed (the caller must
|
|
* drop the connection, its stream is now half-written), and 1 when
|
|
* the member is not a built-in and the caller should try the object
|
|
* tree. The three are distinct so a failed send is never mistaken
|
|
* for "not mine". */
|
|
int __handle_builtin(link_connection_t *conn, const struct link_msg *m)
|
|
{
|
|
/* Hello and AddMatch/RemoveMatch are per-connection state, and on
|
|
* a broker link the connection is shared by every caller: one
|
|
* sender could exhaust the match cap or drop another's rule.
|
|
* Naming and subscription belong to the broker for its own
|
|
* clients, so we do not answer these there. */
|
|
if (conn->broker && m->member &&
|
|
(!strcmp(m->member, "Hello") ||
|
|
!strcmp(m->member, "AddMatch") ||
|
|
!strcmp(m->member, "RemoveMatch"))) {
|
|
__dbg("%s is the broker's to answer, not ours", m->member);
|
|
return __send_error(conn, m,
|
|
"org.freedesktop.DBus.Error.AccessDenied",
|
|
"Handled by the message bus, not by this peer");
|
|
}
|
|
|
|
if (member_is(m, "org.freedesktop.DBus", "Hello") &&
|
|
m->path && strcmp(m->path, "/org/freedesktop/DBus") == 0)
|
|
return handle_hello(conn, m);
|
|
|
|
if (member_is(m, "org.freedesktop.DBus", "AddMatch") &&
|
|
m->path && strcmp(m->path, "/org/freedesktop/DBus") == 0)
|
|
return handle_add_match(conn, m);
|
|
|
|
if (member_is(m, "org.freedesktop.DBus", "RemoveMatch") &&
|
|
m->path && strcmp(m->path, "/org/freedesktop/DBus") == 0)
|
|
return handle_remove_match(conn, m);
|
|
|
|
if (member_is(m, "org.freedesktop.DBus.Peer", "Ping"))
|
|
return handle_ping(conn, m);
|
|
|
|
if (member_is(m, "org.freedesktop.DBus.Peer", "GetMachineId"))
|
|
return handle_get_machine_id(conn, m);
|
|
|
|
if (member_is(m, "org.freedesktop.DBus.Introspectable", "Introspect"))
|
|
return handle_introspect(conn, m);
|
|
|
|
if (member_is(m, "org.freedesktop.DBus.Properties", "Get"))
|
|
return handle_properties_get(conn, m);
|
|
|
|
if (member_is(m, "org.freedesktop.DBus.Properties", "GetAll"))
|
|
return handle_properties_get_all(conn, m);
|
|
|
|
return 1; /* not a built-in, let the caller try the object tree */
|
|
}
|