mirror of
https://github.com/troglobit/finit.git
synced 2026-11-06 15:51:03 +00:00
libink: a brokerless D-Bus implementation for Finit
Finit had no way to answer the question every service manager gets asked: what is running, and change it. D-Bus is how the rest of userspace asks, but linking libdbus, sd-bus or GIO into PID 1 buys a dependency, an allocator and a main loop we do not control. So libink: the wire format, an object tree, and a bus of Finit's own at /run/finit/bus, gated like INIT_SOCKET. It speaks the standard org.freedesktop.DBus, .Peer, .Introspectable interfaces, and Finit's own Manager1, Service1 and Cond1 on top. Methods that change something are marked privileged and answered only for a caller the kernel vouched for, via SO_PEERCRED. Server and client both, since initctl is the first thing that needs to talk to it, and its Start/Stop/Restart/Reload now go over the bus rather than the legacy socket. Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
1 parent
ae9a24f44f
commit
0a269f3298
39 files changed
+5530
-49
No files matched your search
@@ -0,0 +1,8 @@
|
||||
.deps/*
|
||||
.libs/*
|
||||
.dirstamp
|
||||
*.lo
|
||||
libink.*
|
||||
!libink.pc.in
|
||||
Makefile
|
||||
Makefile.in
|
||||
@@ -0,0 +1,22 @@
|
||||
# libink — brokerless D-Bus library (server + client), born inside Finit
|
||||
lib_LTLIBRARIES = libink.la
|
||||
libink_la_SOURCES = server.c auth.c connection.c \
|
||||
proto.c proto.h \
|
||||
marshal.c marshal.h \
|
||||
dispatch.c builtin.c \
|
||||
match.c \
|
||||
path.c \
|
||||
client.c io.c \
|
||||
internal.h
|
||||
|
||||
libink_la_LDFLAGS = -version-info 0:0:0
|
||||
libink_la_CPPFLAGS = -D_GNU_SOURCE -D_DEFAULT_SOURCE -D_BSD_SOURCE
|
||||
libink_la_CFLAGS = -W -Wall -Wextra -Wno-unused-parameter -std=gnu99
|
||||
|
||||
# pkg-config support
|
||||
pkgconfigdir = $(libdir)/pkgconfig
|
||||
pkgconfig_DATA = libink.pc
|
||||
|
||||
# Public headers install to $(includedir)/ink/
|
||||
inkdir = $(includedir)/ink
|
||||
ink_HEADERS = link.h path.h
|
||||
+300
@@ -0,0 +1,300 @@
|
||||
/* libink — D-Bus AUTH EXTERNAL handshake
|
||||
*
|
||||
* Implements the line-based SASL-style exchange described in the
|
||||
* D-Bus specification, section "Authentication Protocol". Only the
|
||||
* AUTH EXTERNAL mechanism is offered; everything else is rejected.
|
||||
*
|
||||
* The exchange:
|
||||
*
|
||||
* client --> [nul byte]
|
||||
* client --> "AUTH EXTERNAL <hex-uid>\r\n"
|
||||
* server <-- "OK <hex-guid>\r\n"
|
||||
* client --> "NEGOTIATE_UNIX_FD\r\n" [optional]
|
||||
* server <-- "ERROR <reason>\r\n" (no fd-passing yet)
|
||||
* client --> "BEGIN\r\n"
|
||||
*
|
||||
* Copyright (c) 2026 Joachim Wiberg <troglobit@gmail.com>
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/random.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "internal.h"
|
||||
|
||||
static const char rejected_ext[] = "REJECTED EXTERNAL\r\n";
|
||||
|
||||
#define write_all(fd, buf, len) __io_write_all((fd), (buf), (len))
|
||||
|
||||
/* Shared by __auth_generate_guid (server) and __auth_client
|
||||
* (client) for hex-encoding GUIDs and uid claims. */
|
||||
static const char hex_digits[] = "0123456789abcdef";
|
||||
|
||||
static int reply(int fd, const char *line)
|
||||
{
|
||||
return write_all(fd, line, strlen(line));
|
||||
}
|
||||
|
||||
static int reject(link_connection_t *conn)
|
||||
{
|
||||
return write_all(conn->fd, rejected_ext, sizeof(rejected_ext) - 1);
|
||||
}
|
||||
|
||||
void __auth_generate_guid(char out[33])
|
||||
{
|
||||
uint8_t raw[16];
|
||||
size_t i;
|
||||
|
||||
if (getrandom(raw, sizeof(raw), 0) != (ssize_t)sizeof(raw)) {
|
||||
/* Extraordinarily unlikely; GUID is informational, not a
|
||||
* security primitive — fall back to something deterministic
|
||||
* rather than uninitialized memory. */
|
||||
for (i = 0; i < sizeof(raw); i++)
|
||||
raw[i] = (uint8_t)(i ^ 0xa5);
|
||||
}
|
||||
|
||||
for (i = 0; i < sizeof(raw); i++) {
|
||||
out[i * 2] = hex_digits[raw[i] >> 4];
|
||||
out[i * 2 + 1] = hex_digits[raw[i] & 0xf];
|
||||
}
|
||||
out[32] = '\0';
|
||||
}
|
||||
|
||||
static int hexval(int c)
|
||||
{
|
||||
if (c >= '0' && c <= '9') return c - '0';
|
||||
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
||||
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Parse "AUTH EXTERNAL <hex>" payload into a uid. The argument is
|
||||
* an even-length hex string whose decoded form is a decimal uid in
|
||||
* ASCII. Returns 0 on success, -1 on malformed input. */
|
||||
static int parse_external_uid(const char *arg, size_t arglen, uid_t *out)
|
||||
{
|
||||
char decoded[24];
|
||||
char *ep = NULL;
|
||||
unsigned long v;
|
||||
size_t i, dlen;
|
||||
|
||||
if (arglen == 0 || (arglen & 1) || arglen / 2 >= sizeof(decoded))
|
||||
return -1;
|
||||
|
||||
dlen = arglen / 2;
|
||||
for (i = 0; i < dlen; i++) {
|
||||
int hi = hexval((unsigned char)arg[i * 2]);
|
||||
int lo = hexval((unsigned char)arg[i * 2 + 1]);
|
||||
|
||||
if (hi < 0 || lo < 0)
|
||||
return -1;
|
||||
decoded[i] = (char)((hi << 4) | lo);
|
||||
}
|
||||
decoded[dlen] = '\0';
|
||||
|
||||
errno = 0;
|
||||
v = strtoul(decoded, &ep, 10);
|
||||
if (errno || !ep || *ep != '\0' || v > (unsigned long)((uid_t)-1))
|
||||
return -1;
|
||||
|
||||
*out = (uid_t)v;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int handle_line(link_connection_t *conn, const char *line, size_t len)
|
||||
{
|
||||
if (len >= 14 && memcmp(line, "AUTH EXTERNAL ", 14) == 0) {
|
||||
uid_t claimed;
|
||||
char ok[64];
|
||||
|
||||
if (parse_external_uid(line + 14, len - 14, &claimed) < 0)
|
||||
return reject(conn);
|
||||
if (conn->peer_uid == (uid_t)-1 || claimed != conn->peer_uid)
|
||||
return reject(conn);
|
||||
|
||||
snprintf(ok, sizeof(ok), "OK %s\r\n", conn->guid);
|
||||
return reply(conn->fd, ok);
|
||||
}
|
||||
|
||||
if (len == 4 && memcmp(line, "AUTH", 4) == 0)
|
||||
return reject(conn);
|
||||
|
||||
if (len == 17 && memcmp(line, "NEGOTIATE_UNIX_FD", 17) == 0)
|
||||
return reply(conn->fd, "ERROR fd-passing not supported\r\n");
|
||||
|
||||
if (len == 5 && memcmp(line, "BEGIN", 5) == 0) {
|
||||
conn->auth = LINK_AUTH_DONE;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (len == 6 && memcmp(line, "CANCEL", 6) == 0)
|
||||
return reject(conn);
|
||||
|
||||
if (len >= 5 && memcmp(line, "ERROR", 5) == 0)
|
||||
return reject(conn);
|
||||
|
||||
return reply(conn->fd, "ERROR Unknown command\r\n");
|
||||
}
|
||||
|
||||
/* Pull one CR+LF-terminated line out of conn->linebuf. Returns the
|
||||
* line length (without the CR+LF), or 0 if no complete line is
|
||||
* present yet. Consumes the line on success. */
|
||||
static size_t take_line(link_connection_t *conn, char *out, size_t outsz)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
for (i = 0; i + 1 < conn->linelen; i++) {
|
||||
if (conn->linebuf[i] == '\r' && conn->linebuf[i + 1] == '\n') {
|
||||
size_t linelen = i;
|
||||
size_t consumed = i + 2;
|
||||
|
||||
if (linelen >= outsz)
|
||||
linelen = outsz - 1;
|
||||
|
||||
memcpy(out, conn->linebuf, linelen);
|
||||
out[linelen] = '\0';
|
||||
|
||||
memmove(conn->linebuf, conn->linebuf + consumed,
|
||||
conn->linelen - consumed);
|
||||
conn->linelen -= consumed;
|
||||
return linelen;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __auth_process(link_connection_t *conn)
|
||||
{
|
||||
uint8_t buf[256];
|
||||
ssize_t n;
|
||||
size_t off = 0;
|
||||
|
||||
n = read(conn->fd, buf, sizeof(buf));
|
||||
if (n == 0)
|
||||
return -1; /* peer closed */
|
||||
if (n < 0) {
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)
|
||||
return 0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (conn->auth == LINK_AUTH_NUL) {
|
||||
if (buf[0] != 0x00) {
|
||||
conn->auth = LINK_AUTH_FAILED;
|
||||
return -1;
|
||||
}
|
||||
off = 1;
|
||||
conn->auth = LINK_AUTH_LINE;
|
||||
}
|
||||
|
||||
if (conn->auth == LINK_AUTH_LINE) {
|
||||
size_t take = (size_t)n - off;
|
||||
char line[LINK_AUTH_LINEBUF_SIZE];
|
||||
size_t linelen;
|
||||
|
||||
if (conn->linelen + take > sizeof(conn->linebuf)) {
|
||||
conn->auth = LINK_AUTH_FAILED;
|
||||
return -1;
|
||||
}
|
||||
memcpy(conn->linebuf + conn->linelen, buf + off, take);
|
||||
conn->linelen += take;
|
||||
|
||||
while ((linelen = take_line(conn, line, sizeof(line))) > 0) {
|
||||
if (handle_line(conn, line, linelen) < 0)
|
||||
return -1;
|
||||
if (conn->auth != LINK_AUTH_LINE)
|
||||
break;
|
||||
}
|
||||
|
||||
/* If BEGIN flipped us to DONE, any remaining linebuf bytes
|
||||
* are the first bytes of the binary D-Bus stream — move
|
||||
* them to rxbuf so the dispatcher can pick them up on the
|
||||
* next process() call. */
|
||||
if (conn->auth == LINK_AUTH_DONE && conn->linelen > 0) {
|
||||
if (conn->linelen > sizeof(conn->rxbuf))
|
||||
return -1;
|
||||
memcpy(conn->rxbuf, conn->linebuf, conn->linelen);
|
||||
conn->rxlen = conn->linelen;
|
||||
conn->linelen = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---- client-side SASL composer ---- */
|
||||
|
||||
/* Read a single CR+LF (or just LF) terminated line from fd into buf.
|
||||
* Returns the line length (without the terminator), or -1 on EOF or
|
||||
* buffer overflow. Blocks until a complete line arrives.
|
||||
*
|
||||
* Used only by __auth_client; the server-side parser does its
|
||||
* own line extraction out of conn->linebuf. */
|
||||
static ssize_t client_read_line(int fd, char *buf, size_t bufsz)
|
||||
{
|
||||
size_t off = 0;
|
||||
|
||||
while (off + 1 < bufsz) {
|
||||
ssize_t n = read(fd, buf + off, 1);
|
||||
|
||||
if (n == 0)
|
||||
return -1;
|
||||
if (n < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
return -1;
|
||||
}
|
||||
if (buf[off] == '\n') {
|
||||
buf[off] = '\0';
|
||||
if (off > 0 && buf[off - 1] == '\r')
|
||||
buf[--off] = '\0';
|
||||
return (ssize_t)off;
|
||||
}
|
||||
off++;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int __auth_client(int fd, uid_t uid)
|
||||
{
|
||||
|
||||
char uidstr[16];
|
||||
char hexuid[32];
|
||||
char line[64];
|
||||
char reply_line[256];
|
||||
size_t i, n;
|
||||
int rc;
|
||||
|
||||
if (write_all(fd, "\0", 1) < 0)
|
||||
return -1;
|
||||
|
||||
n = (size_t)snprintf(uidstr, sizeof(uidstr), "%u", (unsigned)uid);
|
||||
if (n * 2 >= sizeof(hexuid))
|
||||
return -1;
|
||||
for (i = 0; i < n; i++) {
|
||||
unsigned c = (unsigned char)uidstr[i];
|
||||
|
||||
hexuid[i * 2] = hex_digits[c >> 4];
|
||||
hexuid[i * 2 + 1] = hex_digits[c & 0xf];
|
||||
}
|
||||
hexuid[n * 2] = '\0';
|
||||
|
||||
rc = snprintf(line, sizeof(line), "AUTH EXTERNAL %s\r\n", hexuid);
|
||||
if (rc < 0 || (size_t)rc >= sizeof(line))
|
||||
return -1;
|
||||
if (write_all(fd, line, (size_t)rc) < 0)
|
||||
return -1;
|
||||
|
||||
if (client_read_line(fd, reply_line, sizeof(reply_line)) < 0)
|
||||
return -1;
|
||||
if (strncmp(reply_line, "OK ", 3) != 0)
|
||||
return -1;
|
||||
|
||||
if (write_all(fd, "BEGIN\r\n", 7) < 0)
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/* Emit a single <method> stanza for one method definition. */
|
||||
static void emit_method(struct xbuf *x, const link_method_t *m)
|
||||
{
|
||||
const char *p;
|
||||
|
||||
xprintf(x, " <method name=\"%s\">\n", m->name);
|
||||
for (p = m->in_sig ? m->in_sig : ""; *p; p++)
|
||||
xprintf(x, " <arg type=\"%c\" direction=\"in\"/>\n", *p);
|
||||
for (p = m->out_sig ? m->out_sig : ""; *p; p++)
|
||||
xprintf(x, " <arg type=\"%c\" direction=\"out\"/>\n", *p);
|
||||
xprintf(x, " </method>\n");
|
||||
}
|
||||
|
||||
/* Introspection limitation: emit_method prints one <arg> per
|
||||
* character of the signature, which is wrong for compound types
|
||||
* (an "a(ss)" arg appears as four args). Good enough for the
|
||||
* "s", "u", "as" signatures we expose today; replace with a
|
||||
* signature parser when the first compound argument lands. */
|
||||
|
||||
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";
|
||||
|
||||
/* 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);
|
||||
|
||||
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) {
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
/* ---------- 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 ---------- */
|
||||
|
||||
int __handle_builtin(link_connection_t *conn, const struct link_msg *m)
|
||||
{
|
||||
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);
|
||||
|
||||
return -1; /* not a built-in */
|
||||
}
|
||||
+295
@@ -0,0 +1,295 @@
|
||||
/* libink — synchronous client-side D-Bus calls.
|
||||
*
|
||||
* Pairs with server.c / connection.c on the receiving end. The
|
||||
* intent is for short-lived CLI tools (initctl) and tests to use
|
||||
* libink as their D-Bus client rather than reimplementing the
|
||||
* wire format.
|
||||
*
|
||||
* Copyright (c) 2026 Joachim Wiberg <troglobit@gmail.com>
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#include <errno.h>
|
||||
#include <poll.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "internal.h"
|
||||
|
||||
struct link_client {
|
||||
int fd;
|
||||
uint32_t next_serial;
|
||||
link_reply_t reply; /* most recent reply view (points into rxbuf) */
|
||||
/* Distinct from "reply.type == 0": LINK_MSG_INVALID is 0, which
|
||||
* is a wire-valid (if malformed) type, so we need an out-of-band
|
||||
* "have we ever produced a reply?" flag. */
|
||||
int have_reply;
|
||||
/* Re-use the server-side rx buffer size for incoming replies.
|
||||
* Replies to our methods are bounded by the same per-message
|
||||
* sanity cap as everything else. */
|
||||
uint8_t rxbuf[LINK_RX_BUF_SIZE];
|
||||
size_t rxlen;
|
||||
};
|
||||
|
||||
link_client_t *link_client_open(const char *path)
|
||||
{
|
||||
struct sockaddr_un sun = { .sun_family = AF_UNIX };
|
||||
link_client_t *c;
|
||||
int fd;
|
||||
|
||||
if (!path || strlen(path) >= sizeof(sun.sun_path))
|
||||
return NULL;
|
||||
memcpy(sun.sun_path, path, strlen(path) + 1);
|
||||
|
||||
fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (fd < 0)
|
||||
return NULL;
|
||||
if (connect(fd, (struct sockaddr *)&sun, sizeof(sun)) < 0) {
|
||||
close(fd);
|
||||
return NULL;
|
||||
}
|
||||
if (__auth_client(fd, geteuid()) < 0) {
|
||||
close(fd);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
c = calloc(1, sizeof(*c));
|
||||
if (!c) {
|
||||
close(fd);
|
||||
return NULL;
|
||||
}
|
||||
c->fd = fd;
|
||||
c->next_serial = 1;
|
||||
return c;
|
||||
}
|
||||
|
||||
void link_client_close(link_client_t *c)
|
||||
{
|
||||
if (!c)
|
||||
return;
|
||||
if (c->fd >= 0)
|
||||
close(c->fd);
|
||||
free(c);
|
||||
}
|
||||
|
||||
/* read_full / send_all live in libink/io.c. */
|
||||
#define read_full(fd, buf, len) __io_read_full ((fd), (buf), (len))
|
||||
#define send_all(fd, buf, len) __io_write_all((fd), (buf), (len))
|
||||
|
||||
#define ALIGN_UP(x, n) (((x) + (n) - 1) & ~((size_t)((n) - 1)))
|
||||
|
||||
/* Read one complete D-Bus message: the 16-byte fixed header tells
|
||||
* us fields_len + body_len, so we then issue exactly one more read
|
||||
* for the remainder. Both lengths are bounded against rxbuf before
|
||||
* arithmetic so a malformed wire u32 can't wrap into a near-4-GiB
|
||||
* read. */
|
||||
static int read_one(link_client_t *c, struct link_msg *msg)
|
||||
{
|
||||
uint32_t body_len, fields_len, body_off, total;
|
||||
ssize_t consumed;
|
||||
|
||||
memset(msg, 0, sizeof(*msg));
|
||||
|
||||
if (read_full(c->fd, c->rxbuf, 16) < 0)
|
||||
return -1;
|
||||
if (c->rxbuf[0] != 'l')
|
||||
return -1;
|
||||
|
||||
body_len = (uint32_t)c->rxbuf[4]
|
||||
| ((uint32_t)c->rxbuf[5] << 8)
|
||||
| ((uint32_t)c->rxbuf[6] << 16)
|
||||
| ((uint32_t)c->rxbuf[7] << 24);
|
||||
fields_len = (uint32_t)c->rxbuf[12]
|
||||
| ((uint32_t)c->rxbuf[13] << 8)
|
||||
| ((uint32_t)c->rxbuf[14] << 16)
|
||||
| ((uint32_t)c->rxbuf[15] << 24);
|
||||
|
||||
/* Bound the wire-supplied lengths before any arithmetic on
|
||||
* them. Without this, fields_len = 0xFFFFFFF0 would wrap
|
||||
* 16u + fields_len to near zero, bypass the total < rxbuf
|
||||
* check, and trigger an out-of-bounds read. */
|
||||
if (fields_len > sizeof(c->rxbuf) || body_len > sizeof(c->rxbuf))
|
||||
return -1;
|
||||
|
||||
body_off = (uint32_t)ALIGN_UP(16u + fields_len, 8u);
|
||||
total = body_off + body_len;
|
||||
if (total > sizeof(c->rxbuf) || total < 16)
|
||||
return -1;
|
||||
|
||||
if (read_full(c->fd, c->rxbuf + 16, total - 16) < 0)
|
||||
return -1;
|
||||
c->rxlen = total;
|
||||
|
||||
consumed = __msg_parse(c->rxbuf, c->rxlen, msg);
|
||||
if (consumed <= 0)
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void publish_reply(link_client_t *c, const struct link_msg *m)
|
||||
{
|
||||
c->reply.type = m->type;
|
||||
c->reply.signature = m->signature;
|
||||
c->reply.error_name = m->error_name;
|
||||
c->reply.path = m->path;
|
||||
c->reply.interface = m->interface;
|
||||
c->reply.member = m->member;
|
||||
c->reply.body = m->body_avail ? m->body : NULL;
|
||||
c->reply.body_len = m->body_avail;
|
||||
c->have_reply = 1;
|
||||
}
|
||||
|
||||
/* The reply view in c->reply points into c->rxbuf and is invalidated
|
||||
* the moment we touch that buffer again -- clear it at every entry,
|
||||
* even on the bad-args path, so link_client_reply() cannot return
|
||||
* stale dangling pointers from a previous call. */
|
||||
static void clear_reply(link_client_t *c)
|
||||
{
|
||||
if (!c)
|
||||
return;
|
||||
memset(&c->reply, 0, sizeof(c->reply));
|
||||
c->have_reply = 0;
|
||||
}
|
||||
|
||||
/* Wait up to timeout_ms (-1 = forever) for one full inbound frame
|
||||
* and publish it. Returns 0 on success, 1 on timeout, -1 on error. */
|
||||
static int read_and_publish(link_client_t *c, int timeout_ms)
|
||||
{
|
||||
struct link_msg msg;
|
||||
|
||||
if (timeout_ms >= 0) {
|
||||
struct pollfd pfd = { .fd = c->fd, .events = POLLIN };
|
||||
int rc;
|
||||
|
||||
do {
|
||||
rc = poll(&pfd, 1, timeout_ms);
|
||||
} while (rc < 0 && errno == EINTR);
|
||||
if (rc < 0)
|
||||
return -1;
|
||||
if (rc == 0)
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (read_one(c, &msg) < 0)
|
||||
return -1;
|
||||
publish_reply(c, &msg);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int link_client_call(link_client_t *c,
|
||||
const char *obj_path,
|
||||
const char *interface,
|
||||
const char *member,
|
||||
const char *signature,
|
||||
const uint8_t *body, size_t body_len)
|
||||
{
|
||||
/* Generous: Manager1 headers fit in ~150 B, but the buffer is
|
||||
* shared with whatever future callers throw at us, and an
|
||||
* overflow only manifests as a silent LINK_CALL_FAIL via
|
||||
* __msg_build_method_call returning -1. 1 KiB on stack
|
||||
* is cheap insurance. */
|
||||
uint8_t hdr[1024];
|
||||
ssize_t hlen;
|
||||
uint32_t serial;
|
||||
|
||||
clear_reply(c);
|
||||
if (!c || c->fd < 0 || !obj_path || !member)
|
||||
return LINK_CALL_FAIL;
|
||||
|
||||
serial = c->next_serial++;
|
||||
hlen = __msg_build_method_call(hdr, sizeof(hdr), serial,
|
||||
obj_path, interface, member,
|
||||
signature, (uint32_t)body_len);
|
||||
if (hlen < 0)
|
||||
return LINK_CALL_FAIL;
|
||||
|
||||
if (send_all(c->fd, hdr, (size_t)hlen) < 0)
|
||||
return LINK_CALL_FAIL;
|
||||
if (body_len > 0 && send_all(c->fd, body, body_len) < 0)
|
||||
return LINK_CALL_FAIL;
|
||||
|
||||
if (read_and_publish(c, -1) != 0)
|
||||
return LINK_CALL_FAIL;
|
||||
|
||||
if (c->reply.type == LINK_MSG_METHOD_RETURN)
|
||||
return LINK_CALL_OK;
|
||||
if (c->reply.type == LINK_MSG_ERROR)
|
||||
return LINK_CALL_ERROR;
|
||||
return LINK_CALL_FAIL;
|
||||
}
|
||||
|
||||
const link_reply_t *link_client_reply(link_client_t *c)
|
||||
{
|
||||
if (!c || !c->have_reply)
|
||||
return NULL;
|
||||
return &c->reply;
|
||||
}
|
||||
|
||||
/* Marshal varargs into `body` (capacity `cap`) according to `sig`.
|
||||
* Returns the marshalled length on success, -1 on overflow or
|
||||
* unsupported type code. */
|
||||
static ssize_t marshal_va(uint8_t *body, size_t cap,
|
||||
const char *sig, va_list ap)
|
||||
{
|
||||
link_writer_t w;
|
||||
const char *s;
|
||||
|
||||
link_writer_init(&w, body, cap);
|
||||
for (s = sig; *s; s++) {
|
||||
switch (*s) {
|
||||
case 'y':
|
||||
link_w_byte(&w, (uint8_t)va_arg(ap, int));
|
||||
break;
|
||||
case 'b':
|
||||
link_w_bool(&w, va_arg(ap, int));
|
||||
break;
|
||||
case 'u':
|
||||
link_w_u32(&w, va_arg(ap, uint32_t));
|
||||
break;
|
||||
case 's':
|
||||
link_w_string(&w, va_arg(ap, const char *));
|
||||
break;
|
||||
case 'o':
|
||||
link_w_path(&w, va_arg(ap, const char *));
|
||||
break;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return link_writer_finish(&w);
|
||||
}
|
||||
|
||||
int link_client_call_v(link_client_t *c,
|
||||
const char *obj_path,
|
||||
const char *interface,
|
||||
const char *member,
|
||||
const char *signature, ...)
|
||||
{
|
||||
uint8_t body[1024];
|
||||
ssize_t body_len = 0;
|
||||
|
||||
if (signature && *signature) {
|
||||
va_list ap;
|
||||
|
||||
va_start(ap, signature);
|
||||
body_len = marshal_va(body, sizeof(body), signature, ap);
|
||||
va_end(ap);
|
||||
if (body_len < 0)
|
||||
return LINK_CALL_FAIL;
|
||||
}
|
||||
|
||||
return link_client_call(c, obj_path, interface, member,
|
||||
signature, body, (size_t)body_len);
|
||||
}
|
||||
|
||||
int link_client_wait(link_client_t *c, int timeout_ms)
|
||||
{
|
||||
clear_reply(c);
|
||||
if (!c || c->fd < 0)
|
||||
return -1;
|
||||
return read_and_publish(c, timeout_ms);
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/* libink — per-connection lifecycle and dispatch entry point
|
||||
*
|
||||
* Copyright (c) 2026 Joachim Wiberg <troglobit@gmail.com>
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "internal.h"
|
||||
|
||||
int link_connection_get_fd(const link_connection_t *conn)
|
||||
{
|
||||
return conn ? conn->fd : -1;
|
||||
}
|
||||
|
||||
uid_t link_connection_get_uid(const link_connection_t *conn)
|
||||
{
|
||||
return conn ? conn->peer_uid : (uid_t)-1;
|
||||
}
|
||||
|
||||
void link_connection_close(link_connection_t *conn)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
if (!conn)
|
||||
return;
|
||||
|
||||
for (i = 0; i < conn->matches_count; i++)
|
||||
__match_free(conn->matches[i]);
|
||||
|
||||
if (conn->fd >= 0)
|
||||
close(conn->fd);
|
||||
free(conn);
|
||||
}
|
||||
|
||||
/* Process buffered binary D-Bus messages, dispatching each complete
|
||||
* message and shifting consumed bytes out of rxbuf. Returns -1 if
|
||||
* we should drop the connection (peer closed, protocol error,
|
||||
* downstream send failure). */
|
||||
static int process_binary(link_connection_t *conn)
|
||||
{
|
||||
while (conn->rxlen > 0) {
|
||||
struct link_msg msg;
|
||||
ssize_t consumed;
|
||||
|
||||
consumed = __msg_parse(conn->rxbuf, conn->rxlen, &msg);
|
||||
if (consumed == 0)
|
||||
break; /* incomplete; wait for more bytes */
|
||||
if (consumed < 0)
|
||||
return -1;
|
||||
|
||||
if (__dispatch_message(conn, &msg) < 0)
|
||||
return -1;
|
||||
|
||||
memmove(conn->rxbuf, conn->rxbuf + consumed,
|
||||
conn->rxlen - (size_t)consumed);
|
||||
conn->rxlen -= (size_t)consumed;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int link_connection_process(link_connection_t *conn)
|
||||
{
|
||||
if (!conn) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (conn->auth == LINK_AUTH_FAILED)
|
||||
return -1;
|
||||
|
||||
if (conn->auth != LINK_AUTH_DONE) {
|
||||
if (__auth_process(conn) < 0)
|
||||
return -1;
|
||||
|
||||
/* Still in SASL phase — wait for more bytes. */
|
||||
if (conn->auth != LINK_AUTH_DONE)
|
||||
return 0;
|
||||
|
||||
/* Fall through: BEGIN may have arrived in the same read
|
||||
* as the first binary message. auth_process moved those
|
||||
* bytes into rxbuf; they must be dispatched now, because
|
||||
* no further wake-up is guaranteed (the kernel has
|
||||
* already delivered everything that was readable). */
|
||||
if (process_binary(conn) < 0)
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Read additional bytes and dispatch any complete messages.
|
||||
* process_binary is called inside the loop after every
|
||||
* successful read; no second call after EAGAIN because the
|
||||
* buffer hasn't changed. */
|
||||
for (;;) {
|
||||
ssize_t n;
|
||||
size_t room = sizeof(conn->rxbuf) - conn->rxlen;
|
||||
|
||||
if (room == 0) {
|
||||
errno = E2BIG;
|
||||
return -1;
|
||||
}
|
||||
|
||||
n = read(conn->fd, conn->rxbuf + conn->rxlen, room);
|
||||
if (n == 0)
|
||||
return -1; /* peer closed */
|
||||
if (n < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK)
|
||||
return 0;
|
||||
return -1;
|
||||
}
|
||||
conn->rxlen += (size_t)n;
|
||||
if (process_binary(conn) < 0)
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,417 @@
|
||||
/* libink — object tree, vtable registration, and method dispatch.
|
||||
*
|
||||
* Copyright (c) 2026 Joachim Wiberg <troglobit@gmail.com>
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "internal.h"
|
||||
|
||||
/* ---------- object/vtable registration ---------- */
|
||||
|
||||
static struct link_object *find_object(link_server_t *srv, const char *path)
|
||||
{
|
||||
struct link_object *o;
|
||||
|
||||
TAILQ_FOREACH(o, &srv->objects, link)
|
||||
if (strcmp(o->path, path) == 0)
|
||||
return o;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int link_server_remove_object(link_server_t *srv, const char *path)
|
||||
{
|
||||
struct link_object *o;
|
||||
struct link_vtable_entry *e;
|
||||
|
||||
if (!srv || !path) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
o = find_object(srv, path);
|
||||
if (!o) {
|
||||
errno = ENOENT;
|
||||
return -1;
|
||||
}
|
||||
|
||||
while ((e = TAILQ_FIRST(&o->vtables))) {
|
||||
TAILQ_REMOVE(&o->vtables, e, link);
|
||||
free(e);
|
||||
}
|
||||
TAILQ_REMOVE(&srv->objects, o, link);
|
||||
free(o);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int link_server_add_object(link_server_t *srv, const char *path,
|
||||
const link_vtable_t *vt, void *userdata)
|
||||
{
|
||||
struct link_object *o;
|
||||
struct link_vtable_entry *e;
|
||||
size_t plen;
|
||||
|
||||
if (!srv || !path || !*path || !vt || !vt->interface) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
plen = strlen(path);
|
||||
if (plen >= LINK_PATH_MAX) {
|
||||
errno = ENAMETOOLONG;
|
||||
return -1;
|
||||
}
|
||||
|
||||
o = find_object(srv, path);
|
||||
if (!o) {
|
||||
o = calloc(1, sizeof(*o));
|
||||
if (!o)
|
||||
return -1;
|
||||
memcpy(o->path, path, plen + 1);
|
||||
TAILQ_INIT(&o->vtables);
|
||||
TAILQ_INSERT_TAIL(&srv->objects, o, link);
|
||||
}
|
||||
|
||||
e = calloc(1, sizeof(*e));
|
||||
if (!e)
|
||||
return -1;
|
||||
e->vt = vt;
|
||||
e->userdata = userdata;
|
||||
TAILQ_INSERT_TAIL(&o->vtables, e, link);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---------- lookup ---------- */
|
||||
|
||||
static const link_method_t *find_method(const link_vtable_t *vt, const char *name)
|
||||
{
|
||||
const link_method_t *m;
|
||||
|
||||
if (!vt->methods)
|
||||
return NULL;
|
||||
for (m = vt->methods; m->name; m++)
|
||||
if (strcmp(m->name, name) == 0)
|
||||
return m;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* If incoming.interface is NULL, search every interface on the
|
||||
* object for a member with this name. Returns the matching method
|
||||
* and writes back its vtable_entry in *out_e. */
|
||||
static const link_method_t *resolve(struct link_object *o,
|
||||
const char *iface, const char *member,
|
||||
struct link_vtable_entry **out_e)
|
||||
{
|
||||
struct link_vtable_entry *e;
|
||||
const link_method_t *m;
|
||||
|
||||
if (iface) {
|
||||
TAILQ_FOREACH(e, &o->vtables, link) {
|
||||
if (strcmp(e->vt->interface, iface) != 0)
|
||||
continue;
|
||||
m = find_method(e->vt, member);
|
||||
if (m) {
|
||||
*out_e = e;
|
||||
return m;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
TAILQ_FOREACH(e, &o->vtables, link) {
|
||||
m = find_method(e->vt, member);
|
||||
if (m) {
|
||||
*out_e = e;
|
||||
return m;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* ---------- send helpers ---------- */
|
||||
|
||||
/*
|
||||
* Peer fds are non-blocking, so send_all may fail mid-frame (e.g.
|
||||
* EAGAIN from a peer that stopped draining its socket). Any failure
|
||||
* poisons the peer's stream: never retry on the same connection,
|
||||
* drop the peer.
|
||||
*/
|
||||
#define send_all(fd, buf, len) __io_write_all((fd), (buf), (len))
|
||||
|
||||
int __send_method_return(link_connection_t *conn, const struct link_msg *req,
|
||||
const char *out_sig,
|
||||
const uint8_t *body, size_t body_len)
|
||||
{
|
||||
uint8_t hdr[512];
|
||||
ssize_t hlen;
|
||||
uint32_t serial = ++conn->next_serial;
|
||||
|
||||
hlen = __msg_build_return(hdr, sizeof(hdr), serial,
|
||||
req->serial,
|
||||
req->sender,
|
||||
out_sig, (uint32_t)body_len);
|
||||
if (hlen < 0) {
|
||||
errno = EMSGSIZE;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (send_all(conn->fd, hdr, (size_t)hlen) < 0)
|
||||
return -1;
|
||||
if (body_len > 0 && send_all(conn->fd, body, body_len) < 0)
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int link_connection_emit_signal(link_connection_t *conn,
|
||||
const char *path,
|
||||
const char *interface,
|
||||
const char *member,
|
||||
const char *signature,
|
||||
const uint8_t *body, size_t body_len)
|
||||
{
|
||||
uint8_t hdr[512];
|
||||
ssize_t hlen;
|
||||
uint32_t serial;
|
||||
size_t i;
|
||||
int matched = 0;
|
||||
|
||||
if (!conn || !path || !interface || !member) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
if (conn->auth != LINK_AUTH_DONE)
|
||||
return 0; /* peer hasn't finished the SASL phase */
|
||||
|
||||
for (i = 0; i < conn->matches_count; i++) {
|
||||
if (__match_matches(conn->matches[i], path,
|
||||
interface, member)) {
|
||||
matched = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!matched)
|
||||
return 0; /* peer didn't subscribe — nothing to do */
|
||||
|
||||
serial = ++conn->next_serial;
|
||||
hlen = __msg_build_signal(hdr, sizeof(hdr), serial,
|
||||
path, interface, member,
|
||||
signature, (uint32_t)body_len);
|
||||
if (hlen < 0) {
|
||||
errno = EMSGSIZE;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (send_all(conn->fd, hdr, (size_t)hlen) < 0)
|
||||
return -1;
|
||||
if (body_len > 0 && send_all(conn->fd, body, body_len) < 0)
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __send_error(link_connection_t *conn, const struct link_msg *req,
|
||||
const char *error_name, const char *text)
|
||||
{
|
||||
uint8_t hdr[512];
|
||||
uint8_t body[256];
|
||||
ssize_t hlen;
|
||||
size_t blen = 0;
|
||||
uint32_t serial = ++conn->next_serial;
|
||||
const char *sig = NULL;
|
||||
|
||||
if (text && *text) {
|
||||
struct link_writer w;
|
||||
ssize_t n;
|
||||
|
||||
__w_init(&w, body, sizeof(body));
|
||||
__w_string(&w, text);
|
||||
n = __w_finish(&w);
|
||||
if (n < 0) {
|
||||
errno = EMSGSIZE;
|
||||
return -1;
|
||||
}
|
||||
blen = (size_t)n;
|
||||
sig = "s";
|
||||
}
|
||||
|
||||
hlen = __msg_build_error(hdr, sizeof(hdr), serial,
|
||||
req->serial, req->sender,
|
||||
error_name, sig, (uint32_t)blen);
|
||||
if (hlen < 0) {
|
||||
errno = EMSGSIZE;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (send_all(conn->fd, hdr, (size_t)hlen) < 0)
|
||||
return -1;
|
||||
if (blen > 0 && send_all(conn->fd, body, blen) < 0)
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---------- link_call public surface ---------- */
|
||||
|
||||
const char *link_call_path (const link_call_t *c) { return c ? c->incoming.path : NULL; }
|
||||
const char *link_call_interface(const link_call_t *c) { return c ? c->incoming.interface : NULL; }
|
||||
const char *link_call_member (const link_call_t *c) { return c ? c->incoming.member : NULL; }
|
||||
uid_t link_call_uid (const link_call_t *c) { return c ? c->conn->peer_uid : (uid_t)-1; }
|
||||
|
||||
link_writer_t *link_call_reply(link_call_t *call)
|
||||
{
|
||||
if (!call || call->reply_consumed || call->error_sent)
|
||||
return NULL;
|
||||
call->reply_consumed = 1;
|
||||
__w_init(&call->reply_writer,
|
||||
call->conn->txbuf, sizeof(call->conn->txbuf));
|
||||
return &call->reply_writer;
|
||||
}
|
||||
|
||||
int link_call_reply_error(link_call_t *call, const char *name, const char *message)
|
||||
{
|
||||
if (!call || call->error_sent) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
call->error_sent = 1;
|
||||
return __send_error(call->conn, &call->incoming, name, message);
|
||||
}
|
||||
|
||||
/* ---------- public reader wrappers ---------- */
|
||||
|
||||
int link_call_read_byte (link_call_t *c, uint8_t *o) { return __r_byte (&c->read_cursor, o); }
|
||||
int link_call_read_bool (link_call_t *c, int *o) { return __r_bool (&c->read_cursor, o); }
|
||||
int link_call_read_u32 (link_call_t *c, uint32_t *o) { return __r_u32 (&c->read_cursor, o); }
|
||||
int link_call_read_string(link_call_t *c, const char **o) { return __r_string(&c->read_cursor, o); }
|
||||
int link_call_read_path (link_call_t *c, const char **o) { return __r_path (&c->read_cursor, o); }
|
||||
|
||||
/* ---------- public writer wrappers ---------- */
|
||||
|
||||
void link_writer_init (link_writer_t *w, uint8_t *buf, size_t cap) { __w_init(w, buf, cap); }
|
||||
ssize_t link_writer_finish(link_writer_t *w) { return __w_finish(w); }
|
||||
|
||||
void link_w_byte (link_writer_t *w, uint8_t v) { __w_byte(w, v); }
|
||||
void link_w_bool (link_writer_t *w, int v) { __w_bool(w, v); }
|
||||
void link_w_u32 (link_writer_t *w, uint32_t v) { __w_u32(w, v); }
|
||||
void link_w_string (link_writer_t *w, const char *s) { __w_string(w, s); }
|
||||
void link_w_path (link_writer_t *w, const char *s) { __w_path(w, s); }
|
||||
void link_w_array_begin (link_writer_t *w, char ec) { __w_array_begin(w, ec); }
|
||||
void link_w_array_end (link_writer_t *w) { __w_array_end(w); }
|
||||
void link_w_struct_begin(link_writer_t *w) { __w_struct_begin(w); }
|
||||
void link_w_struct_end (link_writer_t *w) { __w_struct_end(w); }
|
||||
|
||||
/* ---------- public reader wrappers ---------- */
|
||||
|
||||
void link_reader_init(link_reader_t *r, const uint8_t *body, size_t len) { __r_init(r, body, len); }
|
||||
int link_r_byte (link_reader_t *r, uint8_t *o) { return __r_byte (r, o); }
|
||||
int link_r_bool (link_reader_t *r, int *o) { return __r_bool (r, o); }
|
||||
int link_r_u32 (link_reader_t *r, uint32_t *o) { return __r_u32 (r, o); }
|
||||
int link_r_string(link_reader_t *r, const char **o) { return __r_string(r, o); }
|
||||
int link_r_path (link_reader_t *r, const char **o) { return __r_path (r, o); }
|
||||
int link_r_done (const link_reader_t *r) { return __r_done (r); }
|
||||
size_t link_r_pos (const link_reader_t *r) { return r->off; }
|
||||
|
||||
/* ---------- dispatch entry point ---------- */
|
||||
|
||||
int __dispatch_message(link_connection_t *conn, const struct link_msg *m)
|
||||
{
|
||||
struct link_object *o;
|
||||
struct link_vtable_entry *e = NULL;
|
||||
const link_method_t *meth;
|
||||
struct link_call call;
|
||||
ssize_t blen;
|
||||
int rc;
|
||||
|
||||
if (m->type != LINK_MSG_METHOD_CALL) {
|
||||
/* Signals and replies from a client to PID 1 are nonsense;
|
||||
* silently drop. */
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!m->path || !m->member) {
|
||||
return __send_error(conn, m,
|
||||
"org.freedesktop.DBus.Error.InvalidArgs",
|
||||
"Method call without path or member");
|
||||
}
|
||||
|
||||
/* Built-in DBus interfaces (Hello, Ping, Introspect, Properties)
|
||||
* are handled here before object-tree lookup, which means they
|
||||
* also run before the LINK_METHOD_PRIVILEGED authz gate further
|
||||
* down. The current set is read-only; do NOT introduce a
|
||||
* state-changing built-in without first adding equivalent
|
||||
* authorisation inside __handle_builtin. */
|
||||
rc = __handle_builtin(conn, m);
|
||||
if (rc >= 0)
|
||||
return rc; /* 0 = handled OK, 1 = built-in but failed; <0 = not a built-in */
|
||||
|
||||
o = find_object(conn->server, m->path);
|
||||
if (!o) {
|
||||
return __send_error(conn, m,
|
||||
"org.freedesktop.DBus.Error.UnknownObject",
|
||||
"No such object");
|
||||
}
|
||||
|
||||
meth = resolve(o, m->interface, m->member, &e);
|
||||
if (!meth) {
|
||||
return __send_error(conn, m,
|
||||
"org.freedesktop.DBus.Error.UnknownMethod",
|
||||
"No such method on this object");
|
||||
}
|
||||
|
||||
/* Validate signature: client must match the declared in_sig. */
|
||||
{
|
||||
const char *got = m->signature ? m->signature : "";
|
||||
const char *want = meth->in_sig ? meth->in_sig : "";
|
||||
|
||||
if (strcmp(got, want) != 0)
|
||||
return __send_error(conn, m,
|
||||
"org.freedesktop.DBus.Error.InvalidArgs",
|
||||
"Argument signature mismatch");
|
||||
}
|
||||
|
||||
/* Per-method authorization. PRIVILEGED methods require uid 0;
|
||||
* the peer's uid was captured via SO_PEERCRED at accept time
|
||||
* and verified against the AUTH EXTERNAL claim, so we can trust
|
||||
* conn->peer_uid here. */
|
||||
if ((meth->flags & LINK_METHOD_PRIVILEGED) && conn->peer_uid != 0) {
|
||||
return __send_error(conn, m,
|
||||
"org.freedesktop.DBus.Error.AccessDenied",
|
||||
"Method requires root privileges");
|
||||
}
|
||||
|
||||
memset(&call, 0, sizeof(call));
|
||||
call.conn = conn;
|
||||
call.incoming = *m;
|
||||
__r_init(&call.read_cursor, m->body, m->body_avail);
|
||||
|
||||
rc = meth->handler(&call, e->userdata);
|
||||
if (rc < 0 && !call.reply_consumed && !call.error_sent) {
|
||||
/* Handler returned an error without sending one. */
|
||||
__send_error(conn, m,
|
||||
"org.freedesktop.DBus.Error.Failed",
|
||||
"Handler failed");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!call.reply_consumed && !call.error_sent) {
|
||||
/* Handler returned 0 but never produced a reply; treat as
|
||||
* empty reply with out_sig "". */
|
||||
__send_method_return(conn, m, NULL, NULL, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (call.reply_consumed && !call.error_sent) {
|
||||
blen = __w_finish(&call.reply_writer);
|
||||
if (blen < 0)
|
||||
return __send_error(conn, m,
|
||||
"org.freedesktop.DBus.Error.Failed",
|
||||
"Reply marshalling overflow");
|
||||
return __send_method_return(conn, m, meth->out_sig,
|
||||
conn->txbuf, (size_t)blen);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/* libink internal types — not for external consumers.
|
||||
*
|
||||
* Copyright (c) 2026 Joachim Wiberg <troglobit@gmail.com>
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
#ifndef LIBINK_INTERNAL_H_
|
||||
#define LIBINK_INTERNAL_H_
|
||||
|
||||
#include <stdint.h>
|
||||
#include <sys/queue.h>
|
||||
|
||||
#include "link.h"
|
||||
#include "marshal.h"
|
||||
#include "proto.h"
|
||||
|
||||
typedef enum {
|
||||
LINK_AUTH_NUL = 0,
|
||||
LINK_AUTH_LINE,
|
||||
LINK_AUTH_DONE,
|
||||
LINK_AUTH_FAILED,
|
||||
} link_auth_state_t;
|
||||
|
||||
#define LINK_PATH_MAX 108
|
||||
#define LINK_AUTH_LINEBUF_SIZE 256
|
||||
#define LINK_RX_BUF_SIZE (64 * 1024)
|
||||
#define LINK_TX_BUF_SIZE (16 * 1024)
|
||||
#define LINK_UNIQUE_NAME_LEN 16
|
||||
#define LINK_MATCH_RULE_MAX 256 /* per-peer match rule cap */
|
||||
#define LINK_MATCH_PEER_CAP 16 /* max active match rules per peer */
|
||||
|
||||
/* Per-vtable record attached to an object's interface list. */
|
||||
struct link_vtable_entry {
|
||||
const link_vtable_t *vt;
|
||||
void *userdata;
|
||||
TAILQ_ENTRY(link_vtable_entry) link;
|
||||
};
|
||||
|
||||
TAILQ_HEAD(link_vtable_list, link_vtable_entry);
|
||||
|
||||
/* An object exposed at one path. */
|
||||
struct link_object {
|
||||
char path[LINK_PATH_MAX];
|
||||
struct link_vtable_list vtables;
|
||||
TAILQ_ENTRY(link_object) link;
|
||||
};
|
||||
|
||||
TAILQ_HEAD(link_object_list, link_object);
|
||||
|
||||
struct link_server {
|
||||
int fd;
|
||||
char path[LINK_PATH_MAX];
|
||||
struct link_object_list objects;
|
||||
uint32_t next_unique_id; /* for ":1.N" names */
|
||||
};
|
||||
|
||||
/* The reply being assembled inside a method handler.
|
||||
*
|
||||
* The reply body lives in conn->txbuf, not on this struct, so a
|
||||
* stack-allocated link_call (in dispatch) stays small. Sharing the
|
||||
* connection's txbuf is safe: the event loop is single-threaded and
|
||||
* a connection only ever has one in-flight method call at a time. */
|
||||
struct link_call {
|
||||
link_connection_t *conn;
|
||||
struct link_msg incoming;
|
||||
struct link_reader read_cursor;
|
||||
struct link_writer reply_writer; /* writes into conn->txbuf */
|
||||
int reply_consumed;
|
||||
int error_sent;
|
||||
};
|
||||
|
||||
/* A parsed AddMatch rule. Fields are NULL when the rule omits the
|
||||
* key, meaning "match anything"; non-NULL means "must equal". */
|
||||
struct link_match {
|
||||
char *raw; /* original string, for RemoveMatch */
|
||||
char *type; /* "signal", or NULL */
|
||||
char *interface;
|
||||
char *member;
|
||||
char *path;
|
||||
};
|
||||
|
||||
struct link_connection {
|
||||
int fd;
|
||||
uid_t peer_uid;
|
||||
|
||||
char guid[33];
|
||||
char unique_name[LINK_UNIQUE_NAME_LEN]; /* ":1.N" */
|
||||
|
||||
link_auth_state_t auth;
|
||||
char linebuf[LINK_AUTH_LINEBUF_SIZE];
|
||||
size_t linelen;
|
||||
|
||||
/* Match rules registered via org.freedesktop.DBus.AddMatch.
|
||||
* Bounded for PID 1 hygiene; a peer that exceeds the cap gets
|
||||
* a LimitsExceeded error reply. */
|
||||
struct link_match *matches[LINK_MATCH_PEER_CAP];
|
||||
size_t matches_count;
|
||||
|
||||
uint8_t rxbuf[LINK_RX_BUF_SIZE];
|
||||
size_t rxlen;
|
||||
|
||||
/* Scratch for outgoing reply bodies. Shared by the dispatch
|
||||
* path (writes through call.reply_writer) and built-in handlers
|
||||
* (send_string_reply). Lifetime ends with each send_method_*
|
||||
* call. */
|
||||
uint8_t txbuf[LINK_TX_BUF_SIZE];
|
||||
|
||||
uint32_t next_serial;
|
||||
|
||||
struct link_server *server; /* back-pointer for dispatch */
|
||||
};
|
||||
|
||||
/* io.c — shared EINTR-resilient I/O loops. */
|
||||
int __io_write_all(int fd, const void *buf, size_t len);
|
||||
int __io_read_full(int fd, void *buf, size_t len);
|
||||
|
||||
/* auth.c */
|
||||
int __auth_process(link_connection_t *conn);
|
||||
void __auth_generate_guid(char out[33]);
|
||||
int __auth_client(int fd, uid_t uid);
|
||||
|
||||
/* dispatch.c */
|
||||
int __dispatch_message(link_connection_t *conn, const struct link_msg *m);
|
||||
int __send_error(link_connection_t *conn, const struct link_msg *req,
|
||||
const char *error_name, const char *text);
|
||||
int __send_method_return(link_connection_t *conn, const struct link_msg *req,
|
||||
const char *out_sig,
|
||||
const uint8_t *body, size_t body_len);
|
||||
|
||||
/* builtin.c */
|
||||
int __handle_builtin(link_connection_t *conn, const struct link_msg *m);
|
||||
|
||||
/* match.c */
|
||||
struct link_match *__match_parse (const char *rule);
|
||||
void __match_free (struct link_match *m);
|
||||
int __match_matches(const struct link_match *m,
|
||||
const char *path, const char *iface,
|
||||
const char *member);
|
||||
int __match_add (link_connection_t *conn, const char *rule);
|
||||
int __match_remove (link_connection_t *conn, const char *rule);
|
||||
|
||||
#endif /* LIBINK_INTERNAL_H_ */
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/* libink — shared I/O helpers.
|
||||
*
|
||||
* Server send paths (libink/dispatch.c, libink/auth.c) and the
|
||||
* client (libink/client.c) all need EINTR-resilient write_all /
|
||||
* read_full. On any other error they return -1 with an unknown
|
||||
* number of bytes already transferred.
|
||||
*
|
||||
* Copyright (c) 2026 Joachim Wiberg <troglobit@gmail.com>
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "internal.h"
|
||||
|
||||
int __io_write_all(int fd, const void *buf, size_t len)
|
||||
{
|
||||
const char *p = buf;
|
||||
|
||||
while (len > 0) {
|
||||
ssize_t n = write(fd, p, len);
|
||||
|
||||
if (n < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
return -1;
|
||||
}
|
||||
p += n;
|
||||
len -= (size_t)n;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __io_read_full(int fd, void *buf, size_t len)
|
||||
{
|
||||
char *p = buf;
|
||||
|
||||
while (len > 0) {
|
||||
ssize_t n = read(fd, p, len);
|
||||
|
||||
if (n == 0)
|
||||
return -1;
|
||||
if (n < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
return -1;
|
||||
}
|
||||
p += n;
|
||||
len -= (size_t)n;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
prefix=@prefix@
|
||||
exec_prefix=@exec_prefix@
|
||||
libdir=@libdir@
|
||||
includedir=@includedir@
|
||||
|
||||
Name: libink
|
||||
Description: Brokerless D-Bus server library, born inside Finit
|
||||
Version: @PACKAGE_VERSION@
|
||||
Libs: -L${libdir} -link
|
||||
Cflags: -I${includedir}
|
||||
+295
@@ -0,0 +1,295 @@
|
||||
/* libink — brokerless D-Bus server library, born inside Finit
|
||||
*
|
||||
* Copyright (c) 2026 Joachim Wiberg <troglobit@gmail.com>
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
#ifndef LIBINK_LINK_H_
|
||||
#define LIBINK_LINK_H_
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct link_server link_server_t;
|
||||
typedef struct link_connection link_connection_t;
|
||||
typedef struct link_call link_call_t;
|
||||
typedef struct link_client link_client_t;
|
||||
|
||||
/* D-Bus message type codes -- see link_reply_t.type. */
|
||||
#define LINK_MSG_INVALID 0
|
||||
#define LINK_MSG_METHOD_CALL 1
|
||||
#define LINK_MSG_METHOD_RETURN 2
|
||||
#define LINK_MSG_ERROR 3
|
||||
#define LINK_MSG_SIGNAL 4
|
||||
|
||||
/* Writer is exposed so callers can stack-allocate one for marshalling
|
||||
* signal/reply bodies. Treat the fields as opaque; use link_writer_init
|
||||
* + the link_w_* helpers + link_writer_finish. Sized for typical D-Bus
|
||||
* messages -- the array stack supports up to 8 levels of nesting. */
|
||||
#define LINK_WRITER_MAX_NESTING 8
|
||||
typedef struct link_writer {
|
||||
uint8_t *buf;
|
||||
size_t cap;
|
||||
size_t off;
|
||||
int err;
|
||||
struct {
|
||||
size_t lenpos;
|
||||
size_t elemstart;
|
||||
} arrays[LINK_WRITER_MAX_NESTING];
|
||||
size_t array_depth;
|
||||
} link_writer_t;
|
||||
|
||||
/* Reader is exposed so callers can stack-allocate one for decoding
|
||||
* reply or signal bodies received from a peer. Treat fields as
|
||||
* opaque; use link_reader_init + the link_r_* helpers. */
|
||||
typedef struct link_reader {
|
||||
const uint8_t *base;
|
||||
size_t off;
|
||||
size_t cap;
|
||||
int err; /* sticky */
|
||||
} link_reader_t;
|
||||
|
||||
/* View of an inbound message (method-return, error, or signal),
|
||||
* populated by link_client_call(_v) and link_client_wait(), and
|
||||
* returned by link_client_reply(). All pointers reference internal
|
||||
* client storage and are invalidated by the next call or wait on the
|
||||
* same client, or by link_client_close(). `body` is NULL iff
|
||||
* body_len==0; `error_name` is non-NULL only when type == LINK_MSG_ERROR;
|
||||
* `path`/`interface`/`member` are non-NULL on signals. */
|
||||
typedef struct {
|
||||
uint8_t type; /* LINK_MSG_METHOD_RETURN, _ERROR, or _SIGNAL */
|
||||
const char *signature;
|
||||
const char *error_name;
|
||||
const char *path;
|
||||
const char *interface;
|
||||
const char *member;
|
||||
const uint8_t *body;
|
||||
size_t body_len;
|
||||
} link_reply_t;
|
||||
|
||||
/* ---------- server / connection lifecycle ---------- */
|
||||
|
||||
int link_server_new (link_server_t **server, const char *path);
|
||||
void link_server_free (link_server_t *server);
|
||||
int link_server_get_fd(const link_server_t *server);
|
||||
|
||||
int link_server_accept(link_server_t *server, link_connection_t **conn);
|
||||
|
||||
int link_connection_get_fd (const link_connection_t *conn);
|
||||
uid_t link_connection_get_uid (const link_connection_t *conn);
|
||||
int link_connection_process (link_connection_t *conn);
|
||||
void link_connection_close (link_connection_t *conn);
|
||||
|
||||
/* ---------- object registration ---------- */
|
||||
|
||||
typedef int (*link_method_fn)(link_call_t *call, void *userdata);
|
||||
|
||||
/* Method flags for link_method_t.flags */
|
||||
#define LINK_METHOD_PRIVILEGED (1u << 0) /* peer must be uid 0 (root) */
|
||||
|
||||
typedef struct {
|
||||
const char *name; /* member name */
|
||||
const char *in_sig; /* input signature (D-Bus, e.g. "" or "s") */
|
||||
const char *out_sig; /* output signature */
|
||||
unsigned flags; /* OR of LINK_METHOD_* */
|
||||
link_method_fn handler;
|
||||
} link_method_t;
|
||||
|
||||
typedef struct {
|
||||
const char *interface; /* e.g. "org.finit.Manager1" */
|
||||
const link_method_t *methods; /* terminated by {NULL, ...} */
|
||||
} link_vtable_t;
|
||||
|
||||
/* Register one (interface, methods) at `path`. Calling repeatedly
|
||||
* with the same path and different vtables adds more interfaces at
|
||||
* that object. The vtable pointer must outlive the server (typically
|
||||
* a static table). */
|
||||
int link_server_add_object(link_server_t *server, const char *path,
|
||||
const link_vtable_t *vt, void *userdata);
|
||||
|
||||
/* Remove every vtable registered at `path` and free the object.
|
||||
* Returns 0 if the object existed, -1 (errno=ENOENT) otherwise. */
|
||||
int link_server_remove_object(link_server_t *server, const char *path);
|
||||
|
||||
/* ---------- call accessors ---------- */
|
||||
|
||||
const char *link_call_path (const link_call_t *call);
|
||||
const char *link_call_interface(const link_call_t *call);
|
||||
const char *link_call_member (const link_call_t *call);
|
||||
uid_t link_call_uid (const link_call_t *call);
|
||||
|
||||
/* ---------- reading method-call arguments ----------
|
||||
*
|
||||
* Cursor starts at the beginning of the request body. Each
|
||||
* function returns 0 on success and advances the cursor; on
|
||||
* failure it returns -1 and leaves the cursor in an error state
|
||||
* (subsequent reads also fail). Strings reference the
|
||||
* connection's rx buffer and are valid for the duration of the
|
||||
* method handler. */
|
||||
|
||||
int link_call_read_byte (link_call_t *call, uint8_t *out);
|
||||
int link_call_read_bool (link_call_t *call, int *out);
|
||||
int link_call_read_u32 (link_call_t *call, uint32_t *out);
|
||||
int link_call_read_string(link_call_t *call, const char **out); /* "s" */
|
||||
int link_call_read_path (link_call_t *call, const char **out); /* "o" */
|
||||
|
||||
/* ---------- reply construction ---------- */
|
||||
|
||||
/* Get the writer for the reply body, write args into it, return 0
|
||||
* from the handler. Dispatch finalizes and sends the reply with
|
||||
* the out_sig declared on the vtable. May be called once per
|
||||
* call. */
|
||||
link_writer_t *link_call_reply(link_call_t *call);
|
||||
|
||||
/* Send a D-Bus error reply. `name` must be a valid D-Bus error
|
||||
* name (e.g. "org.freedesktop.DBus.Error.UnknownMethod"); `message`
|
||||
* may be NULL. */
|
||||
int link_call_reply_error(link_call_t *call, const char *name, const char *message);
|
||||
|
||||
/* ---------- signal emission ----------
|
||||
*
|
||||
* Send a signal to a single peer if its AddMatch rules accept it.
|
||||
* Callers marshal the body separately and pass the resulting bytes.
|
||||
* Returns 0 on success (or "filtered out, nothing sent"), -1 with
|
||||
* errno set on failure: EMSGSIZE and EINVAL mean nothing hit the
|
||||
* wire and the connection is still usable; anything else is a
|
||||
* transport failure that may have left a partial frame -- the
|
||||
* caller must drop the peer. */
|
||||
int link_connection_emit_signal(link_connection_t *conn,
|
||||
const char *path,
|
||||
const char *interface,
|
||||
const char *member,
|
||||
const char *signature,
|
||||
const uint8_t *body, size_t body_len);
|
||||
|
||||
/* ---------- client (outgoing method calls) ----------
|
||||
*
|
||||
* Connect, authenticate as the current effective uid, send BEGIN.
|
||||
* Returns NULL on any failure (caller can fall back to another
|
||||
* transport if it has one). */
|
||||
link_client_t *link_client_open(const char *path);
|
||||
void link_client_close(link_client_t *c);
|
||||
|
||||
/* Status codes returned by link_client_call(_v). */
|
||||
#define LINK_CALL_OK 0 /* method-return received */
|
||||
#define LINK_CALL_ERROR 1 /* server replied with an error */
|
||||
#define LINK_CALL_FAIL (-1) /* transport, parse, or invalid-arg failure */
|
||||
|
||||
/* Send a METHOD_CALL and read the reply synchronously.
|
||||
*
|
||||
* `signature` and `body`/`body_len` describe the outgoing body --
|
||||
* marshal it yourself with link_writer_init + the link_w_* helpers
|
||||
* + link_writer_finish. Pass signature=NULL and body=NULL for
|
||||
* methods that take no arguments.
|
||||
*
|
||||
* After the call, inspect the reply via link_client_reply() -- it
|
||||
* exposes the body bytes (for callers that want to decode them with
|
||||
* link_reader_init + link_r_*) and the error name on LINK_CALL_ERROR.
|
||||
* The reply view is invalidated by the next call on the same client
|
||||
* or by link_client_close(). */
|
||||
int link_client_call(link_client_t *c,
|
||||
const char *obj_path,
|
||||
const char *interface,
|
||||
const char *member,
|
||||
const char *signature,
|
||||
const uint8_t *body, size_t body_len);
|
||||
|
||||
/* Convenience wrapper that marshals the outgoing body from varargs
|
||||
* matching `signature`. Supported type codes (one per arg):
|
||||
* 'y' -> int (promoted uint8_t)
|
||||
* 'b' -> int (0/non-zero)
|
||||
* 'u' -> uint32_t
|
||||
* 's' -> const char *
|
||||
* 'o' -> const char * (object path)
|
||||
*
|
||||
* Pass signature=NULL or "" for void calls. Return value matches
|
||||
* link_client_call; an unsupported type code returns LINK_CALL_FAIL
|
||||
* with no message sent. */
|
||||
int link_client_call_v(link_client_t *c,
|
||||
const char *obj_path,
|
||||
const char *interface,
|
||||
const char *member,
|
||||
const char *signature, ...);
|
||||
|
||||
const link_reply_t *link_client_reply(link_client_t *c);
|
||||
|
||||
/* Wait up to `timeout_ms` milliseconds for the next inbound message
|
||||
* (typically a SIGNAL delivered after an AddMatch subscription), and
|
||||
* populate the same view returned by link_client_reply().
|
||||
* timeout_ms < 0 : block forever
|
||||
* timeout_ms == 0 : non-blocking (returns 1 immediately if no data)
|
||||
* timeout_ms > 0 : wait that long
|
||||
* Returns 0 on success, 1 on timeout, -1 on transport/parse error.
|
||||
*
|
||||
* Note: the timeout gates only the wait for the first byte of the
|
||||
* next frame. Once data starts arriving the rest of the message is
|
||||
* read blockingly; callers that need a hard upper bound should pass
|
||||
* a positive timeout AND have a watchdog at a higher level. */
|
||||
int link_client_wait(link_client_t *c, int timeout_ms);
|
||||
|
||||
/* ---------- standalone writer ----------
|
||||
*
|
||||
* For marshalling bodies outside a method-call handler (signals,
|
||||
* pre-computed replies). Initialise on a caller-owned buffer,
|
||||
* write args via link_w_*, then call link_writer_finish which
|
||||
* returns the body length or -1 on overflow. */
|
||||
void link_writer_init (link_writer_t *w, uint8_t *buf, size_t cap);
|
||||
ssize_t link_writer_finish(link_writer_t *w);
|
||||
|
||||
/* ---------- writer (mirrors the internal marshaller) ---------- */
|
||||
|
||||
void link_w_byte (link_writer_t *w, uint8_t v);
|
||||
void link_w_bool (link_writer_t *w, int v);
|
||||
void link_w_u32 (link_writer_t *w, uint32_t v);
|
||||
void link_w_string (link_writer_t *w, const char *s); /* "s" */
|
||||
void link_w_path (link_writer_t *w, const char *s); /* "o" */
|
||||
void link_w_array_begin (link_writer_t *w, char element_sig);
|
||||
void link_w_array_end (link_writer_t *w);
|
||||
void link_w_struct_begin(link_writer_t *w);
|
||||
void link_w_struct_end (link_writer_t *w);
|
||||
|
||||
/* ---------- standalone reader ----------
|
||||
*
|
||||
* For decoding bodies received off the wire (reply or signal).
|
||||
* Initialise on the body pointer + length, read via link_r_*,
|
||||
* check link_r_done() to confirm everything was consumed. */
|
||||
void link_reader_init(link_reader_t *r, const uint8_t *body, size_t len);
|
||||
int link_r_byte (link_reader_t *r, uint8_t *out);
|
||||
int link_r_bool (link_reader_t *r, int *out);
|
||||
int link_r_u32 (link_reader_t *r, uint32_t *out);
|
||||
int link_r_string (link_reader_t *r, const char **out); /* "s" */
|
||||
int link_r_path (link_reader_t *r, const char **out); /* "o" */
|
||||
int link_r_done (const link_reader_t *r);
|
||||
|
||||
/* Byte offset of the next read inside the original body buffer. Used
|
||||
* to detect end-of-array when walking "a<T>" payloads: read the array
|
||||
* byte-length prefix with link_r_u32 first, record (pos+length) as the
|
||||
* end, then loop while link_r_pos < end. */
|
||||
size_t link_r_pos (const link_reader_t *r);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* LIBINK_LINK_H_ */
|
||||
@@ -0,0 +1,260 @@
|
||||
/* libink — D-Bus body marshalling (writer side).
|
||||
*
|
||||
* Copyright (c) 2026 Joachim Wiberg <troglobit@gmail.com>
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include "marshal.h"
|
||||
|
||||
#define ALIGN_UP(x, n) (((x) + (n) - 1) & ~((size_t)((n) - 1)))
|
||||
|
||||
void __w_init(struct link_writer *w, uint8_t *buf, size_t cap)
|
||||
{
|
||||
w->buf = buf;
|
||||
w->cap = cap;
|
||||
w->off = 0;
|
||||
w->err = 0;
|
||||
w->array_depth = 0;
|
||||
}
|
||||
|
||||
ssize_t __w_finish(struct link_writer *w)
|
||||
{
|
||||
if (w->err || w->array_depth != 0)
|
||||
return -1;
|
||||
return (ssize_t)w->off;
|
||||
}
|
||||
|
||||
static int reserve(struct link_writer *w, size_t align, size_t bytes)
|
||||
{
|
||||
size_t pad;
|
||||
|
||||
if (w->err)
|
||||
return -1;
|
||||
|
||||
pad = ALIGN_UP(w->off, align) - w->off;
|
||||
if (w->off + pad + bytes > w->cap) {
|
||||
w->err = 1;
|
||||
return -1;
|
||||
}
|
||||
while (pad-- > 0)
|
||||
w->buf[w->off++] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void put_u32_at(struct link_writer *w, size_t pos, uint32_t v)
|
||||
{
|
||||
w->buf[pos] = (uint8_t)(v & 0xff);
|
||||
w->buf[pos + 1] = (uint8_t)((v >> 8) & 0xff);
|
||||
w->buf[pos + 2] = (uint8_t)((v >> 16) & 0xff);
|
||||
w->buf[pos + 3] = (uint8_t)((v >> 24) & 0xff);
|
||||
}
|
||||
|
||||
static void put_u32(struct link_writer *w, uint32_t v)
|
||||
{
|
||||
put_u32_at(w, w->off, v);
|
||||
w->off += 4;
|
||||
}
|
||||
|
||||
void __w_byte(struct link_writer *w, uint8_t v)
|
||||
{
|
||||
if (reserve(w, 1, 1) < 0)
|
||||
return;
|
||||
w->buf[w->off++] = v;
|
||||
}
|
||||
|
||||
void __w_bool(struct link_writer *w, int v)
|
||||
{
|
||||
if (reserve(w, 4, 4) < 0)
|
||||
return;
|
||||
put_u32(w, v ? 1u : 0u);
|
||||
}
|
||||
|
||||
void __w_u32(struct link_writer *w, uint32_t v)
|
||||
{
|
||||
if (reserve(w, 4, 4) < 0)
|
||||
return;
|
||||
put_u32(w, v);
|
||||
}
|
||||
|
||||
static void write_lenprefixed(struct link_writer *w, const char *s, int onebyte_len)
|
||||
{
|
||||
size_t len = s ? strlen(s) : 0;
|
||||
|
||||
if (onebyte_len) {
|
||||
if (reserve(w, 1, 1 + len + 1) < 0)
|
||||
return;
|
||||
w->buf[w->off++] = (uint8_t)len;
|
||||
} else {
|
||||
if (reserve(w, 4, 4 + len + 1) < 0)
|
||||
return;
|
||||
put_u32(w, (uint32_t)len);
|
||||
}
|
||||
if (s && len)
|
||||
memcpy(w->buf + w->off, s, len);
|
||||
w->off += len;
|
||||
w->buf[w->off++] = 0;
|
||||
}
|
||||
|
||||
void __w_string(struct link_writer *w, const char *s) { write_lenprefixed(w, s, 0); }
|
||||
void __w_path (struct link_writer *w, const char *s) { write_lenprefixed(w, s, 0); }
|
||||
void __w_sig (struct link_writer *w, const char *s) { write_lenprefixed(w, s, 1); }
|
||||
|
||||
static size_t element_align(char c)
|
||||
{
|
||||
switch (c) {
|
||||
case 'y': case 'g': case 'v': return 1;
|
||||
case 'n': case 'q': return 2;
|
||||
case 'b': case 'i': case 'u':
|
||||
case 's': case 'o': case 'h': case 'a': return 4;
|
||||
case 'x': case 't': case 'd':
|
||||
case '(': case '{': return 8;
|
||||
default: return 1;
|
||||
}
|
||||
}
|
||||
|
||||
void __w_array_begin(struct link_writer *w, char element_sig_first_char)
|
||||
{
|
||||
size_t lenpos;
|
||||
|
||||
if (w->err)
|
||||
return;
|
||||
if (w->array_depth >= LINK_WRITER_MAX_NESTING) {
|
||||
w->err = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (reserve(w, 4, 4) < 0)
|
||||
return;
|
||||
lenpos = w->off;
|
||||
put_u32(w, 0); /* placeholder */
|
||||
|
||||
/* Pad to the element's alignment. These pad bytes are NOT
|
||||
* counted in the array length per the D-Bus spec. */
|
||||
if (reserve(w, element_align(element_sig_first_char), 0) < 0)
|
||||
return;
|
||||
|
||||
w->arrays[w->array_depth].lenpos = lenpos;
|
||||
w->arrays[w->array_depth].elemstart = w->off;
|
||||
w->array_depth++;
|
||||
}
|
||||
|
||||
void __w_array_end(struct link_writer *w)
|
||||
{
|
||||
size_t elemstart, lenpos;
|
||||
uint32_t actual;
|
||||
|
||||
if (w->err || w->array_depth == 0) {
|
||||
w->err = 1;
|
||||
return;
|
||||
}
|
||||
w->array_depth--;
|
||||
lenpos = w->arrays[w->array_depth].lenpos;
|
||||
elemstart = w->arrays[w->array_depth].elemstart;
|
||||
actual = (uint32_t)(w->off - elemstart);
|
||||
put_u32_at(w, lenpos, actual);
|
||||
}
|
||||
|
||||
void __w_struct_begin(struct link_writer *w)
|
||||
{
|
||||
reserve(w, 8, 0);
|
||||
}
|
||||
|
||||
void __w_struct_end(struct link_writer *w)
|
||||
{
|
||||
(void)w;
|
||||
}
|
||||
|
||||
/* ---- reader ---- */
|
||||
|
||||
void __r_init(struct link_reader *r, const uint8_t *body, size_t len)
|
||||
{
|
||||
r->base = body;
|
||||
r->off = 0;
|
||||
r->cap = len;
|
||||
r->err = 0;
|
||||
}
|
||||
|
||||
static int r_skip_align(struct link_reader *r, size_t align)
|
||||
{
|
||||
size_t pad;
|
||||
|
||||
if (r->err)
|
||||
return -1;
|
||||
pad = ALIGN_UP(r->off, align) - r->off;
|
||||
if (r->off + pad > r->cap) {
|
||||
r->err = 1;
|
||||
return -1;
|
||||
}
|
||||
r->off += pad;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static uint32_t rd_u32(const uint8_t *p)
|
||||
{
|
||||
return (uint32_t)p[0]
|
||||
| ((uint32_t)p[1] << 8)
|
||||
| ((uint32_t)p[2] << 16)
|
||||
| ((uint32_t)p[3] << 24);
|
||||
}
|
||||
|
||||
int __r_byte(struct link_reader *r, uint8_t *out)
|
||||
{
|
||||
if (r_skip_align(r, 1) < 0 || r->off + 1 > r->cap) {
|
||||
r->err = 1;
|
||||
return -1;
|
||||
}
|
||||
*out = r->base[r->off++];
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __r_u32(struct link_reader *r, uint32_t *out)
|
||||
{
|
||||
if (r_skip_align(r, 4) < 0 || r->off + 4 > r->cap) {
|
||||
r->err = 1;
|
||||
return -1;
|
||||
}
|
||||
*out = rd_u32(r->base + r->off);
|
||||
r->off += 4;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __r_bool(struct link_reader *r, int *out)
|
||||
{
|
||||
uint32_t v;
|
||||
|
||||
if (__r_u32(r, &v) < 0)
|
||||
return -1;
|
||||
*out = v ? 1 : 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int read_string_like(struct link_reader *r, const char **out)
|
||||
{
|
||||
uint32_t len;
|
||||
|
||||
if (__r_u32(r, &len) < 0)
|
||||
return -1;
|
||||
if (r->off + (size_t)len + 1 > r->cap) {
|
||||
r->err = 1;
|
||||
return -1;
|
||||
}
|
||||
/* Spec requires nul terminator at base[off + len]. */
|
||||
if (r->base[r->off + len] != 0) {
|
||||
r->err = 1;
|
||||
return -1;
|
||||
}
|
||||
*out = (const char *)(r->base + r->off);
|
||||
r->off += (size_t)len + 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __r_string(struct link_reader *r, const char **out) { return read_string_like(r, out); }
|
||||
int __r_path (struct link_reader *r, const char **out) { return read_string_like(r, out); }
|
||||
|
||||
int __r_done(const struct link_reader *r)
|
||||
{
|
||||
return !r->err && r->off == r->cap;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/* libink — D-Bus body marshalling (writer side).
|
||||
*
|
||||
* Copyright (c) 2026 Joachim Wiberg <troglobit@gmail.com>
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
#ifndef LIBINK_MARSHAL_H_
|
||||
#define LIBINK_MARSHAL_H_
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* struct link_writer is defined in ink.h (public). Field layout is
|
||||
* "opaque" per the public contract; this file's helpers manipulate
|
||||
* the fields directly. */
|
||||
#include "link.h"
|
||||
|
||||
void __w_init (struct link_writer *w, uint8_t *buf, size_t cap);
|
||||
ssize_t __w_finish(struct link_writer *w);
|
||||
|
||||
void __w_byte (struct link_writer *w, uint8_t v);
|
||||
void __w_bool (struct link_writer *w, int v);
|
||||
void __w_u32 (struct link_writer *w, uint32_t v);
|
||||
void __w_string (struct link_writer *w, const char *s); /* "s" */
|
||||
void __w_path (struct link_writer *w, const char *s); /* "o" */
|
||||
void __w_sig (struct link_writer *w, const char *s); /* "g" */
|
||||
|
||||
/* element_sig_first_char drives the alignment padding inserted
|
||||
* between the array length prefix and the first element. */
|
||||
void __w_array_begin (struct link_writer *w, char element_sig_first_char);
|
||||
void __w_array_end (struct link_writer *w);
|
||||
|
||||
void __w_struct_begin(struct link_writer *w);
|
||||
void __w_struct_end (struct link_writer *w);
|
||||
|
||||
/* ---- reader ----
|
||||
*
|
||||
* Reads from a message body pointer + length, advancing a cursor.
|
||||
* struct link_reader is defined in link.h (public, opaque); the
|
||||
* helpers here manipulate the fields directly. */
|
||||
void __r_init (struct link_reader *r, const uint8_t *body, size_t len);
|
||||
int __r_byte (struct link_reader *r, uint8_t *out);
|
||||
int __r_bool (struct link_reader *r, int *out);
|
||||
int __r_u32 (struct link_reader *r, uint32_t *out);
|
||||
int __r_string(struct link_reader *r, const char **out); /* "s" */
|
||||
int __r_path (struct link_reader *r, const char **out); /* "o" */
|
||||
int __r_done (const struct link_reader *r);
|
||||
|
||||
#endif /* LIBINK_MARSHAL_H_ */
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
/* libink — D-Bus AddMatch / RemoveMatch rule parsing and matching.
|
||||
*
|
||||
* Subset of the spec: type, interface, member, path. Each entry is
|
||||
* a key='value' pair with single-quoted value, separated by commas.
|
||||
* Backslash escapes inside values (\\ and \') are not interpreted —
|
||||
* a peer needing them will get unexpected literal content. Unknown
|
||||
* keys cause the whole rule to be rejected so a peer learns its
|
||||
* filter didn't take, rather than silently receiving everything.
|
||||
*
|
||||
* Copyright (c) 2026 Joachim Wiberg <troglobit@gmail.com>
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "internal.h"
|
||||
|
||||
static char *dup_range(const char *p, size_t n)
|
||||
{
|
||||
char *s = malloc(n + 1);
|
||||
|
||||
if (!s)
|
||||
return NULL;
|
||||
memcpy(s, p, n);
|
||||
s[n] = '\0';
|
||||
return s;
|
||||
}
|
||||
|
||||
/* Parse one key='value' entry starting at *p. On success advances
|
||||
* *p past the trailing quote and any comma, returns 0. On malformed
|
||||
* input, returns -1. */
|
||||
static int parse_kv(const char **p, char **out_key, char **out_value)
|
||||
{
|
||||
const char *q = *p;
|
||||
const char *key_start, *val_start;
|
||||
|
||||
while (*q == ' ' || *q == '\t')
|
||||
q++;
|
||||
key_start = q;
|
||||
while ((*q >= 'a' && *q <= 'z') || (*q >= 'A' && *q <= 'Z') || *q == '_')
|
||||
q++;
|
||||
if (q == key_start || *q != '=')
|
||||
return -1;
|
||||
*out_key = dup_range(key_start, (size_t)(q - key_start));
|
||||
if (!*out_key)
|
||||
return -1;
|
||||
q++;
|
||||
|
||||
if (*q != '\'') {
|
||||
free(*out_key);
|
||||
return -1;
|
||||
}
|
||||
q++;
|
||||
val_start = q;
|
||||
while (*q && *q != '\'')
|
||||
q++;
|
||||
if (*q != '\'') {
|
||||
free(*out_key);
|
||||
return -1;
|
||||
}
|
||||
*out_value = dup_range(val_start, (size_t)(q - val_start));
|
||||
if (!*out_value) {
|
||||
free(*out_key);
|
||||
return -1;
|
||||
}
|
||||
q++;
|
||||
|
||||
while (*q == ' ' || *q == '\t' || *q == ',')
|
||||
q++;
|
||||
*p = q;
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct link_match *__match_parse(const char *rule)
|
||||
{
|
||||
struct link_match *m;
|
||||
const char *p;
|
||||
|
||||
if (!rule || strlen(rule) >= LINK_MATCH_RULE_MAX) {
|
||||
errno = EINVAL;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
m = calloc(1, sizeof(*m));
|
||||
if (!m)
|
||||
return NULL;
|
||||
m->raw = strdup(rule);
|
||||
if (!m->raw) {
|
||||
free(m);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (p = rule; *p; ) {
|
||||
char *key = NULL, *value = NULL;
|
||||
char **slot = NULL;
|
||||
|
||||
if (parse_kv(&p, &key, &value) < 0)
|
||||
goto bad;
|
||||
|
||||
if (!strcmp(key, "type")) slot = &m->type;
|
||||
else if (!strcmp(key, "interface")) slot = &m->interface;
|
||||
else if (!strcmp(key, "member")) slot = &m->member;
|
||||
else if (!strcmp(key, "path")) slot = &m->path;
|
||||
else {
|
||||
free(key);
|
||||
free(value);
|
||||
goto bad;
|
||||
}
|
||||
|
||||
if (*slot) {
|
||||
/* Duplicate key. */
|
||||
free(key);
|
||||
free(value);
|
||||
goto bad;
|
||||
}
|
||||
*slot = value;
|
||||
free(key);
|
||||
}
|
||||
|
||||
/* No need to default m->type: when it's NULL the matcher below
|
||||
* treats it as "match any", and the only thing libink emits via
|
||||
* the match table is signals, so the effective filter is
|
||||
* already "signal" without the explicit assignment. */
|
||||
return m;
|
||||
|
||||
bad:
|
||||
__match_free(m);
|
||||
errno = EINVAL;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void __match_free(struct link_match *m)
|
||||
{
|
||||
if (!m)
|
||||
return;
|
||||
free(m->raw);
|
||||
free(m->type);
|
||||
free(m->interface);
|
||||
free(m->member);
|
||||
free(m->path);
|
||||
free(m);
|
||||
}
|
||||
|
||||
static int field_matches(const char *want, const char *got)
|
||||
{
|
||||
if (!want)
|
||||
return 1; /* no filter on this field */
|
||||
if (!got)
|
||||
return 0;
|
||||
return strcmp(want, got) == 0;
|
||||
}
|
||||
|
||||
int __match_matches(const struct link_match *m,
|
||||
const char *path, const char *iface,
|
||||
const char *member)
|
||||
{
|
||||
/* Type filter: only signals get delivered through this path. */
|
||||
if (m->type && strcmp(m->type, "signal") != 0)
|
||||
return 0;
|
||||
|
||||
return field_matches(m->path, path)
|
||||
&& field_matches(m->interface, iface)
|
||||
&& field_matches(m->member, member);
|
||||
}
|
||||
|
||||
int __match_add(link_connection_t *conn, const char *rule)
|
||||
{
|
||||
struct link_match *m;
|
||||
|
||||
if (conn->matches_count >= LINK_MATCH_PEER_CAP) {
|
||||
errno = ENOSPC;
|
||||
return -1;
|
||||
}
|
||||
|
||||
m = __match_parse(rule);
|
||||
if (!m)
|
||||
return -1;
|
||||
|
||||
conn->matches[conn->matches_count++] = m;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __match_remove(link_connection_t *conn, const char *rule)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
for (i = 0; i < conn->matches_count; i++) {
|
||||
if (strcmp(conn->matches[i]->raw, rule) == 0) {
|
||||
__match_free(conn->matches[i]);
|
||||
conn->matches[i] = conn->matches[conn->matches_count - 1];
|
||||
conn->matches_count--;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
errno = ENOENT;
|
||||
return -1;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/* libink — D-Bus object-path encoding for arbitrary identifiers.
|
||||
*
|
||||
* Copyright (c) 2026 Joachim Wiberg <troglobit@gmail.com>
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#include "path.h"
|
||||
|
||||
static int is_safe(unsigned char c)
|
||||
{
|
||||
return (c >= 'A' && c <= 'Z')
|
||||
|| (c >= 'a' && c <= 'z')
|
||||
|| (c >= '0' && c <= '9');
|
||||
}
|
||||
|
||||
int link_path_encode(const char *in, char *out, size_t outsz)
|
||||
{
|
||||
static const char hex[] = "0123456789abcdef";
|
||||
size_t off = 0;
|
||||
|
||||
if (!in || !out || outsz == 0)
|
||||
return -1;
|
||||
|
||||
for (; *in; in++) {
|
||||
unsigned char c = (unsigned char)*in;
|
||||
|
||||
if (is_safe(c)) {
|
||||
if (off + 1 >= outsz)
|
||||
return -1;
|
||||
out[off++] = (char)c;
|
||||
} else {
|
||||
if (off + 3 >= outsz)
|
||||
return -1;
|
||||
out[off++] = '_';
|
||||
out[off++] = hex[c >> 4];
|
||||
out[off++] = hex[c & 0xf];
|
||||
}
|
||||
}
|
||||
out[off] = '\0';
|
||||
return (int)off;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/* libink — D-Bus object-path encoding for arbitrary identifiers.
|
||||
*
|
||||
* Copyright (c) 2026 Joachim Wiberg <troglobit@gmail.com>
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
#ifndef LIBINK_PATH_H_
|
||||
#define LIBINK_PATH_H_
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
/* Encode `in` as a D-Bus path segment. Bytes in [A-Za-z0-9] pass
|
||||
* through unchanged; everything else (including '_' itself) becomes
|
||||
* "_HH" with lowercase hex, mirroring systemd's escape_path.
|
||||
*
|
||||
* Returns the encoded length (excluding the terminating nul), or -1
|
||||
* if the output buffer cannot fit the result. `outsz` must
|
||||
* accommodate the encoded bytes plus a trailing nul; the worst-case
|
||||
* size for an N-byte input is 3*N + 1. */
|
||||
int link_path_encode(const char *in, char *out, size_t outsz);
|
||||
|
||||
#endif /* LIBINK_PATH_H_ */
|
||||
+387
@@ -0,0 +1,387 @@
|
||||
/* libink — D-Bus wire protocol: message header parsing and building.
|
||||
*
|
||||
* Implements the binary message header format described in the
|
||||
* D-Bus specification, sections "Message Format" and "Header Fields".
|
||||
* Bodies are deliberately not parsed here — that's the marshaller's
|
||||
* job (marshal.c).
|
||||
*
|
||||
* Native byte order is assumed to be little-endian; messages with the
|
||||
* 'B' endianness flag are rejected for now (every conforming client
|
||||
* on the platforms Finit targets sends 'l').
|
||||
*
|
||||
* Copyright (c) 2026 Joachim Wiberg <troglobit@gmail.com>
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#include <errno.h>
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include "proto.h"
|
||||
|
||||
#define HDR_FIXED_SIZE 16
|
||||
#define MAX_MSG_SIZE (128 * 1024) /* sanity limit for PID 1 */
|
||||
#define ALIGN_UP(x, n) (((x) + (n) - 1) & ~((size_t)((n) - 1)))
|
||||
|
||||
static inline uint32_t rd_u32(const uint8_t *p)
|
||||
{
|
||||
return (uint32_t)p[0]
|
||||
| ((uint32_t)p[1] << 8)
|
||||
| ((uint32_t)p[2] << 16)
|
||||
| ((uint32_t)p[3] << 24);
|
||||
}
|
||||
|
||||
static inline void wr_u32(uint8_t *p, uint32_t v)
|
||||
{
|
||||
p[0] = (uint8_t)(v & 0xff);
|
||||
p[1] = (uint8_t)((v >> 8) & 0xff);
|
||||
p[2] = (uint8_t)((v >> 16) & 0xff);
|
||||
p[3] = (uint8_t)((v >> 24) & 0xff);
|
||||
}
|
||||
|
||||
/* Parse a (length-prefixed, nul-terminated) DBus STRING or PATH from
|
||||
* the header field array. Returns a pointer into buf or NULL on
|
||||
* malformed input. *consumed receives the bytes used including the
|
||||
* nul. */
|
||||
static const char *parse_string(const uint8_t *buf, size_t avail, size_t *consumed)
|
||||
{
|
||||
uint32_t len;
|
||||
|
||||
if (avail < 4)
|
||||
return NULL;
|
||||
len = rd_u32(buf);
|
||||
if (len >= avail - 4) /* need room for len bytes + nul */
|
||||
return NULL;
|
||||
if (buf[4 + len] != 0)
|
||||
return NULL;
|
||||
*consumed = 4 + len + 1;
|
||||
return (const char *)(buf + 4);
|
||||
}
|
||||
|
||||
/* Parse a SIGNATURE (1-byte length, nul-terminated). */
|
||||
static const char *parse_signature(const uint8_t *buf, size_t avail, size_t *consumed)
|
||||
{
|
||||
uint32_t len;
|
||||
|
||||
if (avail < 1)
|
||||
return NULL;
|
||||
len = buf[0];
|
||||
if (len + 2 > avail)
|
||||
return NULL;
|
||||
if (buf[1 + len] != 0)
|
||||
return NULL;
|
||||
*consumed = 1 + len + 1;
|
||||
return (const char *)(buf + 1);
|
||||
}
|
||||
|
||||
ssize_t __msg_parse(const uint8_t *buf, size_t len, struct link_msg *out)
|
||||
{
|
||||
uint32_t fields_len, total_hdr, body_off, total;
|
||||
const uint8_t *fp, *fend;
|
||||
|
||||
memset(out, 0, sizeof(*out));
|
||||
|
||||
if (len < HDR_FIXED_SIZE)
|
||||
return 0;
|
||||
|
||||
if (buf[0] != 'l') {
|
||||
errno = EPROTO;
|
||||
return -1;
|
||||
}
|
||||
if (buf[3] != LINK_PROTOCOL_VERSION) {
|
||||
errno = EPROTONOSUPPORT;
|
||||
return -1;
|
||||
}
|
||||
out->endian = buf[0];
|
||||
out->type = buf[1];
|
||||
out->flags = buf[2];
|
||||
out->body_len = rd_u32(buf + 4);
|
||||
out->serial = rd_u32(buf + 8);
|
||||
fields_len = rd_u32(buf + 12);
|
||||
|
||||
if (fields_len > MAX_MSG_SIZE || out->body_len > MAX_MSG_SIZE) {
|
||||
errno = E2BIG;
|
||||
return -1;
|
||||
}
|
||||
|
||||
total_hdr = HDR_FIXED_SIZE + fields_len;
|
||||
body_off = (uint32_t)ALIGN_UP(total_hdr, 8);
|
||||
total = body_off + out->body_len;
|
||||
|
||||
if (len < total)
|
||||
return 0; /* need more bytes */
|
||||
|
||||
/* Walk the array of (byte field-code, variant). */
|
||||
fp = buf + HDR_FIXED_SIZE;
|
||||
fend = fp + fields_len;
|
||||
while (fp < fend) {
|
||||
uint8_t code;
|
||||
const char *vsig;
|
||||
size_t used;
|
||||
|
||||
fp = buf + ALIGN_UP((size_t)(fp - buf), 8);
|
||||
if (fp >= fend)
|
||||
break;
|
||||
|
||||
code = *fp++;
|
||||
vsig = parse_signature(fp, (size_t)(fend - fp), &used);
|
||||
if (!vsig) {
|
||||
errno = EPROTO;
|
||||
return -1;
|
||||
}
|
||||
fp += used;
|
||||
|
||||
if (vsig[0] == 's' || vsig[0] == 'o') {
|
||||
fp = buf + ALIGN_UP((size_t)(fp - buf), 4);
|
||||
if (fp >= fend) { errno = EPROTO; return -1; }
|
||||
const char *s = parse_string(fp, (size_t)(fend - fp), &used);
|
||||
if (!s) { errno = EPROTO; return -1; }
|
||||
switch (code) {
|
||||
case LINK_HDR_PATH: out->path = s; break;
|
||||
case LINK_HDR_INTERFACE: out->interface = s; break;
|
||||
case LINK_HDR_MEMBER: out->member = s; break;
|
||||
case LINK_HDR_ERROR_NAME: out->error_name = s; break;
|
||||
case LINK_HDR_DESTINATION: out->destination = s; break;
|
||||
case LINK_HDR_SENDER: out->sender = s; break;
|
||||
}
|
||||
fp += used;
|
||||
} else if (vsig[0] == 'g') {
|
||||
const char *s = parse_signature(fp, (size_t)(fend - fp), &used);
|
||||
if (!s) { errno = EPROTO; return -1; }
|
||||
if (code == LINK_HDR_SIGNATURE)
|
||||
out->signature = s;
|
||||
fp += used;
|
||||
} else if (vsig[0] == 'u') {
|
||||
fp = buf + ALIGN_UP((size_t)(fp - buf), 4);
|
||||
if (fp + 4 > fend) { errno = EPROTO; return -1; }
|
||||
uint32_t v = rd_u32(fp);
|
||||
if (code == LINK_HDR_REPLY_SERIAL)
|
||||
out->reply_serial = v;
|
||||
fp += 4;
|
||||
} else {
|
||||
/* Unknown field type — skip whole message. */
|
||||
errno = EPROTO;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
out->body = buf + body_off;
|
||||
out->body_avail = out->body_len;
|
||||
return (ssize_t)total;
|
||||
}
|
||||
|
||||
/* ---------- builders ---------- */
|
||||
|
||||
/* Append a (byte field-code, variant) entry to a header-fields array,
|
||||
* with the entry pre-aligned to 8 bytes. */
|
||||
static int put_field_string(uint8_t *buf, size_t cap, size_t *off,
|
||||
uint8_t code, char vsig_char,
|
||||
const char *value)
|
||||
{
|
||||
size_t o = *off;
|
||||
size_t pad = ALIGN_UP(o, 8) - o;
|
||||
size_t len = strlen(value);
|
||||
|
||||
/* Padding for struct alignment */
|
||||
while (pad-- > 0) {
|
||||
if (o >= cap) return -1;
|
||||
buf[o++] = 0;
|
||||
}
|
||||
|
||||
/* code, variant signature (1B len + 1B char + 1B nul) */
|
||||
if (o + 4 > cap) return -1;
|
||||
buf[o++] = code;
|
||||
buf[o++] = 1;
|
||||
buf[o++] = (uint8_t)vsig_char;
|
||||
buf[o++] = 0;
|
||||
|
||||
if (vsig_char == 's' || vsig_char == 'o') {
|
||||
/* 4-byte align for u32 length */
|
||||
while (o & 3) {
|
||||
if (o >= cap) return -1;
|
||||
buf[o++] = 0;
|
||||
}
|
||||
if (o + 4 + len + 1 > cap) return -1;
|
||||
wr_u32(buf + o, (uint32_t)len);
|
||||
o += 4;
|
||||
memcpy(buf + o, value, len);
|
||||
o += len;
|
||||
buf[o++] = 0;
|
||||
} else if (vsig_char == 'g') {
|
||||
if (o + 1 + len + 1 > cap) return -1;
|
||||
buf[o++] = (uint8_t)len;
|
||||
memcpy(buf + o, value, len);
|
||||
o += len;
|
||||
buf[o++] = 0;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*off = o;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int put_field_u32(uint8_t *buf, size_t cap, size_t *off,
|
||||
uint8_t code, uint32_t value)
|
||||
{
|
||||
size_t o = *off;
|
||||
size_t pad = ALIGN_UP(o, 8) - o;
|
||||
|
||||
while (pad-- > 0) {
|
||||
if (o >= cap) return -1;
|
||||
buf[o++] = 0;
|
||||
}
|
||||
if (o + 8 > cap) return -1;
|
||||
buf[o++] = code;
|
||||
buf[o++] = 1;
|
||||
buf[o++] = 'u';
|
||||
buf[o++] = 0;
|
||||
while (o & 3) {
|
||||
if (o >= cap) return -1;
|
||||
buf[o++] = 0;
|
||||
}
|
||||
if (o + 4 > cap) return -1;
|
||||
wr_u32(buf + o, value);
|
||||
o += 4;
|
||||
*off = o;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static ssize_t finalize_header(uint8_t *buf, size_t cap,
|
||||
uint8_t type, uint8_t flags,
|
||||
uint32_t body_len, uint32_t serial,
|
||||
size_t fields_end)
|
||||
{
|
||||
size_t hdr_end = fields_end;
|
||||
size_t pad = ALIGN_UP(hdr_end, 8) - hdr_end;
|
||||
|
||||
buf[0] = 'l';
|
||||
buf[1] = type;
|
||||
buf[2] = flags;
|
||||
buf[3] = LINK_PROTOCOL_VERSION;
|
||||
wr_u32(buf + 4, body_len);
|
||||
wr_u32(buf + 8, serial);
|
||||
wr_u32(buf + 12, (uint32_t)(hdr_end - HDR_FIXED_SIZE));
|
||||
|
||||
while (pad-- > 0) {
|
||||
if (hdr_end >= cap) return -1;
|
||||
buf[hdr_end++] = 0;
|
||||
}
|
||||
return (ssize_t)hdr_end;
|
||||
}
|
||||
|
||||
ssize_t __msg_build_return(uint8_t *buf, size_t cap,
|
||||
uint32_t serial, uint32_t reply_serial,
|
||||
const char *destination,
|
||||
const char *signature, uint32_t body_len)
|
||||
{
|
||||
size_t off = HDR_FIXED_SIZE;
|
||||
|
||||
if (cap < HDR_FIXED_SIZE)
|
||||
return -1;
|
||||
|
||||
if (put_field_u32(buf, cap, &off, LINK_HDR_REPLY_SERIAL, reply_serial) < 0)
|
||||
return -1;
|
||||
if (destination &&
|
||||
put_field_string(buf, cap, &off, LINK_HDR_DESTINATION, 's', destination) < 0)
|
||||
return -1;
|
||||
if (signature && *signature &&
|
||||
put_field_string(buf, cap, &off, LINK_HDR_SIGNATURE, 'g', signature) < 0)
|
||||
return -1;
|
||||
|
||||
return finalize_header(buf, cap, LINK_MSG_METHOD_RETURN,
|
||||
LINK_FLAG_NO_REPLY_EXPECTED,
|
||||
body_len, serial, off);
|
||||
}
|
||||
|
||||
ssize_t __msg_build_error(uint8_t *buf, size_t cap,
|
||||
uint32_t serial, uint32_t reply_serial,
|
||||
const char *destination,
|
||||
const char *error_name,
|
||||
const char *signature, uint32_t body_len)
|
||||
{
|
||||
size_t off = HDR_FIXED_SIZE;
|
||||
|
||||
if (cap < HDR_FIXED_SIZE || !error_name)
|
||||
return -1;
|
||||
|
||||
if (put_field_u32(buf, cap, &off, LINK_HDR_REPLY_SERIAL, reply_serial) < 0)
|
||||
return -1;
|
||||
if (put_field_string(buf, cap, &off, LINK_HDR_ERROR_NAME, 's', error_name) < 0)
|
||||
return -1;
|
||||
if (destination &&
|
||||
put_field_string(buf, cap, &off, LINK_HDR_DESTINATION, 's', destination) < 0)
|
||||
return -1;
|
||||
if (signature && *signature &&
|
||||
put_field_string(buf, cap, &off, LINK_HDR_SIGNATURE, 'g', signature) < 0)
|
||||
return -1;
|
||||
|
||||
return finalize_header(buf, cap, LINK_MSG_ERROR,
|
||||
LINK_FLAG_NO_REPLY_EXPECTED,
|
||||
body_len, serial, off);
|
||||
}
|
||||
|
||||
ssize_t __msg_build_signal(uint8_t *buf, size_t cap,
|
||||
uint32_t serial,
|
||||
const char *path,
|
||||
const char *interface,
|
||||
const char *member,
|
||||
const char *signature, uint32_t body_len)
|
||||
{
|
||||
size_t off = HDR_FIXED_SIZE;
|
||||
|
||||
if (cap < HDR_FIXED_SIZE || !path || !interface || !member)
|
||||
return -1;
|
||||
|
||||
if (put_field_string(buf, cap, &off, LINK_HDR_PATH, 'o', path) < 0)
|
||||
return -1;
|
||||
if (put_field_string(buf, cap, &off, LINK_HDR_INTERFACE, 's', interface) < 0)
|
||||
return -1;
|
||||
if (put_field_string(buf, cap, &off, LINK_HDR_MEMBER, 's', member) < 0)
|
||||
return -1;
|
||||
if (signature && *signature &&
|
||||
put_field_string(buf, cap, &off, LINK_HDR_SIGNATURE, 'g', signature) < 0)
|
||||
return -1;
|
||||
|
||||
return finalize_header(buf, cap, LINK_MSG_SIGNAL,
|
||||
LINK_FLAG_NO_REPLY_EXPECTED,
|
||||
body_len, serial, off);
|
||||
}
|
||||
|
||||
ssize_t __msg_build_method_call(uint8_t *buf, size_t cap,
|
||||
uint32_t serial,
|
||||
const char *path,
|
||||
const char *interface,
|
||||
const char *member,
|
||||
const char *signature,
|
||||
uint32_t body_len)
|
||||
{
|
||||
size_t off = HDR_FIXED_SIZE;
|
||||
|
||||
if (cap < HDR_FIXED_SIZE || !path || !member)
|
||||
return -1;
|
||||
|
||||
if (put_field_string(buf, cap, &off, LINK_HDR_PATH, 'o', path) < 0)
|
||||
return -1;
|
||||
if (interface &&
|
||||
put_field_string(buf, cap, &off, LINK_HDR_INTERFACE, 's', interface) < 0)
|
||||
return -1;
|
||||
if (put_field_string(buf, cap, &off, LINK_HDR_MEMBER, 's', member) < 0)
|
||||
return -1;
|
||||
if (signature && *signature &&
|
||||
put_field_string(buf, cap, &off, LINK_HDR_SIGNATURE, 'g', signature) < 0)
|
||||
return -1;
|
||||
|
||||
return finalize_header(buf, cap, LINK_MSG_METHOD_CALL,
|
||||
/* flags=0: we expect a reply */
|
||||
0,
|
||||
body_len, serial, off);
|
||||
}
|
||||
|
||||
size_t __msg_header_size(const struct link_msg *m)
|
||||
{
|
||||
(void)m;
|
||||
/* Generous upper bound used by callers to size send buffers. */
|
||||
return 512;
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
/* libink — D-Bus wire protocol: message header parsing and building.
|
||||
*
|
||||
* Copyright (c) 2026 Joachim Wiberg <troglobit@gmail.com>
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
#ifndef LIBINK_PROTO_H_
|
||||
#define LIBINK_PROTO_H_
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "link.h" /* LINK_MSG_* type codes */
|
||||
|
||||
/* Message flags. */
|
||||
#define LINK_FLAG_NO_REPLY_EXPECTED 0x01
|
||||
#define LINK_FLAG_NO_AUTO_START 0x02
|
||||
#define LINK_FLAG_ALLOW_INTERACTIVE_AUTHORIZATION 0x04
|
||||
|
||||
/* Header field codes. */
|
||||
#define LINK_HDR_PATH 1
|
||||
#define LINK_HDR_INTERFACE 2
|
||||
#define LINK_HDR_MEMBER 3
|
||||
#define LINK_HDR_ERROR_NAME 4
|
||||
#define LINK_HDR_REPLY_SERIAL 5
|
||||
#define LINK_HDR_DESTINATION 6
|
||||
#define LINK_HDR_SENDER 7
|
||||
#define LINK_HDR_SIGNATURE 8
|
||||
#define LINK_HDR_UNIX_FDS 9
|
||||
|
||||
#define LINK_PROTOCOL_VERSION 1
|
||||
|
||||
/* Parsed view of an incoming message. Pointers reference bytes
|
||||
* inside the receiver's own rx buffer; treat as borrowed and short-
|
||||
* lived (until the next read of the same connection). */
|
||||
struct link_msg {
|
||||
uint8_t type;
|
||||
uint8_t flags;
|
||||
uint8_t endian; /* 'l' or 'B' */
|
||||
uint32_t body_len;
|
||||
uint32_t serial;
|
||||
uint32_t reply_serial;
|
||||
|
||||
const char *path; /* object path, or NULL */
|
||||
const char *interface; /* may be NULL on method calls */
|
||||
const char *member;
|
||||
const char *error_name;
|
||||
const char *destination;
|
||||
const char *sender;
|
||||
const char *signature; /* may be NULL if body is empty */
|
||||
|
||||
/* Pointer into the rx buffer and length, after header padding. */
|
||||
const uint8_t *body;
|
||||
uint32_t body_avail;
|
||||
};
|
||||
|
||||
/* Parse a complete D-Bus message from `buf` of size `len`. On
|
||||
* success returns the total number of bytes consumed (header +
|
||||
* padding + body) and fills *out. Returns 0 if more bytes are
|
||||
* needed, -1 on malformed input. */
|
||||
ssize_t __msg_parse(const uint8_t *buf, size_t len, struct link_msg *out);
|
||||
|
||||
/* Compute the on-wire size of a future message header given the
|
||||
* fields we'd populate. Used to size send buffers. */
|
||||
size_t __msg_header_size(const struct link_msg *m);
|
||||
|
||||
/* Build a method-return header into `buf` (capacity `cap`).
|
||||
* `reply_serial`/`destination` come from the call being replied to.
|
||||
* `signature` is the body signature ("" if no args). `body_len`
|
||||
* is the length of the body that will follow the header padding.
|
||||
* Returns the number of bytes written, or -1 on overflow. */
|
||||
ssize_t __msg_build_return(uint8_t *buf, size_t cap,
|
||||
uint32_t serial, uint32_t reply_serial,
|
||||
const char *destination,
|
||||
const char *signature, uint32_t body_len);
|
||||
|
||||
/* Build an error reply header. */
|
||||
ssize_t __msg_build_error(uint8_t *buf, size_t cap,
|
||||
uint32_t serial, uint32_t reply_serial,
|
||||
const char *destination,
|
||||
const char *error_name,
|
||||
const char *signature, uint32_t body_len);
|
||||
|
||||
/* Build a signal header (no reply expected, no destination). */
|
||||
ssize_t __msg_build_signal(uint8_t *buf, size_t cap,
|
||||
uint32_t serial,
|
||||
const char *path,
|
||||
const char *interface,
|
||||
const char *member,
|
||||
const char *signature, uint32_t body_len);
|
||||
|
||||
/* Build a method-call header (client side). */
|
||||
ssize_t __msg_build_method_call(uint8_t *buf, size_t cap,
|
||||
uint32_t serial,
|
||||
const char *path,
|
||||
const char *interface,
|
||||
const char *member,
|
||||
const char *signature,
|
||||
uint32_t body_len);
|
||||
|
||||
#endif /* LIBINK_PROTO_H_ */
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
/* libink — listening socket, accept, peer-credential capture
|
||||
*
|
||||
* Copyright (c) 2026 Joachim Wiberg <troglobit@gmail.com>
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "internal.h"
|
||||
|
||||
static void close_save_errno(int fd)
|
||||
{
|
||||
int saved = errno;
|
||||
close(fd);
|
||||
errno = saved;
|
||||
}
|
||||
|
||||
int link_server_new(link_server_t **out, const char *path)
|
||||
{
|
||||
struct sockaddr_un sun = { .sun_family = AF_UNIX };
|
||||
link_server_t *srv;
|
||||
size_t plen;
|
||||
int fd;
|
||||
|
||||
if (!out || !path || !*path) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
plen = strlen(path);
|
||||
if (plen >= sizeof(sun.sun_path) || plen >= LINK_PATH_MAX) {
|
||||
errno = ENAMETOOLONG;
|
||||
return -1;
|
||||
}
|
||||
|
||||
srv = calloc(1, sizeof(*srv));
|
||||
if (!srv)
|
||||
return -1;
|
||||
TAILQ_INIT(&srv->objects);
|
||||
|
||||
fd = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
|
||||
if (fd < 0)
|
||||
goto err_free;
|
||||
|
||||
memcpy(sun.sun_path, path, plen + 1);
|
||||
|
||||
(void)unlink(path);
|
||||
|
||||
/* fchmod() on a Unix-domain socket fd is a silent no-op on Linux:
|
||||
* the file mode is fixed at bind() time as (0777 & ~umask). Set
|
||||
* umask around the bind() so the socket appears with mode 0666
|
||||
* atomically, no race window. World-accessible by design;
|
||||
* per-method authorization happens later in dispatch via
|
||||
* SO_PEERCRED. */
|
||||
{
|
||||
mode_t oldmask = umask(0111);
|
||||
int rc = bind(fd, (struct sockaddr *)&sun, sizeof(sun));
|
||||
int saved = errno;
|
||||
|
||||
umask(oldmask);
|
||||
if (rc < 0) {
|
||||
errno = saved;
|
||||
goto err_close;
|
||||
}
|
||||
}
|
||||
|
||||
if (listen(fd, 16) < 0)
|
||||
goto err_unlink;
|
||||
|
||||
srv->fd = fd;
|
||||
memcpy(srv->path, path, plen + 1);
|
||||
*out = srv;
|
||||
return 0;
|
||||
|
||||
err_unlink:
|
||||
(void)unlink(path);
|
||||
err_close:
|
||||
close_save_errno(fd);
|
||||
err_free:
|
||||
free(srv);
|
||||
return -1;
|
||||
}
|
||||
|
||||
void link_server_free(link_server_t *srv)
|
||||
{
|
||||
struct link_object *o;
|
||||
|
||||
if (!srv)
|
||||
return;
|
||||
|
||||
o = TAILQ_FIRST(&srv->objects);
|
||||
while (o) {
|
||||
struct link_object *next_o = TAILQ_NEXT(o, link);
|
||||
struct link_vtable_entry *e = TAILQ_FIRST(&o->vtables);
|
||||
|
||||
while (e) {
|
||||
struct link_vtable_entry *next_e = TAILQ_NEXT(e, link);
|
||||
|
||||
free(e);
|
||||
e = next_e;
|
||||
}
|
||||
free(o);
|
||||
o = next_o;
|
||||
}
|
||||
|
||||
if (srv->fd >= 0)
|
||||
close(srv->fd);
|
||||
if (srv->path[0])
|
||||
(void)unlink(srv->path);
|
||||
free(srv);
|
||||
}
|
||||
|
||||
int link_server_get_fd(const link_server_t *srv)
|
||||
{
|
||||
if (!srv)
|
||||
return -1;
|
||||
|
||||
return srv->fd;
|
||||
}
|
||||
|
||||
int link_server_accept(link_server_t *srv, link_connection_t **out)
|
||||
{
|
||||
struct ucred cred = { 0 };
|
||||
socklen_t credlen = sizeof(cred);
|
||||
link_connection_t *conn;
|
||||
int cfd;
|
||||
|
||||
if (!srv || !out) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
cfd = accept4(srv->fd, NULL, NULL, SOCK_NONBLOCK | SOCK_CLOEXEC);
|
||||
if (cfd < 0)
|
||||
return -1;
|
||||
|
||||
conn = calloc(1, sizeof(*conn));
|
||||
if (!conn) {
|
||||
close_save_errno(cfd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
conn->fd = cfd;
|
||||
conn->auth = LINK_AUTH_NUL;
|
||||
conn->server = srv;
|
||||
|
||||
if (getsockopt(cfd, SOL_SOCKET, SO_PEERCRED, &cred, &credlen) == 0)
|
||||
conn->peer_uid = cred.uid;
|
||||
else
|
||||
conn->peer_uid = (uid_t)-1;
|
||||
|
||||
__auth_generate_guid(conn->guid);
|
||||
|
||||
*out = conn;
|
||||
return 0;
|
||||
}
|
||||
Reference in new issue
Block a user