/* * Common code for portal-like services * * Copyright © 2018 Red Hat, Inc. * Copyright © 2020 Collabora Ltd. * * SPDX-License-Identifier: LGPL-2.1-or-later * * 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 <http://www.gnu.org/licenses/>. * * Based on xdg-desktop-portal, flatpak-portal and flatpak-spawn. * Authors: * Alexander Larsson <alexl@redhat.com> */ #include "config.h" #include "subprojects/libglnx/config.h" #include <errno.h> #include <locale.h> #include <stdio.h> #include <sysexits.h> #include <sys/ioctl.h> #include <sys/signalfd.h> #include <sys/socket.h> #include <glib.h> #include <glib-unix.h> #include <gio/gio.h> #include <gio/gunixfdlist.h> #include "steam-runtime-tools/glib-backports-internal.h" #include "steam-runtime-tools/utils-internal.h" #include "libglnx/libglnx.h" #include "launcher.h" #include "portal-listener.h" #include "utils.h" typedef GCredentials AutoCredentials; G_DEFINE_AUTOPTR_CLEANUP_FUNC(AutoCredentials, g_object_unref) typedef GDBusAuthObserver AutoDBusAuthObserver; G_DEFINE_AUTOPTR_CLEANUP_FUNC(AutoDBusAuthObserver, g_object_unref) typedef GDBusServer AutoDBusServer; G_DEFINE_AUTOPTR_CLEANUP_FUNC(AutoDBusServer, g_object_unref) struct _PvPortalListenerClass { GObjectClass parent; gboolean (*new_peer_connection) (PvPortalListener *self, GDBusConnection *connection, gpointer user_data); }; G_DEFINE_TYPE (PvPortalListener, pv_portal_listener, G_TYPE_OBJECT) enum { NEW_PEER_CONNECTION, SESSION_BUS_CONNECTED, SESSION_BUS_NAME_ACQUIRED, SESSION_BUS_NAME_LOST, N_SIGNALS }; static guint signal_ids[N_SIGNALS] = { 0 }; void pv_portal_listener_init (PvPortalListener *self) { self->original_environ = g_get_environ (); pv_get_current_dirs (NULL, &self->original_cwd_l); } /* * Divert stdout to stderr, and set up the --info-fd to be the * original stdout or a specified fd. */ gboolean pv_portal_listener_set_up_info_fd (PvPortalListener *self, int fd, GError **error) { self->original_stdout = _srt_divert_stdout_to_stderr (error); if (self->original_stdout == NULL) return FALSE; if (fd > 0) /* < 0 means unset, and 0 is stdout itself */ { self->info_fh = fdopen (fd, "w"); if (self->info_fh == NULL) return glnx_throw_errno_prefix (error, "Unable to create a stdio wrapper for fd %d", fd); } else { self->info_fh = self->original_stdout; } return TRUE; } gboolean pv_portal_listener_check_socket_arguments (PvPortalListener *listener, const char *opt_bus_name, const char *opt_socket, const char *opt_socket_directory, GError **error) { gsize i; if ((opt_socket != NULL) + (opt_socket_directory != NULL) + (opt_bus_name != NULL) != 1) { g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, "Exactly one of --bus-name, --socket, --socket-directory" "is required"); return FALSE; } /* --socket argument needs to be printable so we can print * "socket=%s\n" without escaping */ if (opt_socket != NULL) { for (i = 0; opt_socket[i] != '\0'; i++) { if (!g_ascii_isprint (opt_socket[i])) return glnx_throw (error, "Non-printable characters not allowed in --socket"); } } /* --socket-directory argument likewise */ if (opt_socket_directory != NULL) { for (i = 0; opt_socket_directory[i] != '\0'; i++) { if (!g_ascii_isprint (opt_socket_directory[i])) return glnx_throw (error, "Non-printable characters not allowed in " "--socket-directory"); } } return TRUE; } static void on_bus_acquired (GDBusConnection *connection, const gchar *name, gpointer user_data) { PvPortalListener *self = user_data; g_signal_emit (self, signal_ids[SESSION_BUS_CONNECTED], 0, connection); } static void on_name_acquired (GDBusConnection *connection, const gchar *name, gpointer user_data) { PvPortalListener *self = user_data; g_signal_emit (self, signal_ids[SESSION_BUS_NAME_ACQUIRED], 0, connection, name); } static void on_name_lost (GDBusConnection *connection, const gchar *name, gpointer user_data) { PvPortalListener *self = user_data; g_signal_emit (self, signal_ids[SESSION_BUS_NAME_LOST], 0, connection, name); } #if GLIB_CHECK_VERSION(2, 34, 0) /* * Callback for GDBusServer::allow-mechanism. * Only allow the (most secure) EXTERNAL authentication mechanism, * if possible. */ static gboolean allow_external_cb (G_GNUC_UNUSED GDBusAuthObserver *observer, const char *mechanism, G_GNUC_UNUSED gpointer user_data) { return (g_strcmp0 (mechanism, "EXTERNAL") == 0); } #endif /* * Callback for GDBusServer::authorize-authenticated-peer. * Only allow D-Bus connections from a matching uid. */ static gboolean authorize_authenticated_peer_cb (G_GNUC_UNUSED GDBusAuthObserver *observer, G_GNUC_UNUSED GIOStream *stream, GCredentials *credentials, G_GNUC_UNUSED gpointer user_data) { g_autoptr(AutoCredentials) retry_credentials = NULL; g_autoptr(AutoCredentials) myself = NULL; g_autofree gchar *credentials_str = NULL; if (g_credentials_get_unix_user (credentials, NULL) == (uid_t) -1) { /* Work around https://gitlab.gnome.org/GNOME/glib/issues/1831: * older GLib versions might retrieve incomplete credentials. * Make them try again. */ credentials = NULL; } if (credentials == NULL && G_IS_SOCKET_CONNECTION (stream)) { /* Continue to work around * https://gitlab.gnome.org/GNOME/glib/issues/1831: * older GLib versions might retrieve incomplete or no credentials. */ GSocket *sock; sock = g_socket_connection_get_socket (G_SOCKET_CONNECTION (stream)); retry_credentials = g_socket_get_credentials (sock, NULL); credentials = retry_credentials; } myself = g_credentials_new (); if (credentials != NULL && g_credentials_is_same_user (credentials, myself, NULL)) return TRUE; if (credentials == NULL) credentials_str = g_strdup ("peer with unknown credentials"); else credentials_str = g_credentials_to_string (credentials); g_warning ("Rejecting connection from %s", credentials_str); return FALSE; } static GDBusAuthObserver * observer_new (void) { g_autoptr(AutoDBusAuthObserver) observer = g_dbus_auth_observer_new (); #if GLIB_CHECK_VERSION(2, 34, 0) g_signal_connect (observer, "allow-mechanism", G_CALLBACK (allow_external_cb), NULL); #endif g_signal_connect (observer, "authorize-authenticated-peer", G_CALLBACK (authorize_authenticated_peer_cb), NULL); return g_steal_pointer (&observer); } /* * Double-check credentials of a peer, since we are working with older * versions of GLib that can't necessarily be completely relied on. * We are willing to execute arbitrary code on behalf of an authenticated * connection, so it seems worthwhile to be extra-careful. */ static gboolean check_credentials (GDBusConnection *connection, GError **error) { struct ucred creds; socklen_t len; GIOStream *stream; GSocket *sock; int sockfd; stream = g_dbus_connection_get_stream (connection); len = sizeof (creds); if (!G_IS_SOCKET_CONNECTION (stream)) return glnx_throw (error, "Incoming D-Bus connection is not a socket?"); sock = g_socket_connection_get_socket (G_SOCKET_CONNECTION (stream)); if (sock == NULL) return glnx_throw (error, "Incoming D-Bus connection does not have a socket?"); sockfd = g_socket_get_fd (sock); if (getsockopt (sockfd, SOL_SOCKET, SO_PEERCRED, &creds, &len) < 0) return glnx_throw_errno_prefix (error, "Unable to check credentials"); if (creds.uid != geteuid ()) return glnx_throw (error, "Connection from uid %ld != %ld should have been " "rejected already", (long) creds.uid, (long) geteuid ()); return TRUE; } /* * Callback for GDBusServer::new-connection. * * Returns: %TRUE if the new connection attempt was handled, even if * unsuccessfully. */ static gboolean new_connection_cb (GDBusServer *server, GDBusConnection *connection, gpointer user_data) { PvPortalListener *self = user_data; g_autoptr(GError) error = NULL; gboolean handled; g_return_val_if_fail (self->server == server, FALSE); if (!check_credentials (connection, &error)) { g_warning ("Credentials verification failed: %s", error->message); g_dbus_connection_close (connection, NULL, NULL, NULL); return TRUE; /* handled, unsuccessfully */ } g_signal_emit (self, signal_ids[NEW_PEER_CONNECTION], 0, connection, &handled); g_return_val_if_fail (handled, FALSE); return TRUE; } static GDBusServer * listen_on_address (PvPortalListener *self, const char *address, GError **error) { g_autofree gchar *guid = NULL; g_autoptr(AutoDBusAuthObserver) observer = NULL; g_autoptr(AutoDBusServer) server = NULL; guid = g_dbus_generate_guid (); observer = observer_new (); server = g_dbus_server_new_sync (address, G_DBUS_SERVER_FLAGS_NONE, guid, observer, NULL, error); if (server == NULL) return NULL; g_signal_connect (server, "new-connection", G_CALLBACK (new_connection_cb), self); g_dbus_server_start (server); return g_steal_pointer (&server); } static GDBusServer * listen_on_socket (PvPortalListener *self, GError **error) { g_autofree gchar *address = NULL; g_autofree gchar *escaped = NULL; if (self->server_socket[0] == '@') { escaped = g_dbus_address_escape_value (&self->server_socket[1]); address = g_strdup_printf ("unix:abstract=%s", escaped); } else if (self->server_socket[0] == '/') { escaped = g_dbus_address_escape_value (self->server_socket); address = g_strdup_printf ("unix:path=%s", escaped); } else { g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "Invalid socket address '%s'", self->server_socket); return FALSE; } return listen_on_address (self, address, error); } gboolean pv_portal_listener_listen (PvPortalListener *self, const char *opt_bus_name, GBusNameOwnerFlags flags, const char *opt_socket, const char *opt_socket_directory, GError **error) { g_return_val_if_fail ((opt_bus_name != NULL) + (opt_socket != NULL) + (opt_socket_directory != NULL) == 1, FALSE); if (opt_bus_name != NULL) { g_debug ("Connecting to D-Bus session bus..."); self->session_bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, error); if (self->session_bus == NULL) return glnx_prefix_error (error, "Can't find session bus"); g_debug ("Claiming bus name %s...", opt_bus_name); self->name_owner_id = g_bus_own_name (G_BUS_TYPE_SESSION, opt_bus_name, flags, on_bus_acquired, on_name_acquired, on_name_lost, self, NULL); } else if (opt_socket != NULL) { g_debug ("Listening on socket %s...", opt_socket); self->server_socket = g_strdup (opt_socket); self->server = listen_on_socket (self, error); if (self->server == NULL) return glnx_prefix_error (error, "Unable to listen on socket \"%s\"", opt_socket); } else if (opt_socket_directory != NULL) { g_autofree gchar *unique = NULL; g_autofree gchar *dir = NULL; if (strlen (opt_socket_directory) > PV_MAX_SOCKET_DIRECTORY_LEN) return glnx_throw (error, "Socket directory path \"%s\" too long", opt_socket_directory); dir = realpath (opt_socket_directory, NULL); if (strlen (dir) > PV_MAX_SOCKET_DIRECTORY_LEN) return glnx_throw (error, "Socket directory path \"%s\" too long", dir); g_debug ("Listening on a socket in %s...", opt_socket_directory); /* @unique is long and random, so we assume it is not guessable * by an attacker seeking to deny service by using the name we * intended to use; so we don't need a retry loop for alternative * names in the same directory. */ unique = pv_get_random_uuid (error); if (unique == NULL) return FALSE; self->server_socket = g_build_filename (dir, unique, NULL); g_debug ("Chosen socket is %s", self->server_socket); self->server = listen_on_socket (self, error); if (self->server == NULL) return glnx_prefix_error (error, "Unable to listen on socket \"%s\"", self->server_socket); } else { g_return_val_if_reached (FALSE); } if (self->server_socket != NULL) fprintf (self->info_fh, "socket=%s\n", self->server_socket); if (self->server != NULL) fprintf (self->info_fh, "dbus_address=%s\n", g_dbus_server_get_client_address (self->server)); if (opt_bus_name == NULL) pv_portal_listener_close_info_fh (self, NULL); return TRUE; } /* * If @bus_name is non-NULL, print it to the info fd. Then * close the --info-fd, and also close standard output (if different). */ void pv_portal_listener_close_info_fh (PvPortalListener *self, const char *bus_name) { if (self->info_fh != NULL) { if (bus_name != NULL) fprintf (self->info_fh, "bus_name=%s\n", bus_name); fflush (self->info_fh); } if (self->info_fh == self->original_stdout) self->original_stdout = NULL; else g_clear_pointer (&self->original_stdout, fclose); g_clear_pointer (&self->info_fh, fclose); } void pv_portal_listener_release_name (PvPortalListener *self) { if (self->name_owner_id) g_bus_unown_name (self->name_owner_id); self->name_owner_id = 0; } static void pv_portal_listener_dispose (GObject *object) { PvPortalListener *self = PV_PORTAL_LISTENER (object); pv_portal_listener_close_info_fh (self, NULL); pv_portal_listener_release_name (self); if (self->server != NULL && self->server_socket != NULL) unlink (self->server_socket); g_clear_object (&self->server); g_clear_object (&self->session_bus); G_OBJECT_CLASS (pv_portal_listener_parent_class)->dispose (object); } static void pv_portal_listener_finalize (GObject *object) { PvPortalListener *self = PV_PORTAL_LISTENER (object); g_clear_pointer (&self->server_socket, g_free); g_clear_pointer (&self->original_environ, g_strfreev); g_clear_pointer (&self->original_cwd_l, g_free); G_OBJECT_CLASS (pv_portal_listener_parent_class)->finalize (object); } static void pv_portal_listener_class_init (PvPortalListenerClass *cls) { GObjectClass *object_class = G_OBJECT_CLASS (cls); object_class->dispose = pv_portal_listener_dispose; object_class->finalize = pv_portal_listener_finalize; /* * Basically the same as GDBusServer::new-connection */ signal_ids[NEW_PEER_CONNECTION] = g_signal_new ("new-peer-connection", PV_TYPE_PORTAL_LISTENER, G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (PvPortalListenerClass, new_peer_connection), g_signal_accumulator_true_handled, NULL, NULL, G_TYPE_BOOLEAN, 1, G_TYPE_DBUS_CONNECTION); signal_ids[SESSION_BUS_CONNECTED] = g_signal_new ("session-bus-connected", PV_TYPE_PORTAL_LISTENER, G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_DBUS_CONNECTION); signal_ids[SESSION_BUS_NAME_ACQUIRED] = g_signal_new ("session-bus-name-acquired", PV_TYPE_PORTAL_LISTENER, G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 2, G_TYPE_DBUS_CONNECTION, G_TYPE_STRING); signal_ids[SESSION_BUS_NAME_LOST] = g_signal_new ("session-bus-name-lost", PV_TYPE_PORTAL_LISTENER, G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 2, G_TYPE_DBUS_CONNECTION, G_TYPE_STRING); } PvPortalListener * pv_portal_listener_new (void) { return g_object_new (PV_TYPE_PORTAL_LISTENER, NULL); }