-
Simon McVittie authored
Signed-off-by:
Simon McVittie <smcv@collabora.com>
Simon McVittie authoredSigned-off-by:
Simon McVittie <smcv@collabora.com>
portal-listener.c 18.18 KiB
/*
* 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);
}