From 6b3d9255cf5271b7b614c8dd4815c03d736c8fcb Mon Sep 17 00:00:00 2001 From: Peter Hutterer Date: Tue, 11 Aug 2020 20:54:01 +1000 Subject: [PATCH] libei: implement org.freedesktop.portal.EmulatedInput support The current implementation of that portal has two methods: EmulateInput to authenticate and Connect to get the fd to the EIS implementation. The portal implementation is in charge of finding EIS and restricting it if need be. This uses libsystemd because we can integrate that with epoll and our libei_dispatch() method. GDBus requires a glib mainloop, so it's not really suitable here. Given how simple this is anyway, it's easy to just do the DBus bits in the caller and then hand the fd to ei_setup_backend_fd(). A eis-fake-portal is provided for testing, this "portal" can use the custom portal bus name and connect the eis-demo-client to the eis-demo-server. Signed-off-by: Peter Hutterer --- .gitlab-ci.yml | 8 ++ .gitlab-ci/ci.template | 8 ++ README.md | 42 +++++- meson.build | 30 ++++- meson_options.txt | 1 + src/libei-portal.c | 291 ++++++++++++++++++++++++++++++++++++++++ src/libei-private.h | 3 + src/libei-stubs.c | 38 ++++++ src/libei.c | 5 +- src/libei.h | 11 ++ src/util-sources.c | 6 + src/util-sources.h | 3 + tools/ei-demo-client.c | 32 ++++- tools/eis-fake-portal.c | 263 ++++++++++++++++++++++++++++++++++++ 14 files changed, 724 insertions(+), 17 deletions(-) create mode 100644 meson_options.txt create mode 100644 src/libei-portal.c create mode 100644 src/libei-stubs.c create mode 100644 tools/eis-fake-portal.c diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 354fd11..2f3f9b6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -232,6 +232,14 @@ build-no-libxkcommon-nodeps@fedora:32: before_script: - dnf remove -y libxkcommon-devel +build-no-portal@fedora:32: + extends: + - .fedora-build@template + before_script: + - dnf remove -y libsystemd-devel + variables: + MESON_ARGS: "-Dportal=false" + valgrind@fedora:32: extends: - .fedora-build@template diff --git a/.gitlab-ci/ci.template b/.gitlab-ci/ci.template index e3e4dcf..a85eb6f 100644 --- a/.gitlab-ci/ci.template +++ b/.gitlab-ci/ci.template @@ -253,6 +253,14 @@ build-no-libxkcommon-nodeps@{{distro.name}}:{{version}}: before_script: - dnf remove -y libxkcommon-devel +build-no-portal@{{distro.name}}:{{version}}: + extends: + - .{{distro.name}}-build@template + before_script: + - dnf remove -y libsystemd-devel + variables: + MESON_ARGS: "-Dportal=false" + valgrind@{{distro.name}}:{{version}}: extends: - .{{distro.name}}-build@template diff --git a/README.md b/README.md index 0e08e83..2282513 100644 --- a/README.md +++ b/README.md @@ -157,13 +157,43 @@ Open questions ### Flatpak integration -Where flatpak portals are in use, `libei` will communicate with -the portal and `libeis` with the portal implementation (e.g. -`xdg-desktop-portal-gdk`). The portal is reponsible for -allowing the client to connect and restrictions on the devices a client may -create. `libeis` will run in a private namespace of the compositor. +Where flatpak portals are in use, `libei` can communicate with +the portal through a custom backend. The above diagram modified for +Flatpak would be: -The portal **may** control suspending/resuming devices (in addition to the +``` + +--------------------+ + | Wayland compositor |_ + +--------------------+ \ + | libinput | libeis | \_wayland______ + +----------+---------+ \ + | [eis-0.socket] \ + /dev/input/ / \\ +-------+------------------+ + | ======>| libei | Wayland client A | + | after +-------+------------------+ + initial| handover / + connection| / initial request + | / dbus[org.freedesktop.portal.EmulatedInput] + +--------------------+ + | xdg-desktop-portal | + +--------------------+ +``` + +The current approach works so that +- the compositor starts an `libeis` socket backend at `$XDG_RUNTIME_DIR/eis-0` +- `xdg-desktop-portal` provides `org.freedesktop.portal.EmulatedInput` +- a client connects to the `xdg-desktop-portal` to request emulated input +- `xdg-desktop-portal` authenticates a client and opens the initial + connection to the `libeis` socket. It restricts the capabilities available + on that socket (e.g. sets the client name based on `app-id` using + `libreis`). +- `xdg-desktop-portal` hands over the file descriptor to the client which + can initialize a `libei` context +- from then on, `libei` and `libeis` talk directly to each other, the portal + has no further influence. + +This describes the **current** implementation. Changes to this approach are +likely, e.g. the portal **may** control suspending/resuming devices (in addition to the server). The UI for this is not yet sorted. ### Authentication diff --git a/meson.build b/meson.build index f25cfc9..d614e3f 100644 --- a/meson.build +++ b/meson.build @@ -50,12 +50,30 @@ src_libei = [ 'src/libei-fd.c', 'src/libei-proto.h', 'src/libei-proto.c', + 'src/libei-stubs.c', proto_headers, ] + +if get_option('portal') + dep_systemd = dependency('libsystemd') + config_h.set10('ENABLE_LIBEI_PORTAL', 1) + src_libei += [ + 'src/libei-portal.c', + ] +else + dep_systemd = dependency('', required: false) +endif + +deps_libei = [ + dep_libutil, + dep_protobuf, + dep_systemd, +] + lib_libei = shared_library('ei', src_libei, - dependencies: [dep_libutil, dep_protobuf], + dependencies: deps_libei, install: true ) install_headers('src/libei.h') @@ -69,6 +87,7 @@ pkgconfig.generate(lib_libei, description: 'Emulated Input client library', version: meson.project_version(), libraries: lib_libei, + variables: [ 'ei_portal=' + get_option('portal').to_string() ], ) src_libeis = [ @@ -136,6 +155,13 @@ executable('ei-demo-client', 'tools/ei-demo-client.c', dependencies: [dep_libutil, dep_libei, dep_libxkbcommon]) +if get_option('portal') + executable('eis-fake-portal', + 'tools/eis-fake-portal.c', + include_directories: 'src', + dependencies: [dep_libutil, dep_systemd]) +endif + # tests subproject('munit') @@ -166,7 +192,7 @@ test('unit-tests-ei', src_libei, include_directories: 'src', c_args: ['-D_enable_tests_'], - dependencies: [dep_unittest, dep_libutil, dep_protobuf])) + dependencies: deps_libei + [dep_unittest])) test('unit-tests-eis', executable('unit-tests-eis', diff --git a/meson_options.txt b/meson_options.txt new file mode 100644 index 0000000..97deb22 --- /dev/null +++ b/meson_options.txt @@ -0,0 +1 @@ +option('portal', type: 'boolean', value: 'true', description: 'Enable/disable org.freedesktop.portal support') diff --git a/src/libei-portal.c b/src/libei-portal.c new file mode 100644 index 0000000..7e8cd69 --- /dev/null +++ b/src/libei-portal.c @@ -0,0 +1,291 @@ +/* + * Copyright © 2020 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 "libei.h" +#include "libei-private.h" + +#include "util-io.h" +#include "util-macros.h" +#include "util-mem.h" +#include "util-object.h" +#include "util-strings.h" + +struct ei_portal { + struct object object; + struct source *bus_source; + sd_bus *bus; + sd_bus_slot *slot; + + char *busname; +}; + +static void +ei_portal_destroy(struct ei_portal *portal) +{ + free(portal->busname); + sd_bus_unref(portal->bus); +} + +OBJECT_IMPLEMENT_CREATE(ei_portal); +static +OBJECT_IMPLEMENT_PARENT(ei_portal, ei); +static +OBJECT_IMPLEMENT_REF(ei_portal); +static +OBJECT_IMPLEMENT_UNREF(ei_portal); +#define _cleanup_ei_portal_ _cleanup_(ei_portal_cleanup) + +static void +interface_portal_destroy(struct ei *ei, void *backend) +{ + struct ei_portal *portal = backend; + ei_portal_unref(portal); +} + +static const struct ei_backend_interface interface = { + .destroy = interface_portal_destroy, +}; + +static char * +xdp_token(sd_bus *bus, char **token_out) +{ + _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] = '_'; + } + + char *token = xaprintf("ei_%d", rand()); + *token_out = token; + + return xaprintf("/org/freedesktop/portal/desktop/request/%s/%s", sender, token); +} + +static void +portal_connect(struct ei_portal *portal) +{ + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *response = NULL; + struct sd_bus *bus = portal->bus; + struct ei *ei = ei_portal_parent(portal); + int eisfd; + + int rc = sd_bus_call_method(bus, portal->busname, + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.EmulatedInput", + "Connect", + &error, + &response, + "a{sv}", 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); + if (rc < 0) { + log_error(ei, "Failed to extract fd: %s\n", strerror(-rc)); + goto out; + } + + /* the fd is owned by the message */ + rc = xerrno(dup(eisfd)); + if (rc < 0) { + log_error(ei, "Failed to dup fd: %s\n", strerror(-rc)); + goto out; + } else { + eisfd = rc; + int flags = fcntl(eisfd, F_GETFL, 0); + fcntl(eisfd, F_SETFL, flags | O_NONBLOCK); + } + + log_debug(ei, "Initiating ei context with fd %d from portal\n", eisfd); + + /* We're done with DBus, lets clean up */ + source_remove(portal->bus_source); + source_unref(portal->bus_source); + portal->bus = sd_bus_unref(portal->bus); + + rc = ei_set_connection(ei, eisfd); +out: + if (rc < 0) { + log_error(ei, "Failed to set the connection: %s\n", strerror(-rc)); + ei_disconnect(ei); + } +} + +static int +portal_response_received(sd_bus_message *m, void *userdata, sd_bus_error *error) +{ + struct ei_portal *portal = userdata; + struct ei *ei = ei_portal_parent(portal); + unsigned response; + + assert(m); + assert(portal); + + /* We'll only get this signal once */ + portal->slot = sd_bus_slot_unref(portal->slot); + + 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); + + if (response != 0) { + ei_disconnect(ei); + return 0; + } + + portal_connect(portal); + + return 0; +} + +static void +dbus_dispatch(struct source *source, void *data) +{ + struct ei_portal *portal = data; + + /* We need to ref the bus here, portal_connect() may remove + * portal->bus but that needs to stay valid here until the end of + * the loop. + */ + _cleanup_(sd_bus_unrefp) struct sd_bus *bus = sd_bus_ref(portal->bus); + + int rc; + do { + rc = sd_bus_process(bus, NULL); + } while (rc > 0); + + if (rc != 0) { + log_error(ei_portal_parent(portal), "dbus processing failed with %s", strerror(-rc)); + } +} + +static int +portal_init(struct ei *ei, const char *busname) +{ + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *response = NULL; + _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL; + _cleanup_(sd_bus_slot_unrefp) sd_bus_slot *slot = NULL; + _cleanup_ei_portal_ struct ei_portal *portal = ei_portal_create(&ei->object); + const char *path = NULL; + + int rc = sd_bus_open_user(&bus); + if (rc < 0 ) { + log_error(ei, "Failed to init dbus: %s\n", strerror(-rc)); + return -ECONNREFUSED; + } + + _cleanup_free_ char *token = NULL; + _cleanup_free_ char *handle = xdp_token(bus, &token); + + rc = sd_bus_match_signal(bus, &slot, + busname, + handle, + "org.freedesktop.portal.Request", + "Response", + portal_response_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", + &error, + &response, + "a{sv}", 1, + "handle_token", /* string key */ + "s", token /* variant string */ + ); + + if (rc < 0) { + log_error(ei, "Failed to call method: %s\n", strerror(-rc)); + return -ECONNREFUSED; + } + + rc = sd_bus_message_read(response, "o", &path); + if (rc < 0) { + log_error(ei, "Failed to parse response: %s\n", strerror(-rc)); + return -ECONNREFUSED; + } + + log_debug(ei, "portal Response object is %s\n", path); + + struct source *s = source_new(sd_bus_get_fd(bus), dbus_dispatch, portal); + source_never_close_fd(s); /* the bus object handles the fd */ + rc = sink_add_source(ei->sink, s); + if (rc == 0) { + portal->bus_source = source_ref(s); + portal->bus = sd_bus_ref(bus); + portal->slot = sd_bus_slot_ref(slot); + } + + portal->busname = xstrdup(busname); + + ei->backend = ei_portal_ref(portal); + ei->backend_interface = interface; + + source_unref(s); + + return 0; +} + +_public_ int +ei_setup_backend_portal(struct ei *ei) +{ + return portal_init(ei, "org.freedesktop.portal.Desktop"); +} + +_public_ int +ei_setup_backend_portal_busname(struct ei *ei, const char *busname) +{ + return portal_init(ei, busname); +} diff --git a/src/libei-private.h b/src/libei-private.h index 9996d33..07225df 100644 --- a/src/libei-private.h +++ b/src/libei-private.h @@ -113,6 +113,9 @@ ei_init(struct ei *ei); int ei_set_connection(struct ei *ei, int fd); +void +ei_disconnect(struct ei *ei); + int ei_add_device(struct ei_device *device); diff --git a/src/libei-stubs.c b/src/libei-stubs.c new file mode 100644 index 0000000..09c32c4 --- /dev/null +++ b/src/libei-stubs.c @@ -0,0 +1,38 @@ +/* + * Copyright © 2020 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 "libei.h" + +#include "util-macros.h" + +#if !ENABLE_LIBEI_PORTAL +_public_ int +ei_setup_backend_portal(struct ei *ei) +{ + return -ENOSYS; +} +#endif diff --git a/src/libei.c b/src/libei.c index 0f341b9..68401ff 100644 --- a/src/libei.c +++ b/src/libei.c @@ -66,9 +66,6 @@ OBJECT_IMPLEMENT_GETTER(ei_event, type, enum ei_event_type); _public_ OBJECT_IMPLEMENT_GETTER(ei_event, device, struct ei_device*); -static void -ei_disconnect(struct ei *ei); - static void ei_destroy(struct ei *ei) { @@ -276,7 +273,7 @@ connection_send_key(struct ei *ei, struct ei_device *device, return ei_proto_send_key(ei, device, k, is_press); } -static void +void ei_disconnect(struct ei *ei) { if (ei->state == EI_STATE_DISCONNECTED || diff --git a/src/libei.h b/src/libei.h index 4862b28..bbe0e2c 100644 --- a/src/libei.h +++ b/src/libei.h @@ -238,6 +238,17 @@ ei_unref(struct ei *ei); void ei_set_user_data(struct ei *ei, void *user_data); +/** + * Connect to the org.freedesktop.portal.Desktop portal + * + * @return 0 on success or a negative errno on failure + */ +int +ei_setup_backend_portal(struct ei *ei); + +int +ei_setup_backend_portal_busname(struct ei *ei, const char *busname); + /** * Return the custom data pointer for this context. libei will not look at or * modify the pointer. Use ei_set_user_data() to change the user data. diff --git a/src/util-sources.c b/src/util-sources.c index 7157c84..778660c 100644 --- a/src/util-sources.c +++ b/src/util-sources.c @@ -114,6 +114,12 @@ source_new(int sourcefd, source_dispatch_t dispatch, void *user_data) } +void +source_never_close_fd(struct source *s) +{ + s->close_behavior = SOURCE_CLOSE_FD_NEVER; +} + static void sink_destroy(struct sink *sink) { diff --git a/src/util-sources.h b/src/util-sources.h index aa61c66..65b2610 100644 --- a/src/util-sources.h +++ b/src/util-sources.h @@ -82,6 +82,9 @@ source_set_user_data(struct source *source, void *user_data); struct source * source_new(int fd, source_dispatch_t dispatch, void *user_data); +void +source_never_close_fd(struct source *s); + struct sink * sink_new(void); diff --git a/tools/ei-demo-client.c b/tools/ei-demo-client.c index a79398e..7584db1 100644 --- a/tools/ei-demo-client.c +++ b/tools/ei-demo-client.c @@ -135,7 +135,7 @@ static void usage(FILE *fp, const char *argv0) { fprintf(fp, - "Usage: %s [--verbose] [--socket] [--layout=us]\n" + "Usage: %s [--verbose] [--socket|--portal] [--busname=a.b.c.d] [--layout=us]\n" "\n" "Start an EI demo client. The client will connect to EIS\n" "with the chosen backend (default: socket) and emulate pointer\n" @@ -144,8 +144,11 @@ usage(FILE *fp, const char *argv0) "Options:\n" " --socket Use the socket backend. The socket path is $LIBEI_SOCKET if set, \n" " otherwise XDG_RUNTIME/eis-0\n" + " --portal Use the portal backend.\n" + " --busname Use the given busname (default: org.freedesktop.portal.Desktop)\n" " --layout Use the given XKB layout (requires libxkbcommon). Default: none\n" - " --verbose Enable debugging output\n", + " --verbose Enable debugging output\n" + "", argv0); } @@ -153,21 +156,28 @@ int main(int argc, char **argv) { enum { SOCKET, + PORTAL, } backend = SOCKET; bool verbose = false; const char *layout = NULL; + _cleanup_free_ char *busname = xstrdup("org.freedesktop.portal.Desktop"); + while (1) { enum { OPT_BACKEND_SOCKET, + OPT_BACKEND_PORTAL, + OPT_BUSNAME, OPT_LAYOUT, OPT_VERBOSE, }; static struct option long_opts[] = { {"socket", no_argument, 0, OPT_BACKEND_SOCKET}, - {"layout", required_argument, 0, OPT_LAYOUT}, - {"verbose", no_argument, 0, OPT_VERBOSE}, - {"help", no_argument, 0, 'h'}, + { "portal", no_argument, 0, OPT_BACKEND_PORTAL}, + { "busname", required_argument, 0, OPT_BUSNAME}, + { "layout", required_argument, 0, OPT_LAYOUT}, + { "verbose", no_argument, 0, OPT_VERBOSE}, + { "help", no_argument, 0, 'h'}, {NULL}, }; @@ -186,6 +196,13 @@ int main(int argc, char **argv) case OPT_BACKEND_SOCKET: backend = SOCKET; break; + case OPT_BACKEND_PORTAL: + backend = PORTAL; + break; + case OPT_BUSNAME: + free(busname); + busname = xstrdup(optarg); + break; case OPT_LAYOUT: layout = optarg; break; @@ -209,6 +226,11 @@ int main(int argc, char **argv) const char SOCKETNAME[] = "eis-0"; colorprint("connecting to %s\n", SOCKETNAME); rc = ei_setup_backend_socket(ei, getenv("LIBEI_SOCKET") ? NULL : SOCKETNAME); + } else if (backend == PORTAL) { +#if ENABLE_LIBEI_PORTAL + colorprint("connecting to %s\n", busname); + rc = ei_setup_backend_portal_busname(ei, busname); +#endif } if (rc != 0) { diff --git a/tools/eis-fake-portal.c b/tools/eis-fake-portal.c new file mode 100644 index 0000000..43c47a0 --- /dev/null +++ b/tools/eis-fake-portal.c @@ -0,0 +1,263 @@ +/* + * Copyright © 2020 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 "util-io.h" +#include "util-mem.h" +#include "util-logger.h" +#include "util-strings.h" + +#include + +#define _cleanup_sd_bus_ _cleanup_(sd_bus_unrefp) +#define _cleanup_sd_bus_slot_ _cleanup_(sd_bus_slot_unrefp) +#define _cleanup_sd_event_ _cleanup_(sd_event_unrefp) + +struct portal { + struct logger *logger; + char *busname; +} 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 +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_SIGNAL("Response", "ua{sv}", 0), + SD_BUS_VTABLE_END, +}; + +static int +create_request_object(struct portal *portal, + sd_bus *bus, + const char *objectpath) +{ + _cleanup_sd_bus_slot_ sd_bus_slot *slot = NULL; + call(sd_bus_add_object_vtable(bus, &slot, + objectpath, + "org.freedesktop.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.portal.Request", + "Response", + "ua{sv}", + response, + 0 /* Array size */ + ); + /* note: _cleanup_ removes object immediately */ +} + +static char * +sender_token(const char *input) +{ + if (!input) + return NULL; + + char *token = strstrip(input, ":"); + for (size_t idx = 0; token[idx]; idx++) { + if (token[idx] == '.') + token[idx] = '_'; + } + + return token; +} + +static int +portal_emulate_input(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")) + return -EINVAL; + call(sd_bus_message_exit_container(m)); + + _cleanup_free_ char *sender = sender_token(sd_bus_message_get_sender(m)); + if (!sender) + return -ENOMEM; + + /* Send back the object path of the object we're about to create. We + * then create the object, so if that fails we have a problem but + * meh, this is for testing only .*/ + _cleanup_free_ char *objpath = xaprintf("%s/request/%s/%s", + "/org/freedesktop/portal/desktop", + sender, + handle_token); + call(sd_bus_reply_method_return(m, "o", objpath)); + + /* now create the object */ + return create_request_object(portal, sd_bus_message_get_bus(m), objpath); +} + +static int +portal_connect(sd_bus_message *m, void *userdata, + sd_bus_error *ret_error) +{ + struct portal *portal = userdata; + + const char *xdg = getenv("XDG_RUNTIME_DIR"); + if (!xdg) + return -ENOENT; + + _cleanup_free_ char *sockpath = xaprintf("%s/eis-0", xdg); + int handle = xconnect(sockpath); + log_debug(portal, "passing Handle %d\n", handle); + return sd_bus_reply_method_return(m, "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_VTABLE_END, +}; + +static int +run(struct portal *portal) +{ + _cleanup_sd_bus_ sd_bus *bus = NULL; + _cleanup_sd_bus_slot_ 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.portal.EmulatedInput", + portal_vtable, + portal); + if (rc < 0) + return rc; + + log_debug(portal, "Portal object at: /org/freedesktop/portal/desktop\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); + + _cleanup_sd_event_ struct 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.portal.EmulatedInput interface\n" + "\n" + "Options:\n" + " --busname use the given busname instead of the default org.freedesktop.libei.Desktop\n" + "", + basename(argv0)); +} + +int +main(int argc, char **argv) +{ + _cleanup_free_ char *busname = xstrdup("org.freedesktop.portal.Desktop"); + + while (1) { + enum opts { + OPT_BUSNAME, + }; + static struct option long_opts[] = { + { "busname", required_argument, 0, OPT_BUSNAME}, + { "help", no_argument, 0, 'h'}, + { 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; + } + } + + portal.busname = steal(&busname); + portal.logger = logger_new("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; +}