/* 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 __has_include() #include #elif __has_include() #include #else #include #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_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) return NULL; sender = xstrdup(name + 1); /* drop initial : */ for (unsigned i = 0; sender[i]; i++) { if (sender[i] == '.') sender[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 void portal_connect_to_eis(struct oeffis *oeffis) { _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; _unref_(sd_bus_message) *response = NULL; sd_bus *bus = oeffis->bus; int eisfd; int rc = 0; with_signals_blocked(SIGALRM) { rc = sd_bus_call_method(bus, oeffis->busname, "/org/freedesktop/portal/desktop", "org.freedesktop.portal.RemoteDesktop", "ConnectToEIS", &error, &response, "oa{sv}", oeffis->session_path, 0); } if (rc < 0) { oeffis_disconnect(oeffis, "Failed to call ConnectToEIS: %s", strerror(-rc)); return; } rc = sd_bus_message_read(response, "h", &eisfd); if (rc < 0) { oeffis_disconnect(oeffis, "Unable to get fd from portal: %s", strerror(-rc)); return; } /* 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; } else { eisfd = rc; int flags = fcntl(eisfd, F_GETFL, 0); fcntl(eisfd, F_SETFL, flags | O_NONBLOCK); } log_debug("Got fd %d from portal\n", eisfd); rc = oeffis_set_eis_fd(oeffis, eisfd); if (rc < 0) oeffis_disconnect(oeffis, "Failed to set the fd: %s", strerror(-rc)); } 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 reponse is %u\n", 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 reponse is %u\n", 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, 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 reponse is %u\n", 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\n", 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\n", 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