From 11ef9999b66c241be9e19f239ec63faa95a4ff17 Mon Sep 17 00:00:00 2001 From: Alexander Larsson Date: Mon, 26 Mar 2018 15:14:00 +0200 Subject: [PATCH] Add flatpak portal This listens to org.freedesktop.portal.Flatpak and lets flatpak sandboxes do flatpak specific things. Initially this only allows access to "Spawn", which lets you start a new copy of the current or latest version of the calling app, optionally with sandboxing. This allows the app to re-start itself with the latest version after an update, and the sandboxing is useful for apps that want to manually sandbox part of themselves. You can also expose one or more subdirectories of ~/.var/app/$appid/sandbox/ to the app, read-write or read-only. This is useful to communicate with the sandbox. --- .gitignore | 5 +- Makefile.am | 1 + common/Makefile.am.inc | 2 - common/flatpak-utils.c | 1 - data/Makefile.am.inc | 2 + data/org.freedesktop.portal.Flatpak.xml | 62 ++ portal/Makefile.am.inc | 39 + portal/flatpak-portal-app-info.c | 225 +++++ portal/flatpak-portal-app-info.h | 43 + {common => portal}/flatpak-portal-error.c | 0 {common => portal}/flatpak-portal-error.h | 0 portal/flatpak-portal.c | 810 ++++++++++++++++++ portal/flatpak-portal.h | 31 + portal/flatpak-portal.service.in | 7 + .../org.freedesktop.portal.Flatpak.service.in | 4 + 15 files changed, 1226 insertions(+), 6 deletions(-) create mode 100644 data/org.freedesktop.portal.Flatpak.xml create mode 100644 portal/Makefile.am.inc create mode 100644 portal/flatpak-portal-app-info.c create mode 100644 portal/flatpak-portal-app-info.h rename {common => portal}/flatpak-portal-error.c (100%) rename {common => portal}/flatpak-portal-error.h (100%) create mode 100644 portal/flatpak-portal.c create mode 100644 portal/flatpak-portal.h create mode 100644 portal/flatpak-portal.service.in create mode 100644 portal/org.freedesktop.portal.Flatpak.service.in diff --git a/.gitignore b/.gitignore index 5ada4804..63375d7f 100644 --- a/.gitignore +++ b/.gitignore @@ -33,15 +33,14 @@ gtk-doc.make flatpak flatpak-*.tar.xz flatpak-session-helper -xdg-document-portal -xdg-permission-store -flatpak-builder +flatpak-portal *~ profile/flatpak.sh flatpak-dbus.[ch] flatpak-document-dbus.[ch] flatpak-systemd-dbus.[ch] flatpak-resources.[ch] +flatpak-portal-dbus.[ch] flatpak-dbus-proxy permission-store-dbus.[ch] flatpak-system-helper diff --git a/Makefile.am b/Makefile.am index ce2508ff..d379d2bf 100644 --- a/Makefile.am +++ b/Makefile.am @@ -92,6 +92,7 @@ include data/Makefile.am.inc include app/Makefile.am.inc include lib/Makefile.am.inc include session-helper/Makefile.am.inc +include portal/Makefile.am.inc include system-helper/Makefile.am.inc include dbus-proxy/Makefile.am.inc include tests/Makefile.am.inc diff --git a/common/Makefile.am.inc b/common/Makefile.am.inc index 74db3b47..42bdb25b 100644 --- a/common/Makefile.am.inc +++ b/common/Makefile.am.inc @@ -54,8 +54,6 @@ libflatpak_common_la_SOURCES = \ common/flatpak-context.h \ common/flatpak-exports.c \ common/flatpak-exports.h \ - common/flatpak-portal-error.c \ - common/flatpak-portal-error.h \ common/flatpak-utils.c \ common/flatpak-utils.h \ common/flatpak-table-printer.c \ diff --git a/common/flatpak-utils.c b/common/flatpak-utils.c index 9f5ddd4c..e061e915 100644 --- a/common/flatpak-utils.c +++ b/common/flatpak-utils.c @@ -23,7 +23,6 @@ #include "flatpak-utils.h" #include "lib/flatpak-error.h" #include "flatpak-dir.h" -#include "flatpak-portal-error.h" #include "flatpak-oci-registry.h" #include "flatpak-run.h" diff --git a/data/Makefile.am.inc b/data/Makefile.am.inc index 9bfe6782..511b319c 100644 --- a/data/Makefile.am.inc +++ b/data/Makefile.am.inc @@ -1,10 +1,12 @@ introspectiondir = $(datadir)/dbus-1/interfaces introspection_DATA = \ data/org.freedesktop.Flatpak.xml \ + data/org.freedesktop.portal.Flatpak.xml \ $(NULL) EXTRA_DIST += \ data/org.freedesktop.portal.Documents.xml \ data/org.freedesktop.systemd1.xml \ data/org.freedesktop.Flatpak.xml \ + data/org.freedesktop.portal.Flatpak.xml \ $(NULL) diff --git a/data/org.freedesktop.portal.Flatpak.xml b/data/org.freedesktop.portal.Flatpak.xml new file mode 100644 index 00000000..a452b531 --- /dev/null +++ b/data/org.freedesktop.portal.Flatpak.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/portal/Makefile.am.inc b/portal/Makefile.am.inc new file mode 100644 index 00000000..a7736351 --- /dev/null +++ b/portal/Makefile.am.inc @@ -0,0 +1,39 @@ +libexec_PROGRAMS += \ + flatpak-portal \ + $(NULL) + +service_in_files += portal/flatpak-portal.service.in +systemduserunit_DATA += portal/flatpak-portal.service + +service_in_files += portal/org.freedesktop.portal.Flatpak.service.in +dbus_service_DATA += portal/org.freedesktop.portal.Flatpak.service + +portal_built_sources = portal/flatpak-portal-dbus.c portal/flatpak-portal-dbus.h + +portal/flatpak-portal-dbus.c: data/org.freedesktop.portal.Flatpak.xml Makefile + mkdir -p $(builddir)/portal + $(AM_V_GEN) $(GDBUS_CODEGEN) \ + --interface-prefix org.freedesktop.portal \ + --c-namespace Portal \ + --generate-c-code $(builddir)/portal/flatpak-portal-dbus \ + $(srcdir)/data/org.freedesktop.portal.Flatpak.xml \ + $(NULL) + +portal/%-dbus.h: portal/%-dbus.c + @true # Built as a side-effect of the rules for the .c + +flatpak_portal_SOURCES = \ + $(portal_built_sources) \ + portal/flatpak-portal.c \ + portal/flatpak-portal.h \ + portal/flatpak-portal-app-info.c \ + portal/flatpak-portal-app-info.h \ + portal/flatpak-portal-error.c \ + portal/flatpak-portal-error.h \ + $(NULL) + +BUILT_SOURCES += $(portal_built_sources) +CLEANFILES += $(portal_built_sources) + +flatpak_portal_LDADD = $(AM_LDADD) $(BASE_LIBS) +flatpak_portal_CFLAGS = $(AM_CFLAGS) $(BASE_CFLAGS) diff --git a/portal/flatpak-portal-app-info.c b/portal/flatpak-portal-app-info.c new file mode 100644 index 00000000..1776bd69 --- /dev/null +++ b/portal/flatpak-portal-app-info.c @@ -0,0 +1,225 @@ +/* + * Copyright © 2018 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Alexander Larsson + */ + +#include "config.h" + +#include +#include "libglnx/libglnx.h" +#include "flatpak-portal-app-info.h" +#include "flatpak-portal-error.h" + +G_LOCK_DEFINE (app_infos); +static GHashTable *app_infos; + +static void +ensure_app_infos (void) +{ + if (app_infos == NULL) + app_infos = g_hash_table_new_full (g_str_hash, g_str_equal, + NULL, (GDestroyNotify) g_key_file_unref); +} + +static GKeyFile * +lookup_cached_app_info_by_sender (const char *sender) +{ + GKeyFile *keyfile = NULL; + + G_LOCK (app_infos); + keyfile = g_hash_table_lookup (app_infos, sender); + if (keyfile) + g_key_file_ref (keyfile); + G_UNLOCK (app_infos); + + return keyfile; +} + +static void +invalidate_cached_app_info_by_sender (const char *sender) +{ + G_LOCK (app_infos); + g_hash_table_remove (app_infos, sender); + G_UNLOCK (app_infos); +} + +static void +add_cached_app_info_by_sender (const char *sender, GKeyFile *keyfile) +{ + G_LOCK (app_infos); + g_hash_table_add (app_infos, g_key_file_ref (keyfile)); + G_UNLOCK (app_infos); +} + + +/* Returns NULL on failure, keyfile with name "" if not sandboxed, and full app-info otherwise */ +static GKeyFile * +parse_app_id_from_fileinfo (int pid) +{ + g_autofree char *root_path = NULL; + glnx_autofd int root_fd = -1; + glnx_autofd int info_fd = -1; + struct stat stat_buf; + g_autoptr(GError) local_error = NULL; + g_autoptr(GMappedFile) mapped = NULL; + g_autoptr(GKeyFile) metadata = NULL; + + root_path = g_strdup_printf ("/proc/%u/root", pid); + root_fd = openat (AT_FDCWD, root_path, O_RDONLY | O_NONBLOCK | O_DIRECTORY | O_CLOEXEC | O_NOCTTY); + if (root_fd == -1) + { + /* Not able to open the root dir shouldn't happen. Probably the app died and + *we're failing due to /proc/$pid not existing. In that case fail instead + of treating this as privileged. */ + g_debug ("Unable to open %s", root_path); + return NULL; + } + + metadata = g_key_file_new (); + + info_fd = openat (root_fd, ".flatpak-info", O_RDONLY | O_CLOEXEC | O_NOCTTY); + if (info_fd == -1) + { + if (errno == ENOENT) + { + /* No file => on the host */ + g_key_file_set_string (metadata, FLATPAK_METADATA_GROUP_APPLICATION, + FLATPAK_METADATA_KEY_NAME, ""); + return g_steal_pointer (&metadata); + } + + return NULL; /* Some weird error => failure */ + } + + if (fstat (info_fd, &stat_buf) != 0 || !S_ISREG (stat_buf.st_mode)) + return NULL; /* Some weird fd => failure */ + + mapped = g_mapped_file_new_from_fd (info_fd, FALSE, &local_error); + if (mapped == NULL) + { + g_warning ("Can't map .flatpak-info file: %s", local_error->message); + return NULL; + } + + if (!g_key_file_load_from_data (metadata, + g_mapped_file_get_contents (mapped), + g_mapped_file_get_length (mapped), + G_KEY_FILE_NONE, &local_error)) + { + g_warning ("Can't load .flatpak-info file: %s", local_error->message); + return NULL; + } + + return g_steal_pointer (&metadata); +} + +GKeyFile * +flatpak_invocation_lookup_app_info (GDBusMethodInvocation *invocation, + GCancellable *cancellable, + GError **error) +{ + GDBusConnection *connection = g_dbus_method_invocation_get_connection (invocation); + const gchar *sender = g_dbus_method_invocation_get_sender (invocation); + g_autoptr(GDBusMessage) msg = NULL; + g_autoptr(GDBusMessage) reply = NULL; + g_autoptr(GVariantIter) iter = NULL; + const char *key; + GVariant *value; + GKeyFile *keyfile; + + keyfile = lookup_cached_app_info_by_sender (sender); + if (keyfile) + return keyfile; + + msg = g_dbus_message_new_method_call ("org.freedesktop.DBus", + "/org/freedesktop/DBus", + "org.freedesktop.DBus", + "GetConnectionCredentials"); + g_dbus_message_set_body (msg, g_variant_new ("(s)", sender)); + + reply = g_dbus_connection_send_message_with_reply_sync (connection, msg, + G_DBUS_SEND_MESSAGE_FLAGS_NONE, + 30000, + NULL, + cancellable, + error); + if (reply == NULL) + return NULL; + + if (g_dbus_message_get_message_type (reply) == G_DBUS_MESSAGE_TYPE_METHOD_RETURN) + { + GVariant *body = g_dbus_message_get_body (reply); + + g_variant_get (body, "(a{sv})", &iter); + while (g_variant_iter_loop (iter, "{&sv}", &key, &value)) + { + if (strcmp (key, "ProcessID") == 0) + { + guint32 pid = g_variant_get_uint32 (value); + keyfile = parse_app_id_from_fileinfo (pid); + break; + } + } + } + + if (keyfile == NULL) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "Can't find peer app id"); + return NULL; + } + + add_cached_app_info_by_sender (sender, keyfile); + + return keyfile; +} + +static void +name_owner_changed (GDBusConnection *connection, + const gchar *sender_name, + const gchar *object_path, + const gchar *interface_name, + const gchar *signal_name, + GVariant *parameters, + gpointer user_data) +{ + const char *name, *from, *to; + + g_variant_get (parameters, "(sss)", &name, &from, &to); + + if (name[0] == ':' && + strcmp (name, from) == 0 && + strcmp (to, "") == 0) + { + invalidate_cached_app_info_by_sender (name); + } +} + +void +flatpak_connection_track_name_owners (GDBusConnection *connection) +{ + ensure_app_infos (); + g_dbus_connection_signal_subscribe (connection, + "org.freedesktop.DBus", + "org.freedesktop.DBus", + "NameOwnerChanged", + "/org/freedesktop/DBus", + NULL, + G_DBUS_SIGNAL_FLAGS_NONE, + name_owner_changed, + NULL, NULL); +} diff --git a/portal/flatpak-portal-app-info.h b/portal/flatpak-portal-app-info.h new file mode 100644 index 00000000..44fb039a --- /dev/null +++ b/portal/flatpak-portal-app-info.h @@ -0,0 +1,43 @@ +/* + * Copyright © 2018 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Alexander Larsson + */ + +#ifndef __FLATPAK_PORTAL_APP_INFO_H__ +#define __FLATPAK_PORTAL_APP_INFO_H__ + +#define FLATPAK_METADATA_GROUP_APPLICATION "Application" +#define FLATPAK_METADATA_GROUP_RUNTIME "Runtime" +#define FLATPAK_METADATA_GROUP_INSTANCE "Instance" +#define FLATPAK_METADATA_GROUP_CONTEXT "Context" +#define FLATPAK_METADATA_KEY_NAME "name" +#define FLATPAK_METADATA_KEY_ARCH "arch" +#define FLATPAK_METADATA_KEY_BRANCH "branch" +#define FLATPAK_METADATA_KEY_EXTRA_ARGS "extra-args" +#define FLATPAK_METADATA_KEY_APP_COMMIT "app-commit" +#define FLATPAK_METADATA_KEY_RUNTIME_COMMIT "runtime-commit" +#define FLATPAK_METADATA_KEY_SHARED "shared" +#define FLATPAK_METADATA_KEY_INSTANCE_PATH "instance-path" + +GKeyFile * flatpak_invocation_lookup_app_info (GDBusMethodInvocation *invocation, + GCancellable *cancellable, + GError **error); + +void flatpak_connection_track_name_owners (GDBusConnection *connection); + +#endif /* __FLATPAK_PORTAL_APP_INFO_H__ */ diff --git a/common/flatpak-portal-error.c b/portal/flatpak-portal-error.c similarity index 100% rename from common/flatpak-portal-error.c rename to portal/flatpak-portal-error.c diff --git a/common/flatpak-portal-error.h b/portal/flatpak-portal-error.h similarity index 100% rename from common/flatpak-portal-error.h rename to portal/flatpak-portal-error.h diff --git a/portal/flatpak-portal.c b/portal/flatpak-portal.c new file mode 100644 index 00000000..e181520c --- /dev/null +++ b/portal/flatpak-portal.c @@ -0,0 +1,810 @@ +/* + * Copyright © 2018 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Alexander Larsson + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include "libglnx/libglnx.h" +#include "flatpak-portal-dbus.h" +#include "flatpak-portal.h" +#include "flatpak-portal-app-info.h" +#include "flatpak-portal-error.h" + +#define IDLE_TIMEOUT_SECS 10*60 + +static GHashTable *client_pid_data_hash = NULL; +static GDBusConnection *session_bus = NULL; +static gboolean no_idle_exit = FALSE; +static guint name_owner_id = 0; +static GMainLoop *main_loop; +static PortalFlatpak *portal; +static gboolean opt_verbose; + +static void +skeleton_died_cb (gpointer data) +{ + g_debug ("skeleton finalized, exiting"); + g_main_loop_quit (main_loop); +} + +static gboolean +unref_skeleton_in_timeout_cb (gpointer user_data) +{ + static gboolean unreffed = FALSE; + g_debug ("unreffing portal main ref"); + if (!unreffed) + { + g_object_unref (portal); + unreffed = TRUE; + } + + return G_SOURCE_REMOVE; +} + +static void +unref_skeleton_in_timeout (void) +{ + if (name_owner_id) + g_bus_unown_name (name_owner_id); + name_owner_id = 0; + + /* After we've lost the name or idled we drop the main ref on the helper + so that we'll exit when it drops to zero. However, if there are + outstanding calls these will keep the refcount up during the + execution of them. We do the unref on a timeout to make sure + we're completely draining the queue of (stale) requests. */ + g_timeout_add (500, unref_skeleton_in_timeout_cb, NULL); +} + +static guint idle_timeout_id = 0; + +static gboolean +idle_timeout_cb (gpointer user_data) +{ + if (name_owner_id && g_hash_table_size (client_pid_data_hash) == 0) + { + g_debug ("Idle - unowning name"); + unref_skeleton_in_timeout (); + } + + idle_timeout_id = 0; + return G_SOURCE_REMOVE; +} + +G_LOCK_DEFINE_STATIC (idle); +static void +schedule_idle_callback (void) +{ + G_LOCK(idle); + + if (!no_idle_exit) + { + if (idle_timeout_id != 0) + g_source_remove (idle_timeout_id); + + idle_timeout_id = g_timeout_add_seconds (IDLE_TIMEOUT_SECS, idle_timeout_cb, NULL); + } + + G_UNLOCK(idle); +} + +typedef struct { + GPid pid; + char *client; + guint child_watch; +} PidData; + +static void +pid_data_free (PidData *data) +{ + g_free (data->client); + g_free (data); +} + +static void +child_watch_died (GPid pid, + gint status, + gpointer user_data) +{ + PidData *pid_data = user_data; + g_autoptr(GVariant) signal_variant = NULL; + + g_debug ("Client Pid %d died", pid_data->pid); + + signal_variant = g_variant_ref_sink (g_variant_new ("(uu)", pid, status)); + g_dbus_connection_emit_signal (session_bus, + pid_data->client, + "/org/freedesktop/portal/Flatpak", + "org.freedesktop.portal.Flatpak", + "SpawnExited", + signal_variant, + NULL); + + /* This frees the pid_data, so be careful */ + g_hash_table_remove (client_pid_data_hash, GUINT_TO_POINTER(pid_data->pid)); + + /* This might have caused us to go to idle (zero children) */ + schedule_idle_callback (); +} + +typedef struct { + int from; + int to; + int final; +} FdMapEntry; + +typedef struct { + FdMapEntry *fd_map; + int fd_map_len; + gboolean set_tty; + int tty; +} ChildSetupData; + +static void +child_setup_func (gpointer user_data) +{ + ChildSetupData *data = (ChildSetupData *)user_data; + FdMapEntry *fd_map = data->fd_map; + sigset_t set; + int i; + + /* Unblock all signals */ + sigemptyset (&set); + if (pthread_sigmask (SIG_SETMASK, &set, NULL) == -1) + { + g_error ("Failed to unblock signals when starting child"); + return; + } + + /* Reset the handlers for all signals to their defaults. */ + for (i = 1; i < NSIG; i++) + { + if (i != SIGSTOP && i != SIGKILL) + signal (i, SIG_DFL); + } + + for (i = 0; i < data->fd_map_len; i++) + { + if (fd_map[i].from != fd_map[i].to) + { + dup2 (fd_map[i].from, fd_map[i].to); + close (fd_map[i].from); + } + } + + /* Second pass in case we needed an inbetween fd value to avoid conflicts */ + for (i = 0; i < data->fd_map_len; i++) + { + if (fd_map[i].to != fd_map[i].final) + { + dup2 (fd_map[i].to, fd_map[i].final); + close (fd_map[i].to); + } + } + + /* We become our own session and process group, because it never makes sense + to share the flatpak-session-helper dbus activated process group */ + setsid (); + setpgid (0, 0); + + if (data->set_tty) + { + /* data->tty is our from fd which is closed at this point. + * so locate the destnation fd and use it for the ioctl. + */ + for (i = 0; i < data->fd_map_len; i++) + { + if (fd_map[i].from == data->tty) + { + if (ioctl (fd_map[i].final, TIOCSCTTY, 0) == -1) + g_debug ("ioctl(%d, TIOCSCTTY, 0) failed: %s", + fd_map[i].final, strerror (errno)); + break; + } + } + } +} + +static gboolean +is_valid_expose (const char *expose) +{ + /* No subdirs or absolute paths */ + if (strchr (expose, '/')) + return FALSE; + + return TRUE; +} + +static gboolean +handle_spawn (PortalFlatpak *object, + GDBusMethodInvocation *invocation, + GUnixFDList *fd_list, + const gchar *arg_cwd_path, + const gchar *const *arg_argv, + GVariant *arg_fds, + GVariant *arg_envs, + guint arg_flags, + GVariant *arg_options) +{ + g_autoptr(GError) error = NULL; + ChildSetupData child_setup_data = { NULL }; + GPid pid; + PidData *pid_data; + gsize i, j, n_fds, n_envs; + const gint *fds; + g_autofree FdMapEntry *fd_map = NULL; + gchar **env; + gint32 max_fd; + GKeyFile *app_info; + g_autoptr(GPtrArray) flatpak_argv = g_ptr_array_new_with_free_func (g_free); + g_autofree char *app_id = NULL; + g_autofree char *branch = NULL; + g_autofree char *arch = NULL; + g_autofree char *app_commit = NULL; + g_autofree char *runtime_commit = NULL; + g_autofree char *instance_path = NULL; + g_auto(GStrv) extra_args = NULL; + g_auto(GStrv) shares = NULL; + g_auto(GStrv) sandbox_expose = NULL; + g_auto(GStrv) sandbox_expose_ro = NULL; + gboolean sandboxed; + + app_info = g_object_get_data (G_OBJECT (invocation), "app-info"); + g_assert (app_info != NULL); + + app_id = g_key_file_get_string (app_info, + FLATPAK_METADATA_GROUP_APPLICATION, + FLATPAK_METADATA_KEY_NAME, NULL); + g_assert (app_id != NULL); + branch = g_key_file_get_string (app_info, + FLATPAK_METADATA_GROUP_INSTANCE, + FLATPAK_METADATA_KEY_BRANCH, NULL); + instance_path = g_key_file_get_string (app_info, + FLATPAK_METADATA_GROUP_INSTANCE, + FLATPAK_METADATA_KEY_INSTANCE_PATH, NULL); + arch = g_key_file_get_string (app_info, + FLATPAK_METADATA_GROUP_INSTANCE, + FLATPAK_METADATA_KEY_ARCH, NULL); + extra_args = g_key_file_get_string_list (app_info, + FLATPAK_METADATA_GROUP_INSTANCE, + FLATPAK_METADATA_KEY_EXTRA_ARGS, NULL, NULL); + app_commit = g_key_file_get_string (app_info, + FLATPAK_METADATA_GROUP_INSTANCE, + FLATPAK_METADATA_KEY_APP_COMMIT, NULL); + runtime_commit = g_key_file_get_string (app_info, + FLATPAK_METADATA_GROUP_INSTANCE, + FLATPAK_METADATA_KEY_RUNTIME_COMMIT, NULL); + shares = g_key_file_get_string_list (app_info, FLATPAK_METADATA_GROUP_CONTEXT, + FLATPAK_METADATA_KEY_SHARED, NULL, NULL); + + g_debug ("spawn() called from app: %s", app_id); + + if (*app_id == 0) + { + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, + G_DBUS_ERROR_INVALID_ARGS, + "org.freedesktop.portal.Flatpak.Spawn only works in a flatpak"); + return TRUE; + } + + if (*arg_cwd_path == 0) + arg_cwd_path = NULL; + + if (arg_argv == NULL || *arg_argv == NULL) + { + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, + G_DBUS_ERROR_INVALID_ARGS, + "No command given"); + return TRUE; + } + + g_variant_lookup (arg_options, "sandbox-expose", "^as", &sandbox_expose); + g_variant_lookup (arg_options, "sandbox-expose-ro", "^as", &sandbox_expose_ro); + + if (instance_path == NULL && + ((sandbox_expose != NULL && sandbox_expose[0] != NULL) || + (sandbox_expose_ro != NULL && sandbox_expose_ro[0] != NULL))) + { + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, + G_DBUS_ERROR_INVALID_ARGS, + "Invalid sandbox expose, caller has no instance path"); + return TRUE; + } + + for (i = 0; sandbox_expose != NULL && sandbox_expose[i] != NULL; i++) + { + const char *expose = sandbox_expose[i]; + g_debug ("exposing %s", expose); + if (!is_valid_expose (expose)) + { + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, + G_DBUS_ERROR_INVALID_ARGS, + "Invalid sandbox expose"); + return TRUE; + } + } + + for (i = 0; sandbox_expose_ro != NULL && sandbox_expose_ro[i] != NULL; i++) + { + const char *expose = sandbox_expose_ro[i]; + g_debug ("exposing %s", expose); + if (!is_valid_expose (expose)) + { + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, + G_DBUS_ERROR_INVALID_ARGS, + "Invalid sandbox expose"); + return TRUE; + } + } + + g_debug ("Running spawn command %s", arg_argv[0]); + + n_fds = 0; + fds = NULL; + if (fd_list != NULL) + { + n_fds = g_variant_n_children (arg_fds); + fds = g_unix_fd_list_peek_fds (fd_list, NULL); + } + fd_map = g_new0 (FdMapEntry, n_fds); + + child_setup_data.fd_map = fd_map; + child_setup_data.fd_map_len = n_fds; + + max_fd = -1; + for (i = 0; i < n_fds; i++) + { + gint32 handle, fd; + g_variant_get_child (arg_fds, i, "{uh}", &fd, &handle); + fd_map[i].to = fd; + fd_map[i].from = fds[i]; + fd_map[i].final = fd_map[i].to; + + /* If stdin/out/err is a tty we try to set it as the controlling + tty for the app, this way we can use this to run in a terminal. */ + if ((fd == 0 || fd == 1 || fd == 2) && + !child_setup_data.set_tty && + isatty (fds[i])) + { + child_setup_data.set_tty = TRUE; + child_setup_data.tty = fds[i]; + } + + max_fd = MAX (max_fd, fd_map[i].to); + max_fd = MAX (max_fd, fd_map[i].from); + } + + /* We make a second pass over the fds to find if any "to" fd index + overlaps an already in use fd (i.e. one in the "from" category + that are allocated randomly). If a fd overlaps "to" fd then its + a caller issue and not our fault, so we ignore that. */ + for (i = 0; i < n_fds; i++) + { + int to_fd = fd_map[i].to; + gboolean conflict = FALSE; + + /* At this point we're fine with using "from" values for this + value (because we handle to==from in the code), or values + that are before "i" in the fd_map (because those will be + closed at this point when dup:ing). However, we can't + reuse a fd that is in "from" for j > i. */ + for (j = i + 1; j < n_fds; j++) + { + int from_fd = fd_map[j].from; + if (from_fd == to_fd) + { + conflict = TRUE; + break; + } + } + + if (conflict) + fd_map[i].to = ++max_fd; + } + + if (arg_flags & FLATPAK_SPAWN_FLAGS_CLEAR_ENV) + { + char *empty[] = { NULL }; + env = g_strdupv (empty); + } + else + env = g_get_environ (); + + n_envs = g_variant_n_children (arg_envs); + for (i = 0; i < n_envs; i++) + { + const char *var = NULL; + const char *val = NULL; + g_variant_get_child (arg_envs, i, "{&s&s}", &var, &val); + + env = g_environ_setenv (env, var, val, TRUE); + } + + g_ptr_array_add (flatpak_argv, g_strdup ("flatpak")); + g_ptr_array_add (flatpak_argv, g_strdup ("run")); + + sandboxed = (arg_flags & FLATPAK_SPAWN_FLAGS_SANDBOX) != 0; + + if (sandboxed) + g_ptr_array_add (flatpak_argv, g_strdup ("--sandbox")); + else + { + for (i = 0; extra_args != NULL && extra_args[i] != NULL; i++) + g_ptr_array_add (flatpak_argv, g_strdup (extra_args[i])); + } + + /* Inherit launcher network access from launcher, unless + NO_NETWORK set. */ + if (shares != NULL && g_strv_contains ((const char * const *)shares, "network") && + !(arg_flags & FLATPAK_SPAWN_FLAGS_NO_NETWORK)) + g_ptr_array_add (flatpak_argv, g_strdup ("--share=network")); + else + g_ptr_array_add (flatpak_argv, g_strdup ("--unshare=network")); + + if (instance_path) + { + for (i = 0; sandbox_expose != NULL && sandbox_expose[i] != NULL; i++) + g_ptr_array_add (flatpak_argv, g_strdup_printf ("--filesystem=%s/sandbox/%s:rw", instance_path, sandbox_expose[i])); + for (i = 0; sandbox_expose_ro != NULL && sandbox_expose_ro[i] != NULL; i++) + g_ptr_array_add (flatpak_argv, g_strdup_printf ("--filesystem=%s/sandbox/%s:ro", instance_path, sandbox_expose_ro[i])); + } + + for (i = 0; sandbox_expose_ro != NULL && sandbox_expose_ro[i] != NULL; i++) + { + const char *expose = sandbox_expose_ro[i]; + g_debug ("exposing %s", expose); + } + + if ((arg_flags & FLATPAK_SPAWN_FLAGS_LATEST_VERSION) == 0) + { + if (app_commit) + g_ptr_array_add (flatpak_argv, g_strdup_printf ("--commit=%s", app_commit)); + if (runtime_commit) + g_ptr_array_add (flatpak_argv, g_strdup_printf ("--runtime-commit=%s", runtime_commit)); + } + + + if (arg_argv[0][0] != 0) + g_ptr_array_add (flatpak_argv, g_strdup_printf ("--command=%s", arg_argv[0])); + + g_ptr_array_add (flatpak_argv, g_strdup_printf ("%s/%s/%s", app_id, arch ? arch : "", branch ? branch : "")); + for (i = 1; arg_argv[i] != NULL; i++) + g_ptr_array_add (flatpak_argv, g_strdup (arg_argv[i])); + g_ptr_array_add (flatpak_argv, NULL); + + if (opt_verbose) + { + g_autoptr(GString) cmd = g_string_new (""); + int i; + + for (i = 0; flatpak_argv->pdata[i] != NULL; i++) + { + if (i > 0) + g_string_append (cmd, " "); + g_string_append (cmd, flatpak_argv->pdata[i]); + } + + g_debug ("Starting: %s\n", cmd->str); + } + + if (!g_spawn_async_with_pipes (arg_cwd_path, + (char **)flatpak_argv->pdata, + env, + G_SPAWN_SEARCH_PATH|G_SPAWN_DO_NOT_REAP_CHILD, + child_setup_func, &child_setup_data, + &pid, + NULL, + NULL, + NULL, + &error)) + { + gint code = G_DBUS_ERROR_FAILED; + if (g_error_matches (error, G_SPAWN_ERROR, G_SPAWN_ERROR_ACCES)) + code = G_DBUS_ERROR_ACCESS_DENIED; + else if (g_error_matches (error, G_SPAWN_ERROR, G_SPAWN_ERROR_NOENT)) + code = G_DBUS_ERROR_FILE_NOT_FOUND; + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, code, + "Failed to start command: %s", + error->message); + return TRUE; + } + + pid_data = g_new0 (PidData, 1); + pid_data->pid = pid; + pid_data->client = g_strdup (g_dbus_method_invocation_get_sender (invocation)); + pid_data->child_watch = g_child_watch_add_full (G_PRIORITY_DEFAULT, + pid, + child_watch_died, + pid_data, + NULL); + + g_debug ("Client Pid is %d", pid_data->pid); + + g_hash_table_replace (client_pid_data_hash, GUINT_TO_POINTER(pid_data->pid), + pid_data); + + portal_flatpak_complete_spawn (object, invocation, NULL, pid); + return TRUE; +} + +static gboolean +handle_spawn_signal (PortalFlatpak *object, + GDBusMethodInvocation *invocation, + guint arg_pid, + guint arg_signal, + gboolean arg_to_process_group) +{ + PidData *pid_data = NULL; + + g_debug ("spawn_signal(%d %d)", arg_pid, arg_signal); + + pid_data = g_hash_table_lookup (client_pid_data_hash, GUINT_TO_POINTER(arg_pid)); + if (pid_data == NULL || + strcmp (pid_data->client, g_dbus_method_invocation_get_sender (invocation)) != 0) + { + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, + G_DBUS_ERROR_UNIX_PROCESS_ID_UNKNOWN, + "No such pid"); + return TRUE; + } + + g_debug ("Sending signal %d to client pid %d", arg_signal, arg_pid); + + if (arg_to_process_group) + killpg (pid_data->pid, arg_signal); + else + kill (pid_data->pid, arg_signal); + + portal_flatpak_complete_spawn_signal (portal, invocation); + + return TRUE; +} + +static gboolean +authorize_method_handler (GDBusInterfaceSkeleton *interface, + GDBusMethodInvocation *invocation, + gpointer user_data) +{ + g_autoptr(GError) error = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autofree char *app_id = NULL; + + /* Ensure we don't idle exit */ + schedule_idle_callback (); + + keyfile = flatpak_invocation_lookup_app_info (invocation, NULL, &error); + if (keyfile == NULL) + { + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, + "Authorization error: %s", error->message); + return FALSE; + } + + app_id = g_key_file_get_string (keyfile, + FLATPAK_METADATA_GROUP_APPLICATION, + FLATPAK_METADATA_KEY_NAME, &error); + if (app_id == NULL) + { + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, + "Authorization error: %s", error->message); + return FALSE; + } + + g_object_set_data_full (G_OBJECT (invocation), "app-info", g_steal_pointer (&keyfile), (GDestroyNotify)g_key_file_unref); + + return TRUE; +} + +static void +on_bus_acquired (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + GError *error = NULL; + + g_debug ("Bus acquired, creating skeleton"); + + portal = portal_flatpak_skeleton_new (); + + g_object_set_data_full (G_OBJECT(portal), "track-alive", GINT_TO_POINTER(42), skeleton_died_cb); + + g_dbus_interface_skeleton_set_flags (G_DBUS_INTERFACE_SKELETON (portal), + G_DBUS_INTERFACE_SKELETON_FLAGS_HANDLE_METHOD_INVOCATIONS_IN_THREAD); + + portal_flatpak_set_version (PORTAL_FLATPAK (portal), 1); + g_signal_connect (portal, "handle-spawn", G_CALLBACK (handle_spawn), NULL); + g_signal_connect (portal, "handle-spawn-signal", G_CALLBACK (handle_spawn_signal), NULL); + + g_signal_connect (portal, "g-authorize-method", G_CALLBACK (authorize_method_handler), NULL); + + if (!g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (portal), + connection, + "/org/freedesktop/portal/Flatpak", + &error)) + { + g_warning ("error: %s", error->message); + g_error_free (error); + } +} + +static void +on_name_acquired (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + g_debug ("Name acquired"); +} + +static void +on_name_lost (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + g_debug ("Name lost"); + unref_skeleton_in_timeout (); +} + +static void +binary_file_changed_cb (GFileMonitor *file_monitor, + GFile *file, + GFile *other_file, + GFileMonitorEvent event_type, + gpointer data) +{ + static gboolean got_it = FALSE; + + if (!got_it) + { + g_debug ("binary file changed"); + unref_skeleton_in_timeout (); + } + + got_it = TRUE; +} + + +static void +message_handler (const gchar *log_domain, + GLogLevelFlags log_level, + const gchar *message, + gpointer user_data) +{ + /* Make this look like normal console output */ + if (log_level & G_LOG_LEVEL_DEBUG) + g_printerr ("F: %s\n", message); + else + g_printerr ("%s: %s\n", g_get_prgname (), message); +} + +int +main (int argc, + char **argv) +{ + gchar exe_path[PATH_MAX+1]; + ssize_t exe_path_len; + gboolean replace; + gboolean show_version; + GOptionContext *context; + GBusNameOwnerFlags flags; + g_autoptr(GError) error = NULL; + const GOptionEntry options[] = { + { "replace", 'r', 0, G_OPTION_ARG_NONE, &replace, "Replace old daemon.", NULL }, + { "verbose", 'v', 0, G_OPTION_ARG_NONE, &opt_verbose, "Enable debug output.", NULL }, + { "version", 0, 0, G_OPTION_ARG_NONE, &show_version, "Show program version.", NULL}, + { "no-idle-exit", 0, 0, G_OPTION_ARG_NONE, &no_idle_exit, "Don't exit when idle.", NULL }, + { NULL } + }; + + setlocale (LC_ALL, ""); + + g_setenv ("GIO_USE_VFS", "local", TRUE); + + g_set_prgname (argv[0]); + + g_log_set_handler (G_LOG_DOMAIN, G_LOG_LEVEL_MESSAGE, message_handler, NULL); + + context = g_option_context_new (""); + + replace = FALSE; + opt_verbose = FALSE; + show_version = FALSE; + + g_option_context_set_summary (context, "Flatpak portal"); + g_option_context_add_main_entries (context, options, GETTEXT_PACKAGE); + + if (!g_option_context_parse (context, &argc, &argv, &error)) + { + g_printerr ("%s: %s", g_get_application_name(), error->message); + g_printerr ("\n"); + g_printerr ("Try \"%s --help\" for more information.", + g_get_prgname ()); + g_printerr ("\n"); + g_option_context_free (context); + return 1; + } + + if (show_version) + { + g_print (PACKAGE_STRING "\n"); + return 0; + } + + if (opt_verbose) + g_log_set_handler (G_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, message_handler, NULL); + + client_pid_data_hash = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)pid_data_free); + + session_bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error); + if (session_bus == NULL) + { + g_printerr ("Can't find bus: %s\n", error->message); + return 1; + } + + exe_path_len = readlink ("/proc/self/exe", exe_path, sizeof (exe_path) - 1); + if (exe_path_len > 0) + { + exe_path[exe_path_len] = 0; + GFileMonitor *monitor; + g_autoptr(GFile) exe = NULL; + g_autoptr(GError) local_error = NULL; + + exe = g_file_new_for_path (exe_path); + monitor = g_file_monitor_file (exe, + G_FILE_MONITOR_NONE, + NULL, + &local_error); + if (monitor == NULL) + g_warning ("Failed to set watch on %s: %s", exe_path, error->message); + else + g_signal_connect (monitor, "changed", + G_CALLBACK (binary_file_changed_cb), NULL); + } + + flatpak_connection_track_name_owners (session_bus); + + flags = G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT; + if (replace) + flags |= G_BUS_NAME_OWNER_FLAGS_REPLACE; + + name_owner_id = g_bus_own_name (G_BUS_TYPE_SESSION, + "org.freedesktop.portal.Flatpak", + flags, + on_bus_acquired, + on_name_acquired, + on_name_lost, + NULL, + NULL); + + /* Ensure we don't idle exit */ + schedule_idle_callback (); + + main_loop = g_main_loop_new (NULL, FALSE); + g_main_loop_run (main_loop); + + return 0; +} diff --git a/portal/flatpak-portal.h b/portal/flatpak-portal.h new file mode 100644 index 00000000..da137d30 --- /dev/null +++ b/portal/flatpak-portal.h @@ -0,0 +1,31 @@ +/* + * Copyright © 2018 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Alexander Larsson + */ + +#ifndef __FLATPAK_PORTAL_H__ +#define __FLATPAK_PORTAL_H__ + +typedef enum { + FLATPAK_SPAWN_FLAGS_CLEAR_ENV = 1 << 0, + FLATPAK_SPAWN_FLAGS_LATEST_VERSION = 1 << 1, + FLATPAK_SPAWN_FLAGS_SANDBOX = 1 << 2, + FLATPAK_SPAWN_FLAGS_NO_NETWORK = 1 << 3, +} FlatpakSpawnFlags; + +#endif /* __FLATPAK_PORTAL_H__ */ diff --git a/portal/flatpak-portal.service.in b/portal/flatpak-portal.service.in new file mode 100644 index 00000000..a19431fa --- /dev/null +++ b/portal/flatpak-portal.service.in @@ -0,0 +1,7 @@ +[Unit] +Description=flatpak portal + +[Service] +BusName=org.freedesktop.portal.Flatpak +ExecStart=@libexecdir@/flatpak-portal +Type=dbus diff --git a/portal/org.freedesktop.portal.Flatpak.service.in b/portal/org.freedesktop.portal.Flatpak.service.in new file mode 100644 index 00000000..280e9b05 --- /dev/null +++ b/portal/org.freedesktop.portal.Flatpak.service.in @@ -0,0 +1,4 @@ +[D-BUS Service] +Name=org.freedesktop.portal.Flatpak +Exec=@libexecdir@/flatpak-portal +SystemdService=flatpak-portal.service