libei/src/liboeffis.c

789 lines
20 KiB
C

/* SPDX-License-Identifier: MIT */
/*
* Copyright © 2022 Red Hat, Inc.
*
* 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 (including the next
* paragraph) 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.
*/
#include "config.h"
#if HAVE_LIBSYSTEMD
#include <systemd/sd-bus.h>
#elif HAVE_LIBELOGIND
#include <elogind/sd-bus.h>
#elif HAVE_BASU
#include <basu/sd-bus.h>
#endif
#include "util-io.h"
#include "util-macros.h"
#include "util-object.h"
#include "util-sources.h"
#include "util-strings.h"
#include "util-time.h"
#include "util-version.h"
#include "liboeffis.h"
_Static_assert(sizeof(enum oeffis_event_type) == sizeof(int), "Invalid enum size");
_Static_assert(sizeof(enum oeffis_device) == sizeof(uint32_t), "Invalid enum size");
/* oeffis is simple enough that we don't really need debugging or a log
* handler. If you want this on, just define it */
/* #define log_debug(...) fprintf(stderr, "DEBUG " __VA_ARGS__) */
#define log_debug(...) /* */
static void
portal_init(struct oeffis *oeffis, const char *busname);
static void
portal_start(struct oeffis *oeffis);
enum oeffis_state {
OEFFIS_STATE_NEW,
OEFFIS_STATE_CREATE_SESSION,
OEFFIS_STATE_SESSION_CREATED,
OEFFIS_STATE_STARTED,
OEFFIS_STATE_CONNECTED_TO_EIS,
OEFFIS_STATE_DISCONNECTED, /* used for closed as well since
internally it's the same thing */
};
struct oeffis {
struct object object;
void *user_data;
struct sink *sink;
enum oeffis_state state;
uint32_t devices;
/* We can have a maximum of 3 events (connected, optional closed,
* disconnected) so we can have the event queue be a fixed
* null-terminated array, have a pointer to the current element and
* shift that on with every event */
enum oeffis_event_type event_queue[4];
/* Points to the next client-visible event in the event queue. only
* advanced by the client */
enum oeffis_event_type *next_event;
int eis_fd;
/* NULL until OEFFIS_STATE_DISCONNECTED */
char *error_message;
/* internal epollfd tickler */
struct source *epoll_tickler_source;
int pipefd[2];
/* sd-bus pieces */
struct source *bus_source;
sd_bus *bus;
sd_bus_slot *slot_request_response; /* Re-used for Request.Response */
sd_bus_slot *slot_session_closed;
char *busname;
char *session_path;
char *sender_name;
};
static void
oeffis_destroy(struct oeffis *oeffis)
{
free(oeffis->error_message);
sink_unref(oeffis->sink);
xclose(oeffis->eis_fd);
xclose(oeffis->pipefd[0]);
xclose(oeffis->pipefd[1]);
free(oeffis->sender_name);
free(oeffis->session_path);
free(oeffis->busname);
sd_bus_close(oeffis->bus);
sd_bus_unref(oeffis->bus);
sd_bus_slot_unref(oeffis->slot_request_response);
sd_bus_slot_unref(oeffis->slot_session_closed);
}
static
OBJECT_IMPLEMENT_CREATE(oeffis);
_public_
OBJECT_IMPLEMENT_REF(oeffis);
_public_
OBJECT_IMPLEMENT_UNREF_CLEANUP(oeffis);
_public_
OBJECT_IMPLEMENT_SETTER(oeffis, user_data, void *);
_public_
OBJECT_IMPLEMENT_GETTER(oeffis, user_data, void *);
_public_
OBJECT_IMPLEMENT_GETTER(oeffis, error_message, const char *);
DEFINE_UNREF_CLEANUP_FUNC(source);
static void
tickle(struct oeffis *oeffis)
{
xwrite(oeffis->pipefd[1], "kitzel", 6);
}
static void
_printf_(2, 3)
oeffis_disconnect(struct oeffis *oeffis, const char *fmt, ...)
{
if (oeffis->state == OEFFIS_STATE_DISCONNECTED)
return;
va_list args;
va_start(args, fmt);
oeffis->state = OEFFIS_STATE_DISCONNECTED;
oeffis->error_message = xvaprintf(fmt, args);
va_end(args);
*oeffis->next_event = OEFFIS_EVENT_DISCONNECTED;
oeffis->eis_fd = xclose(oeffis->eis_fd);
tickle(oeffis);
/* FIXME: need to so more here? */
}
static void
tickled(struct source *source, void *data)
{
/* Nothing to do here, just drain the data */
char buf[64];
xread(source_get_fd(source), buf, sizeof(buf));
}
_public_ struct oeffis *
oeffis_new(void *user_data)
{
_unref_(oeffis) *oeffis = oeffis_create(NULL);
oeffis->state = OEFFIS_STATE_NEW;
oeffis->user_data = user_data;
oeffis->next_event = oeffis->event_queue;
oeffis->eis_fd = -1;
oeffis->pipefd[0] = -1;
oeffis->pipefd[1] = -1;
oeffis->sink = sink_new();
if (!oeffis->sink)
return NULL;
/* set up a pipe we can write to to force the epoll to wake up even when
* nothing else happens */
int rc = xpipe2(oeffis->pipefd, O_CLOEXEC|O_NONBLOCK);
if (rc < 0)
return NULL;
_unref_(source) *s = source_new(oeffis->pipefd[0], tickled, NULL);
sink_add_source(oeffis->sink, s);
return steal(&oeffis);
}
_public_ int
oeffis_get_fd(struct oeffis *oeffis)
{
return sink_get_fd(oeffis->sink);
}
_public_ int
oeffis_get_eis_fd(struct oeffis *oeffis)
{
if (oeffis->state != OEFFIS_STATE_CONNECTED_TO_EIS) {
errno = ENODEV;
return -1;
}
return xdup(oeffis->eis_fd);
}
_public_ enum oeffis_event_type
oeffis_get_event(struct oeffis *oeffis)
{
enum oeffis_event_type e = *oeffis->next_event;
if (e != OEFFIS_EVENT_NONE)
oeffis->next_event++;
assert(oeffis->next_event < oeffis->event_queue + ARRAY_LENGTH(oeffis->event_queue));
return e;
}
_public_ void
oeffis_create_session(struct oeffis *oeffis, uint32_t devices)
{
oeffis_create_session_on_bus(oeffis, "org.freedesktop.portal.Desktop", devices);
}
_public_ void
oeffis_create_session_on_bus(struct oeffis *oeffis, const char *busname, uint32_t devices)
{
if (oeffis->state != OEFFIS_STATE_NEW)
return;
oeffis->devices = devices;
oeffis->state = OEFFIS_STATE_CREATE_SESSION;
portal_init(oeffis, busname);
}
_public_ void
oeffis_dispatch(struct oeffis *oeffis)
{
sink_dispatch(oeffis->sink);
}
static int
oeffis_set_eis_fd(struct oeffis *oeffis, int eisfd)
{
if (oeffis->state != OEFFIS_STATE_STARTED)
return -EALREADY;
oeffis->state = OEFFIS_STATE_CONNECTED_TO_EIS;
oeffis->eis_fd = eisfd;
*oeffis->next_event = OEFFIS_EVENT_CONNECTED_TO_EIS;
tickle(oeffis);
return 0;
}
static void
oeffis_close(struct oeffis *oeffis)
{
switch (oeffis->state) {
case OEFFIS_STATE_NEW:
oeffis_disconnect(oeffis, "Bug: Received Session.Close in state NEW.");
break;
case OEFFIS_STATE_CREATE_SESSION:
case OEFFIS_STATE_SESSION_CREATED:
case OEFFIS_STATE_CONNECTED_TO_EIS:
case OEFFIS_STATE_STARTED:
*oeffis->next_event = OEFFIS_EVENT_CLOSED;
tickle(oeffis);
oeffis->state = OEFFIS_STATE_DISCONNECTED;
break;
case OEFFIS_STATE_DISCONNECTED:
break;
}
}
/********************************************** DBus implementation **************************************************/
static char *
sender_name(sd_bus *bus)
{
_cleanup_free_ char *sender = NULL;
const char *name = NULL;
if ((sd_bus_get_unique_name(bus, &name) != 0) || strlen(name) < 1)
return NULL;
name += 1; /* drop initial : */
sender = xalloc(strlen(name));
for (unsigned i = 0; name[i]; i++) {
sender[i] = name[i] == '.' ? '_' : name[i];
}
return steal(&sender);
}
static char *
xdp_token(void)
{
/* next for easier debugging, rand() so we don't ever conflict in
* real life situations */
static uint32_t next = 0;
return xaprintf("oeffis_%u_%d", next++, rand());
}
static char *
xdp_request_path(char *sender_name, char *token)
{
return xaprintf("/org/freedesktop/portal/desktop/request/%s/%s", sender_name, token);
}
static char *
xdp_session_path(char *sender_name, char *token)
{
return xaprintf("/org/freedesktop/portal/desktop/session/%s/%s", sender_name, token);
}
static int
connect_to_eis_returned(sd_bus_message *m, void *userdata, sd_bus_error *ret_error)
{
int eisfd;
struct oeffis *oeffis = userdata;
int rc = sd_bus_message_get_errno(m);
if (rc > 0) {
oeffis_disconnect(oeffis, "Error calling ConnectToEIS: %s", strerror(rc));
return rc;
}
rc = sd_bus_message_read(m, "h", &eisfd);
if (rc < 0) {
oeffis_disconnect(oeffis, "Unable to get fd from portal: %s", strerror(-rc));
return -rc;
}
/* the fd is owned by the message */
rc = xerrno(xdup(eisfd));
if (rc < 0) {
oeffis_disconnect(oeffis, "Failed to dup fd: %s", strerror(-rc));
return -rc;
} else {
eisfd = rc;
int flags = fcntl(eisfd, F_GETFL, 0);
fcntl(eisfd, F_SETFL, flags | O_NONBLOCK);
}
log_debug("Got fd %d from portal", eisfd);
rc = oeffis_set_eis_fd(oeffis, eisfd);
if (rc < 0) {
oeffis_disconnect(oeffis, "Failed to set the fd: %s", strerror(-rc));
return -rc;
}
return 0;
}
static void
portal_connect_to_eis(struct oeffis *oeffis)
{
sd_bus *bus = oeffis->bus;
int rc = 0;
with_signals_blocked(SIGALRM) {
rc = sd_bus_call_method_async(bus, NULL, oeffis->busname,
"/org/freedesktop/portal/desktop",
"org.freedesktop.portal.RemoteDesktop",
"ConnectToEIS",
&connect_to_eis_returned,
oeffis,
"oa{sv}",
oeffis->session_path,
0);
}
if (rc < 0) {
oeffis_disconnect(oeffis, "Failed to call ConnectToEIS: %s", strerror(-rc));
return;
}
}
static int
session_closed_received(sd_bus_message *m, void *userdata, sd_bus_error *error)
{
struct oeffis *oeffis = userdata;
oeffis_close(oeffis);
return 0;
}
static void
dbus_dispatch(struct source *source, void *data)
{
struct oeffis *oeffis = data;
sd_bus *bus = oeffis->bus;
int rc;
do {
rc = sd_bus_process(bus, NULL);
} while (rc > 0);
if (rc < 0)
oeffis_disconnect(oeffis, "dbus processing failed with %s", strerror(-rc));
}
static int
portal_setup_request(struct oeffis *oeffis, sd_bus_message_handler_t response_handler,
char **token_return, sd_bus_slot **slot_return)
{
sd_bus *bus = oeffis->bus;
_unref_(sd_bus_slot) *slot = NULL;
_cleanup_free_ char *token = xdp_token();
_cleanup_free_ char *handle = xdp_request_path(oeffis->sender_name, token);
int rc = 0;
with_signals_blocked(SIGALRM) {
rc = sd_bus_match_signal(bus, &slot,
oeffis->busname,
handle,
"org.freedesktop.portal.Request",
"Response",
response_handler,
oeffis);
}
if (rc < 0) {
oeffis_disconnect(oeffis, "Failed to subscribe to Request.Response signal: %s", strerror(-rc));
return rc;
}
*token_return = steal(&token);
*slot_return = steal(&slot);
return 0;
}
static int
portal_start_response_received(sd_bus_message *m, void *userdata, sd_bus_error *error)
{
struct oeffis *oeffis = userdata;
/* We'll only get this signal once */
oeffis->slot_request_response = sd_bus_slot_unref(oeffis->slot_request_response);
unsigned int response;
int rc = sd_bus_message_read(m, "u", &response);
if (rc < 0) {
oeffis_disconnect(oeffis, "Failed to read response from signal: %s", strerror(-rc));
return 0;
}
log_debug("Portal Start response is %u", response);
if (response != 0) {
oeffis_disconnect(oeffis, "Portal denied Start");
return 0;
}
oeffis->state = OEFFIS_STATE_STARTED;
/* Response includes the the device bitmask but we don't care about this here */
/* Don't need a separate state here, ConnectToEIS is synchronous */
portal_connect_to_eis(oeffis);
return 0;
}
static void
portal_start(struct oeffis *oeffis)
{
_cleanup_free_ char *token = NULL;
_unref_(sd_bus_slot) *request_slot = NULL;
int rc = portal_setup_request(oeffis, portal_start_response_received, &token, &request_slot);
if (rc < 0)
return;
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
_unref_(sd_bus_message) *response = NULL;
sd_bus *bus = oeffis->bus;
with_signals_blocked(SIGALRM) {
rc = sd_bus_call_method(bus,
oeffis->busname,
"/org/freedesktop/portal/desktop",
"org.freedesktop.portal.RemoteDesktop",
"Start",
&error,
&response,
"osa{sv}",
oeffis->session_path,
"", /* parent window */
1,
"handle_token", /* string key */
"s", token /* variant string */
);
}
if (rc < 0) {
oeffis_disconnect(oeffis, "Failed to call method: %s", strerror(-rc));
return;
}
const char *path = NULL;
rc = sd_bus_message_read(response, "o", &path);
if (rc < 0) {
oeffis_disconnect(oeffis, "Failed to parse Start reply: %s", strerror(-rc));
return;
}
oeffis->slot_request_response = sd_bus_slot_ref(request_slot);
return;
}
static int
portal_select_devices_response_received(sd_bus_message *m, void *userdata, sd_bus_error *error)
{
struct oeffis *oeffis = userdata;
/* We'll only get this signal once */
oeffis->slot_request_response = sd_bus_slot_unref(oeffis->slot_request_response);
unsigned int response;
int rc = sd_bus_message_read(m, "u", &response);
if (rc < 0) {
oeffis_disconnect(oeffis, "Failed to read response from signal: %s", strerror(-rc));
return 0;
}
log_debug("Portal SelectDevices response is %u", response);
if (response != 0) {
oeffis_disconnect(oeffis, "Portal denied SelectDevices");
return 0;
}
/* Response includes the the device bitmask but we don't care about this here */
portal_start(oeffis);
return 0;
}
static void
portal_select_devices(struct oeffis *oeffis)
{
sd_bus *bus = oeffis->bus;
_cleanup_free_ char *token = NULL;
_unref_(sd_bus_slot) *request_slot = NULL;
int rc = portal_setup_request(oeffis, portal_select_devices_response_received, &token, &request_slot);
if (rc < 0)
return;
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
_unref_(sd_bus_message) *response = NULL;
with_signals_blocked(SIGALRM) {
rc = sd_bus_call_method(bus,
oeffis->busname,
"/org/freedesktop/portal/desktop",
"org.freedesktop.portal.RemoteDesktop",
"SelectDevices",
&error,
&response,
"oa{sv}",
oeffis->session_path,
oeffis->devices == OEFFIS_DEVICE_ALL_DEVICES ? 1 : 2,
"handle_token", /* string key */
"s", token, /* variant string */
"types", /* string key */
"u", oeffis->devices
);
}
if (rc < 0) {
oeffis_disconnect(oeffis, "Failed to call method: %s", strerror(-rc));
return;
}
const char *path = NULL;
rc = sd_bus_message_read(response, "o", &path);
if (rc < 0) {
oeffis_disconnect(oeffis, "Failed to parse Start reply: %s", strerror(-rc));
return;
}
oeffis->slot_request_response = sd_bus_slot_ref(request_slot);
}
static int
portal_create_session_response_received(sd_bus_message *m, void *userdata, sd_bus_error *error)
{
struct oeffis *oeffis = userdata;
/* We'll only get this signal once */
oeffis->slot_request_response = sd_bus_slot_unref(oeffis->slot_request_response);
unsigned int response;
int rc = sd_bus_message_read(m, "u", &response);
if (rc < 0) {
oeffis_disconnect(oeffis, "Failed to read response from signal: %s", strerror(-rc));
return 0;
}
log_debug("Portal CreateSession response is %u", response);
const char *session_handle = NULL;
if (response == 0) {
const char *key;
rc = sd_bus_message_read(m, "a{sv}", 1, &key, "s", &session_handle);
if (rc < 0) {
oeffis_disconnect(oeffis, "Failed to read session handle from signal: %s", strerror(-rc));
return 0;
}
if (!streq(key, "session_handle")) {
oeffis_disconnect(oeffis, "Invalid or unhandled option: %s", key);
return 0;
}
}
if (response != 0) {
oeffis_disconnect(oeffis, "Portal denied CreateSession");
return 0;
}
oeffis->session_path = xstrdup(session_handle);
oeffis->state = OEFFIS_STATE_SESSION_CREATED;
portal_select_devices(oeffis);
return 0;
}
static void
portal_init(struct oeffis *oeffis, const char *busname)
{
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
_unref_(sd_bus) *bus = NULL;
_unref_(sd_bus_message) *response = NULL;
const char *path = NULL;
int rc = sd_bus_open_user(&bus);
if (rc < 0) {
oeffis_disconnect(oeffis, "Failed to init dbus: %s", strerror(-rc));
return;
}
oeffis->sender_name = sender_name(bus);
if (!oeffis->sender_name) {
oeffis_disconnect(oeffis, "Failed to parse sender name");
return;
}
oeffis->bus = sd_bus_ref(bus);
oeffis->busname = xstrdup(busname);
uint32_t version;
rc = sd_bus_get_property_trivial(bus, busname,
"/org/freedesktop/portal/desktop",
"org.freedesktop.portal.RemoteDesktop",
"version",
&error,
'u',
&version);
if (rc < 0) {
oeffis_disconnect(oeffis, "Failed to get RemoteDesktop.version: %s", strerror(sd_bus_error_get_errno(&error)));
return;
} else if (version < VERSION_V(2)) {
oeffis_disconnect(oeffis, "RemoteDesktop.version is %u, we need 2", version);
return;
}
log_debug("RemoteDesktop.version is %u", version);
_cleanup_free_ char *token = NULL;
_unref_(sd_bus_slot) *request_slot = NULL;
rc = portal_setup_request(oeffis, portal_create_session_response_received, &token, &request_slot);
if (rc < 0)
return;
_unref_(sd_bus_slot) *session_slot = NULL;
_cleanup_free_ char *session_token = xdp_token();
_cleanup_free_ char *session_handle = xdp_session_path(oeffis->sender_name, session_token);
rc = sd_bus_match_signal(bus, &session_slot,
busname,
session_handle,
"org.freedesktop.portal.Session",
"Closed",
session_closed_received,
oeffis);
if (rc < 0) {
oeffis_disconnect(oeffis, "Failed to subscribe to Session.Closed signal: %s", strerror(-rc));
return;
}
with_signals_blocked(SIGALRM) {
rc = sd_bus_call_method(bus,
busname,
"/org/freedesktop/portal/desktop",
"org.freedesktop.portal.RemoteDesktop",
"CreateSession",
&error,
&response,
"a{sv}", 2,
"handle_token", /* string key */
"s", token, /* variant string */
"session_handle_token", /* string key */
"s", session_token /* variant string */
);
}
if (rc < 0) {
oeffis_disconnect(oeffis, "Failed to call method: %s", strerror(-rc));
return;
}
rc = sd_bus_message_read(response, "o", &path);
if (rc < 0) {
oeffis_disconnect(oeffis, "Failed to parse CreateSession reply: %s", strerror(-rc));
return;
}
log_debug("Portal Response object is %s", path);
_unref_(source) *s = source_new(sd_bus_get_fd(bus), dbus_dispatch, oeffis);
source_never_close_fd(s); /* the bus object handles the fd */
rc = sink_add_source(oeffis->sink, s);
if (rc == 0) {
oeffis->bus_source = source_ref(s);
oeffis->slot_request_response = sd_bus_slot_ref(request_slot);
oeffis->slot_session_closed = sd_bus_slot_ref(session_slot);
}
return;
}
#ifdef _enable_tests_
#include "util-munit.h"
MUNIT_TEST(test_init_unref)
{
struct oeffis *oeffis = oeffis_new(NULL);
munit_assert_int(oeffis->state, ==, OEFFIS_STATE_NEW);
munit_assert_not_null(oeffis->sink);
munit_assert_int(oeffis->eis_fd, ==, -1);
struct oeffis *refd = oeffis_ref(oeffis);
munit_assert_ptr_equal(oeffis, refd);
munit_assert_int(oeffis->object.refcount, ==, 2);
struct oeffis *unrefd = oeffis_unref(oeffis);
munit_assert_null(unrefd);
unrefd = oeffis_unref(oeffis);
munit_assert_null(unrefd);
return MUNIT_OK;
}
MUNIT_TEST(test_failed_connect)
{
struct oeffis *oeffis = oeffis_new(NULL);
enum oeffis_event_type event;
oeffis_create_session_on_bus(oeffis, "foo.bar.Example", 0);
while ((event = oeffis_get_event(oeffis)) == OEFFIS_EVENT_NONE)
oeffis_dispatch(oeffis);
munit_assert_int(event, ==, OEFFIS_EVENT_DISCONNECTED);
munit_assert_int(oeffis->state, ==, OEFFIS_STATE_DISCONNECTED);
munit_assert_not_null(oeffis->error_message);
oeffis_unref(oeffis);
return MUNIT_OK;
}
#endif