diff --git a/meson.build b/meson.build index 0672122..8b4cee3 100644 --- a/meson.build +++ b/meson.build @@ -186,6 +186,10 @@ if get_option('portal') 'tools/eis-fake-portal.c', include_directories: 'src', dependencies: [dep_libutil, dep_systemd, dep_libreis]) + executable('eis-fake-impl-portal', + 'tools/eis-fake-impl-portal.c', + include_directories: 'src', + dependencies: [dep_libutil, dep_systemd, dep_libreis]) endif # tests diff --git a/src/libei-portal.c b/src/libei-portal.c index 1b2b2c4..ba08e37 100644 --- a/src/libei-portal.c +++ b/src/libei-portal.c @@ -96,7 +96,7 @@ xdp_token(sd_bus *bus, char **token_out) } static void -portal_connect(struct ei_portal *portal) +portal_connect(struct ei_portal *portal, const char *session_handle) { _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; _unref_(sd_bus_message) *response = NULL; @@ -107,19 +107,40 @@ portal_connect(struct ei_portal *portal) int rc = sd_bus_call_method(bus, portal->busname, "/org/freedesktop/portal/desktop", "org.freedesktop.portal.EmulatedInput", - "Connect", + "ConnectToEIS", &error, &response, - "a{sv}", 0); + "oa{sv}", + session_handle, + 0); if (rc < 0) { log_error(ei, "Failed to call method: %s\n", strerror(-rc)); goto out; } - rc = sd_bus_message_read(response, "h", &eisfd); + int status; + rc = sd_bus_message_read(response, "u", &status); if (rc < 0) { - log_error(ei, "Failed to extract fd: %s\n", strerror(-rc)); + log_error(ei, "Failed to extract status, invalid message format: %s\n", strerror(-rc)); + goto out; + } + + if (status != 0) { + log_info(ei, "Unable to get fd from portal\n"); + ei_disconnect(ei); + return; + } + + const char *key; + rc = sd_bus_message_read(response, "a{sv}", 1, &key, "h", &eisfd); + if (rc < 0) { + log_error(ei, "Failed to extract fd, invalid message format: %s\n", strerror(-rc)); + goto out; + } + + if (!streq(key, "fd")) { + log_error(ei, "Invalid key '%s', expected 'fd'\n", key); goto out; } @@ -162,25 +183,54 @@ portal_response_received(sd_bus_message *m, void *userdata, sd_bus_error *error) /* We'll only get this signal once */ portal->slot = sd_bus_slot_unref(portal->slot); - int rc = sd_bus_message_read(m, "u", &response); + int rc = sd_bus_message_read(m, "u", &response); if (rc < 0) { log_error(ei, "Failed to read response from signal: %s\n", strerror(-rc)); ei_disconnect(ei); return 0; } - log_debug(ei, "Portal EmulateInput reponse is %d\n", response); + log_debug(ei, "Portal CreateSession reponse is %d\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) { + log_error(ei, "Failed to read session handle from signal: %s\n", strerror(-rc)); + ei_disconnect(ei); + return 0; + } + + if (!streq(key, "session_handle")) { + log_error(ei, "Invalid or unhandled option: %ss\n", key); + ei_disconnect(ei); + return 0; + } + } + + /* FIXME: we need to subscribe to the sessions Close signal */ if (response != 0) { ei_disconnect(ei); return 0; } - portal_connect(portal); + portal_connect(portal, session_handle); return 0; } +static int +session_closed_received(sd_bus_message *m, void *userdata, sd_bus_error *error) +{ + struct ei_portal *portal = userdata; + struct ei *ei = ei_portal_parent(portal); + + log_error(ei, "Session closed received"); + return 0; +} + static void dbus_dispatch(struct source *source, void *data) { @@ -221,6 +271,8 @@ portal_init(struct ei *ei, const char *busname) _cleanup_free_ char *token = NULL; _cleanup_free_ char *handle = xdp_token(bus, &token); + _cleanup_free_ char *session_token = xdp_token(bus, &session_token); + _cleanup_free_ char *session_handle = xdp_token(bus, &session_token); rc = sd_bus_match_signal(bus, &slot, busname, @@ -234,16 +286,30 @@ portal_init(struct ei *ei, const char *busname) return -ECONNREFUSED; } + rc = sd_bus_match_signal(bus, &slot, + busname, + session_handle, + "org.freedesktop.portal.Session", + "Closed", + session_closed_received, + portal); + if (rc < 0) { + log_error(ei, "Failed to subscribe to signal: %s\n", strerror(-rc)); + return -ECONNREFUSED; + } + rc = sd_bus_call_method(bus, busname, "/org/freedesktop/portal/desktop", "org.freedesktop.portal.EmulatedInput", - "EmulateInput", + "CreateSession", &error, &response, - "a{sv}", 1, + "a{sv}", 2, "handle_token", /* string key */ - "s", token /* variant string */ + "s", token, /* variant string */ + "session_handle_token", /* string key */ + "s", session_token /* variant string */ ); if (rc < 0) { diff --git a/tools/eis-fake-impl-portal.c b/tools/eis-fake-impl-portal.c new file mode 100644 index 0000000..7120df1 --- /dev/null +++ b/tools/eis-fake-impl-portal.c @@ -0,0 +1,371 @@ +/* + * Copyright © 2021 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" + +#include +#include +#include +#include +#include + +#include "util-io.h" +#include "util-list.h" +#include "util-mem.h" +#include "util-logger.h" +#include "util-strings.h" + +#include "libreis.h" + +#include + +DEFINE_UNREF_CLEANUP_FUNC(reis); + +struct session { + unsigned int version; + struct list link; + struct sd_bus_slot *slot; + char *handle; +}; + +struct portal { + unsigned int version; + struct logger *logger; + char *busname; + + struct list sessions; +} portal; + + +#define call(_call) do { \ + int _rc = _call; \ + if (_rc < 0) { \ + log_error(portal, "Failed with %s %s:%d\n", strerror(-_rc), __func__, __LINE__); \ + return _rc; \ + } } while(0) + +static int +session_close(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) +{ + sd_bus_reply_method_return(m, ""); + /* Not implemented */ + return 0; +} + +static const sd_bus_vtable session_vtable[] = { + SD_BUS_VTABLE_START(0), + SD_BUS_METHOD("Close", "", "", session_close, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_PROPERTY("version", "u", NULL, offsetof(struct session, version), SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_VTABLE_END, +}; + +static int +create_session_object(struct portal *portal, + sd_bus *bus, + const char *objectpath) +{ + struct session *session = calloc(sizeof *session, 1); + + session->handle = xstrdup(objectpath); + + call(sd_bus_add_object_vtable(bus, &session->slot, + objectpath, + "org.freedesktop.impl.portal.Session", + session_vtable, + session)); + + list_append(&portal->sessions, &session->link); + + log_debug(portal, "Session created on %s\n", objectpath); + return 0; +} + +static int +request_close(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) +{ + /* We don't live long enough for this to be called */ + return 0; +} + +static const sd_bus_vtable request_vtable[] = { + SD_BUS_VTABLE_START(0), + SD_BUS_METHOD("Close", "", "", request_close, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_VTABLE_END, +}; + +static int +create_request_object(struct portal *portal, + sd_bus *bus, + const char *objectpath, + const char *session_path) +{ + _unref_(sd_bus_slot) *slot = NULL; + + create_session_object(portal, bus, session_path); + + call(sd_bus_add_object_vtable(bus, &slot, + objectpath, + "org.freedesktop.impl.portal.Request", + request_vtable, + NULL)); + + int response = 0; + log_debug(portal, "emitting Response %d on %s\n", response, objectpath); + return sd_bus_emit_signal(bus, + objectpath, + "org.freedesktop.impl.portal.Request", + "Response", + "ua{sv}", + response, + 1, + "session_handle",/* string key */ + "s", session_path /* variant string */ + ); + /* note: _unref_ removes object immediately */ +} + +static int +portal_create_session(sd_bus_message *m, void *userdata, + sd_bus_error *ret_error) +{ + struct portal *portal = userdata; + static int session_id = 1234; + + const char *handle; + const char *session_handle; + const char *app_id; + + sd_bus_message_read(m, "oosa{sv}", + &handle, + &session_handle, + &app_id, 0); + + log_debug(portal, "CreateSession: app-id %s, session %s, handle %s\n", app_id, session_handle, handle); + + _cleanup_free_ char *sid = xaprintf("%d", session_id++); + + call(sd_bus_reply_method_return(m, "ua{sv}", 0, 1, "session_id", "s", sid)); + + return create_request_object(portal, sd_bus_message_get_bus(m), handle, session_handle); +} + +static void +set_prop_cmdline(struct reis *reis) +{ + char path[PATH_MAX]; + + xsnprintf(path, sizeof(path), "/proc/%u/cmdline", getpid()); + _cleanup_close_ int fd = open(path, O_RDONLY); + if (fd < 0) + return; + + int len; + if ((len = read(fd, path, sizeof(path) - 1)) < 0) + return; + path[len] = '\0'; + + reis_set_property_with_permissions(reis, "ei.application.cmdline", path, REIS_PROPERTY_PERM_NONE); +} + +static void +set_prop_pid(struct reis *reis) +{ + char pid[64]; + + xsnprintf(pid, sizeof(pid), "%u", getpid()); + reis_set_property_with_permissions(reis, "ei.application.pid", pid, REIS_PROPERTY_PERM_NONE); +} + +static void +set_prop_type(struct reis *reis) +{ + reis_set_property_with_permissions(reis, "ei.connection.type", "portal", REIS_PROPERTY_PERM_NONE); +} + +static int +portal_connect_to_eis(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) +{ + struct portal *portal = userdata; + + log_debug(portal, "Got a connect request\n"); + + const char *xdg = getenv("XDG_RUNTIME_DIR"); + if (!xdg) + return -ENOENT; + + const char *session_handle; + const char *app_id; + call(sd_bus_message_read(m, "os", &session_handle, &app_id)); + + log_debug(portal, "Connect request session %s appid %s\n", session_handle, app_id); + + struct session *session; + bool found = false; + list_for_each_safe(session, &portal->sessions, link) { + if (streq(session->handle, session_handle)) { + found = true; + break; + } + } + + /* We're aborting here because it's a client bug and this is just a + * fake portal */ + if (!found) { + log_error(portal, "Invalid session handle %s\n", session_handle); + abort(); + } + + log_error(portal, "Valid session %s, connecting to EIS\n", session_handle); + + _cleanup_free_ char *sockpath = xaprintf("%s/eis-0", xdg); + int fd = xconnect(sockpath); + if (fd < 0) { + log_error(portal, "Failed to connect to EIS (%s)\n", strerror(-fd)); + return sd_bus_reply_method_return(m, "ua{sv}", 1, 0); + } + + _unref_(reis) *reis = reis_new(fd); + assert(reis); + set_prop_pid(reis); + set_prop_cmdline(reis); + set_prop_type(reis); + + log_debug(portal, "passing fd %d\n", fd); + return sd_bus_reply_method_return(m, "ua{sv}", 0, + 1, /* array length */ + "fd", "h", fd /* name: "fd", type handle, value fd */); +} + +static const sd_bus_vtable portal_vtable[] = { + SD_BUS_VTABLE_START(0), + SD_BUS_METHOD("CreateSession", "oosa{sv}", "ua{sv}", portal_create_session, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD("ConnectToEIS", "osa{sv}", "ua{sv}", portal_connect_to_eis, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_PROPERTY("version", "u", NULL, offsetof(struct portal, version), SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_VTABLE_END, +}; + +static int +run(struct portal *portal) +{ + _unref_(sd_bus) *bus = NULL; + _unref_(sd_bus_slot) *slot = NULL; + int rc = sd_bus_open_user(&bus); + if (rc < 0) + return rc; + + rc = sd_bus_add_object_vtable(bus, &slot, + "/org/freedesktop/portal/desktop", + "org.freedesktop.impl.portal.EmulatedInput", + portal_vtable, + portal); + if (rc < 0) + return rc; + + sd_bus_add_object_manager(bus, NULL, "/org/freedesktop/portal/desktop"); + + log_debug(portal, "Portal object at: /org/freedesktop/ei/EmulatedInput\n"); + + rc = sd_bus_request_name(bus, portal->busname, 0); + if (rc < 0) + return rc; + + log_debug(portal, "Portal DBus name: %s\n", portal->busname); + + _unref_(sd_event) *event = NULL; + rc = sd_event_default(&event); + if (rc < 0) + return rc; + + rc = sd_event_set_watchdog(event, true); + if (rc < 0) + return rc; + + rc = sd_bus_attach_event(bus, event, 0); + if (rc < 0) + return rc; + + return sd_event_loop(event); +} + +static void +usage(FILE *fp, const char *argv0) +{ + fprintf(fp, + "Usage: %s [--busname=a.b.c.d]\n" + "\n" + "Emulates an XDG Desktop portal for the org.freedesktop.impl.portal.EmulatedInput interface\n" + "\n" + "Options:\n" + " --busname use the given busname instead of the default org.freedesktop.impl.portal.eis.EmulatedInput\n" + "", + basename(argv0)); +} + +int +main(int argc, char **argv) +{ + _cleanup_free_ char *busname = xstrdup("org.freedesktop.impl.portal.eis.EmulatedInput"); + + while (1) { + enum opts { + OPT_BUSNAME, + }; + static struct option long_opts[] = { + { "busname", required_argument, 0, OPT_BUSNAME}, + { "help", no_argument, 0, 'h'}, + { .name = NULL }, + }; + + int optind = 0; + int c = getopt_long(argc, argv, "h", long_opts, &optind); + if (c == -1) + break; + + switch(c) { + case 'h': + usage(stdout, argv[0]); + return EXIT_SUCCESS; + case OPT_BUSNAME: + free(busname); + busname = xstrdup(optarg); + break; + default: + usage(stderr, argv[0]); + return EXIT_FAILURE; + } + } + + list_init(&portal.sessions); + portal.version = 1; + portal.busname = steal(&busname); + portal.logger = logger_new("impl.portal", NULL); + logger_set_priority(portal.logger, LOGGER_DEBUG); + + int rc = run(&portal); + if (rc < 0) + fprintf(stderr, "Failed to start fake portal: %s\n", strerror(-rc)); + + logger_unref(portal.logger); + free(portal.busname); + return rc == 0; +} diff --git a/tools/eis-fake-portal.c b/tools/eis-fake-portal.c index 7c62811..2fe9590 100644 --- a/tools/eis-fake-portal.c +++ b/tools/eis-fake-portal.c @@ -30,6 +30,7 @@ #include #include "util-io.h" +#include "util-list.h" #include "util-mem.h" #include "util-logger.h" #include "util-strings.h" @@ -40,9 +41,17 @@ DEFINE_UNREF_CLEANUP_FUNC(reis); +struct session { + struct list link; + struct sd_bus_slot *slot; + char *handle; +}; + struct portal { struct logger *logger; char *busname; + + struct list sessions; } portal; @@ -53,6 +62,41 @@ struct portal { return _rc; \ } } while(0) +static int +session_close(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) +{ + /* Not implemented */ + return 0; +} + +static const sd_bus_vtable session_vtable[] = { + SD_BUS_VTABLE_START(0), + SD_BUS_METHOD("Close", "", "", session_close, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_SIGNAL("Closed", "ua{sv}", 0), + SD_BUS_VTABLE_END, +}; + +static int +create_session_object(struct portal *portal, + sd_bus *bus, + const char *objectpath) +{ + struct session *session = calloc(sizeof *session, 1); + + session->handle = xstrdup(objectpath); + + call(sd_bus_add_object_vtable(bus, &session->slot, + objectpath, + "org.freedesktop.portal.Session", + session_vtable, + NULL)); + + list_append(&portal->sessions, &session->link); + + log_debug(portal, "Session created on %s\n", objectpath); + return 0; +} + static int request_close(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) { @@ -70,9 +114,13 @@ static const sd_bus_vtable request_vtable[] = { static int create_request_object(struct portal *portal, sd_bus *bus, - const char *objectpath) + const char *objectpath, + const char *session_path) { _unref_(sd_bus_slot) *slot = NULL; + + create_session_object(portal, bus, session_path); + call(sd_bus_add_object_vtable(bus, &slot, objectpath, "org.freedesktop.portal.Request", @@ -87,7 +135,9 @@ create_request_object(struct portal *portal, "Response", "ua{sv}", response, - 0 /* Array size */ + 1, + "session_handle",/* string key */ + "s", session_path /* variant string */ ); /* note: _unref_ removes object immediately */ } @@ -108,18 +158,36 @@ sender_token(const char *input) } static int -portal_emulate_input(sd_bus_message *m, void *userdata, - sd_bus_error *ret_error) +portal_create_session(sd_bus_message *m, void *userdata, + sd_bus_error *ret_error) { struct portal *portal = userdata; call(sd_bus_message_enter_container(m, 'a', "{sv}")); - const char *key, *handle_token; - call(sd_bus_message_read(m, "{sv}", &key, "s", &handle_token)); - /* we only have a handle token in the dict so far */ - if (!streq(key, "handle_token")) + const char *session_token = NULL; + const char *handle_token = NULL; + + const char *key, *val; + call(sd_bus_message_read(m, "{sv}", &key, "s", &val)); + if (streq(key, "handle_token")) + handle_token = val; + else if (streq(key, "session_handle_token")) + session_token = val; + else return -EINVAL; + + call(sd_bus_message_read(m, "{sv}", &key, "s", &val)); + if (streq(key, "handle_token")) + handle_token = val; + else if (streq(key, "session_handle_token")) + session_token = val; + else + return -EINVAL; + + assert(handle_token); + assert(session_token); + call(sd_bus_message_exit_container(m)); _cleanup_free_ char *sender = sender_token(sd_bus_message_get_sender(m)); @@ -135,8 +203,13 @@ portal_emulate_input(sd_bus_message *m, void *userdata, handle_token); call(sd_bus_reply_method_return(m, "o", objpath)); + _cleanup_free_ char *session_path = xaprintf("%s/session/%s/%s", + "/org/freedesktop/portal/desktop", + sender, + session_token); + /* now create the object */ - return create_request_object(portal, sd_bus_message_get_bus(m), objpath); + return create_request_object(portal, sd_bus_message_get_bus(m), objpath, session_path); } static void @@ -173,8 +246,7 @@ set_prop_type(struct reis *reis) } static int -portal_connect(sd_bus_message *m, void *userdata, - sd_bus_error *ret_error) +portal_connect_to_eis(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) { struct portal *portal = userdata; @@ -182,6 +254,28 @@ portal_connect(sd_bus_message *m, void *userdata, if (!xdg) return -ENOENT; + const char *session_handle; + + call(sd_bus_message_read(m, "oa{sv}", &session_handle, 0)); + + struct session *session; + bool found = false; + list_for_each_safe(session, &portal->sessions, link) { + if (streq(session->handle, session_handle)) { + found = true; + break; + } + } + + /* We're aborting here because it's a client bug and this is just a + * fake portal */ + if (!found) { + log_error(portal, "Invalid session handle %s\n", session_handle); + abort(); + } + + log_info(portal, "Valid session %s, connecting to EIS\n", session_handle); + _cleanup_free_ char *sockpath = xaprintf("%s/eis-0", xdg); int handle = xconnect(sockpath); if (handle < 0) { @@ -196,13 +290,15 @@ portal_connect(sd_bus_message *m, void *userdata, set_prop_type(reis); log_debug(portal, "passing Handle %d\n", handle); - return sd_bus_reply_method_return(m, "h", handle); + return sd_bus_reply_method_return(m, "ua{sv}", 0, + 1, /* array size */ + "fd", "h", handle); } static const sd_bus_vtable portal_vtable[] = { SD_BUS_VTABLE_START(0), - SD_BUS_METHOD("EmulateInput", "a{sv}", "o", portal_emulate_input, SD_BUS_VTABLE_UNPRIVILEGED), - SD_BUS_METHOD("Connect", "a{sv}", "h", portal_connect, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD("CreateSession", "a{sv}", "o", portal_create_session, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD("ConnectToEIS", "oa{sv}", "ua{sv}", portal_connect_to_eis, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_VTABLE_END, }; @@ -295,6 +391,7 @@ main(int argc, char **argv) } } + list_init(&portal.sessions); portal.busname = steal(&busname); portal.logger = logger_new("portal", NULL); logger_set_priority(portal.logger, LOGGER_DEBUG);