/*
* Copyright © 2018 Red Hat, Inc
* Copyright © 2023 GNOME Foundation Inc.
*
* 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 .
*
* Authors:
* Matthias Clasen
* Hubert Figuière
*/
#include "config.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "file-transfer.h"
#include "src/xdp-app-info.h"
#include "src/xdp-utils.h"
#include "document-portal-dbus.h"
#include "document-enums.h"
#include "document-portal.h"
#include "document-portal-fuse.h"
static XdpDbusFileTransfer *file_transfer;
typedef struct
{
char *path;
int parent_dev;
int parent_ino;
gboolean is_dir;
} ExportedFile;
static void
exported_file_free (gpointer data)
{
ExportedFile *file = data;
g_free (file->path);
g_free (file);
}
typedef struct
{
GObject object;
GMutex mutex;
GPtrArray *files;
gboolean writable;
gboolean autostop;
char *key;
char *sender;
XdpAppInfo *app_info;
} FileTransfer;
typedef struct
{
GObjectClass parent_class;
} FileTransferClass;
static GType file_transfer_get_type (void);
G_DEFINE_TYPE (FileTransfer, file_transfer, G_TYPE_OBJECT)
G_DEFINE_AUTOPTR_CLEANUP_FUNC (FileTransfer, g_object_unref);
static void
file_transfer_init (FileTransfer *transfer)
{
g_mutex_init (&transfer->mutex);
}
static void
file_transfer_finalize (GObject *object)
{
FileTransfer *transfer = (FileTransfer *)object;
g_mutex_clear (&transfer->mutex);
g_clear_object (&transfer->app_info);
g_clear_pointer (&transfer->files, g_ptr_array_unref);
g_clear_pointer (&transfer->key, g_free);
g_clear_pointer (&transfer->sender, g_free);
G_OBJECT_CLASS (file_transfer_parent_class)->finalize (object);
}
static void
file_transfer_class_init (FileTransferClass *class)
{
G_OBJECT_CLASS (class)->finalize = file_transfer_finalize;
}
static inline void
auto_unlock_unref_helper (FileTransfer **transfer)
{
if (!*transfer)
return;
g_mutex_unlock (&(*transfer)->mutex);
g_object_unref (*transfer);
}
static inline FileTransfer *
auto_lock_helper (FileTransfer *transfer)
{
if (transfer)
g_mutex_lock (&transfer->mutex);
return transfer;
}
#define TRANSFER_AUTOLOCK_UNREF(transfer) \
G_GNUC_UNUSED __attribute__((cleanup (auto_unlock_unref_helper))) \
FileTransfer * G_PASTE (auto_unlock_unref, __LINE__) = \
auto_lock_helper (transfer);
G_LOCK_DEFINE (transfers);
static GHashTable *transfers;
static FileTransfer *
lookup_transfer (const char *key)
{
FileTransfer *transfer;
G_LOCK (transfers);
transfer = (FileTransfer *)g_hash_table_lookup (transfers, key);
if (transfer)
g_object_ref (transfer);
G_UNLOCK (transfers);
return transfer;
}
static FileTransfer *
file_transfer_start (XdpAppInfo *app_info,
const char *sender,
gboolean writable,
gboolean autostop)
{
FileTransfer *transfer;
transfer = g_object_new (file_transfer_get_type (), NULL);
transfer->app_info = g_object_ref (app_info);
transfer->sender = g_strdup (sender);
transfer->writable = writable;
transfer->autostop = autostop;
transfer->files = g_ptr_array_new_with_free_func (exported_file_free);
G_LOCK (transfers);
do {
guint64 key;
g_free (transfer->key);
key = g_random_int ();
key = (key << 32) | g_random_int ();
transfer->key = g_strdup_printf ("%" G_GUINT64_FORMAT, key);
}
while (g_hash_table_contains (transfers, transfer->key));
g_hash_table_insert (transfers, transfer->key, g_object_ref (transfer));
G_UNLOCK (transfers);
g_debug ("start file transfer owned by '%s' (%s)",
xdp_app_info_get_id (transfer->app_info),
transfer->sender);
return transfer;
}
static gboolean
stop (gpointer data)
{
FileTransfer *transfer = data;
g_object_unref (transfer);
return G_SOURCE_REMOVE;
}
static void
file_transfer_stop (FileTransfer *transfer)
{
GDBusConnection *bus;
g_debug ("stop file transfer owned by '%s' (%s)",
xdp_app_info_get_id (transfer->app_info),
transfer->sender);
bus = g_dbus_interface_skeleton_get_connection (G_DBUS_INTERFACE_SKELETON (file_transfer));
g_dbus_connection_emit_signal (bus,
transfer->sender,
"/org/freedesktop/portal/documents",
"org.freedesktop.portal.FileTransfer",
"TransferClosed",
g_variant_new ("(s)", transfer->key),
NULL);
G_LOCK (transfers);
g_hash_table_steal (transfers, transfer->key);
G_UNLOCK (transfers);
g_idle_add (stop, transfer);
}
static void
file_transfer_add_file (FileTransfer *transfer,
const char *path,
struct stat *st_buf,
struct stat *parent_st_buf)
{
ExportedFile *file;
file = g_new (ExportedFile, 1);
file->path = g_strdup (path);
file->is_dir = S_ISDIR (st_buf->st_mode);
file->parent_dev = parent_st_buf->st_dev;
file->parent_ino = parent_st_buf->st_ino;
g_ptr_array_add (transfer->files, file);
}
static char **
file_transfer_execute (FileTransfer *transfer,
XdpAppInfo *target_app_info,
GError **error)
{
DocumentAddFullFlags common_flags;
DocumentPermissionFlags perms;
const char *target_app_id;
int n_fds;
g_autofree int *fds = NULL;
g_autofree int *parent_devs = NULL;
g_autofree int *parent_inos = NULL;
g_autofree DocumentAddFullFlags *documents_flags = NULL;
int i;
g_auto(GStrv) ids = NULL;
char **files = NULL;
g_debug ("retrieve %d files for %s from file transfer owned by '%s' (%s)",
transfer->files->len,
xdp_app_info_get_id (target_app_info),
xdp_app_info_get_id (transfer->app_info),
transfer->sender);
/* if the target is unsandboxed, just return the files as-is */
if (xdp_app_info_is_host (target_app_info))
{
files = g_new (char *, transfer->files->len + 1);
for (i = 0; i < transfer->files->len; i++)
{
ExportedFile *file = (ExportedFile*)g_ptr_array_index (transfer->files, i);
files[i] = g_strdup (file->path);
}
files[i] = NULL;
return files;
}
common_flags = DOCUMENT_ADD_FLAGS_REUSE_EXISTING | DOCUMENT_ADD_FLAGS_AS_NEEDED_BY_APP;
perms = DOCUMENT_PERMISSION_FLAGS_READ;
if (transfer->writable)
perms |= DOCUMENT_PERMISSION_FLAGS_WRITE;
target_app_id = xdp_app_info_get_id (target_app_info);
n_fds = transfer->files->len;
fds = g_new (int, n_fds);
parent_devs = g_new (int, n_fds);
parent_inos = g_new (int, n_fds);
documents_flags = g_new (DocumentAddFullFlags, n_fds);
for (i = 0; i < n_fds; i++)
{
ExportedFile *file = (ExportedFile*)g_ptr_array_index (transfer->files, i);
fds[i] = open (file->path, O_PATH | O_CLOEXEC);
if (fds[i] == -1)
{
g_set_error (error, G_IO_ERROR, g_io_error_from_errno (errno), "File transfer %s failed", transfer->key);
for (; i > 0; i--)
close (fds[i - 1]);
return NULL;
}
documents_flags[i] = common_flags | (file->is_dir ? DOCUMENT_ADD_FLAGS_DIRECTORY : 0);
parent_devs[i] = file->parent_dev;
parent_inos[i] = file->parent_ino;
}
ids = document_add_full (fds, parent_devs, parent_inos, documents_flags, n_fds, transfer->app_info, target_app_id, perms, error);
for (i = 0; i < n_fds; i++)
close (fds[i]);
if (ids)
{
const char *mountpoint = xdp_fuse_get_mountpoint ();
files = g_new (char *, n_fds + 1);
for (i = 0; i < n_fds; i++)
{
ExportedFile *file = (ExportedFile *) g_ptr_array_index (transfer->files, i);
if (ids[i][0] == '\0')
files[i] = g_strdup (file->path);
else
{
g_autofree char *name = g_path_get_basename (file->path);
files[i] = g_build_filename (mountpoint, ids[i], name, NULL);
}
}
files[n_fds] = NULL;
}
return files;
}
static void
start_transfer (GDBusMethodInvocation *invocation,
GVariant *parameters,
XdpAppInfo *app_info)
{
g_autoptr(GVariant) options = NULL;
g_autoptr(FileTransfer) transfer = NULL;
gboolean writable;
gboolean autostop;
const char *sender;
g_variant_get (parameters, "(@a{sv})", &options);
if (!g_variant_lookup (options, "writable", "b", &writable))
writable = FALSE;
if (!g_variant_lookup (options, "autostop", "b", &autostop))
autostop = TRUE;
sender = g_dbus_method_invocation_get_sender (invocation);
transfer = file_transfer_start (app_info, sender, writable, autostop);
g_dbus_method_invocation_return_value (invocation, g_variant_new ("(s)", transfer->key));
}
static void
add_files (GDBusMethodInvocation *invocation,
GVariant *parameters,
XdpAppInfo *app_info)
{
FileTransfer *transfer;
const char *key;
g_autoptr(GVariant) options = NULL;
GDBusMessage *message;
GUnixFDList *fd_list;
g_autoptr(GVariantIter) iter = NULL;
int fd_id;
const int *fds;
int n_fds;
g_variant_get (parameters, "(&sah@a{sv})", &key, &iter, &options);
transfer = lookup_transfer (key);
if (transfer == NULL)
{
g_dbus_method_invocation_return_error (invocation,
G_DBUS_ERROR,
G_DBUS_ERROR_ACCESS_DENIED,
"Invalid transfer");
return;
}
TRANSFER_AUTOLOCK_UNREF (transfer);
if (strcmp (transfer->sender, g_dbus_method_invocation_get_sender (invocation)) != 0)
{
g_dbus_method_invocation_return_error (invocation,
G_DBUS_ERROR,
G_DBUS_ERROR_ACCESS_DENIED,
"Invalid transfer");
return;
}
message = g_dbus_method_invocation_get_message (invocation);
fd_list = g_dbus_message_get_unix_fd_list (message);
if (fd_list == NULL)
{
g_dbus_method_invocation_return_error (invocation,
G_DBUS_ERROR,
G_DBUS_ERROR_INVALID_ARGS,
"Invalid transfer");
return;
}
fds = g_unix_fd_list_peek_fds (fd_list, &n_fds);
g_debug ("add %d files to file transfer owned by '%s' (%s)", n_fds,
xdp_app_info_get_id (transfer->app_info),
transfer->sender);
while (g_variant_iter_next (iter, "h", &fd_id))
{
int fd = -1;
g_autofree char *path = NULL;
gboolean fd_is_writable;
struct stat st_buf;
struct stat parent_st_buf;
if (fd_id < n_fds)
fd = fds[fd_id];
if (fd == -1)
{
g_dbus_method_invocation_return_error (invocation,
G_DBUS_ERROR,
G_DBUS_ERROR_ACCESS_DENIED,
"Invalid transfer");
return;
}
if (!validate_fd (fd, app_info, VALIDATE_FD_FILE_TYPE_ANY, &st_buf, &parent_st_buf, &path, &fd_is_writable, NULL) ||
(transfer->writable && !fd_is_writable))
{
g_dbus_method_invocation_return_error (invocation,
XDG_DESKTOP_PORTAL_ERROR,
XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED,
"Can't export file");
return;
}
file_transfer_add_file (transfer, path, &st_buf, &parent_st_buf);
}
g_dbus_method_invocation_return_value (invocation, NULL);
}
static void
retrieve_files (GDBusMethodInvocation *invocation,
GVariant *parameters,
XdpAppInfo *app_info)
{
const char *key;
FileTransfer *transfer;
g_auto(GStrv) files = NULL;
g_autoptr(GError) error = NULL;
g_variant_get (parameters, "(&s@a{sv})", &key, NULL);
transfer = lookup_transfer (key);
if (transfer == NULL)
{
g_dbus_method_invocation_return_error (invocation,
G_DBUS_ERROR,
G_DBUS_ERROR_ACCESS_DENIED,
"Invalid transfer");
return;
}
TRANSFER_AUTOLOCK_UNREF (transfer);
files = file_transfer_execute (transfer, app_info, &error);
if (files == NULL)
g_dbus_method_invocation_return_gerror (invocation, error);
else
g_dbus_method_invocation_return_value (invocation, g_variant_new ("(^as)", files));
if (transfer->autostop)
file_transfer_stop (transfer);
}
static void
stop_transfer (GDBusMethodInvocation *invocation,
GVariant *parameters,
XdpAppInfo *app_info)
{
const char *key;
FileTransfer *transfer;
g_variant_get (parameters, "(&s)", &key);
transfer = lookup_transfer (key);
if (transfer == NULL)
{
g_dbus_method_invocation_return_error (invocation,
G_DBUS_ERROR,
G_DBUS_ERROR_ACCESS_DENIED,
"Invalid transfer");
return;
}
TRANSFER_AUTOLOCK_UNREF (transfer);
file_transfer_stop (transfer);
g_dbus_method_invocation_return_value (invocation, NULL);
}
typedef void (*PortalMethod) (GDBusMethodInvocation *invocation,
GVariant *parameters,
XdpAppInfo *app_info);
static gboolean
handle_method (GCallback method_callback,
GDBusMethodInvocation *invocation)
{
g_autoptr(GError) error = NULL;
g_autoptr(XdpAppInfo) app_info = NULL;
PortalMethod portal_method = (PortalMethod)method_callback;
app_info = xdp_invocation_ensure_app_info_sync (invocation, NULL, &error);
if (app_info == NULL)
g_dbus_method_invocation_return_gerror (invocation, error);
else
portal_method (invocation, g_dbus_method_invocation_get_parameters (invocation), app_info);
return TRUE;
}
GDBusInterfaceSkeleton *
file_transfer_create (void)
{
file_transfer = xdp_dbus_file_transfer_skeleton_new ();
g_signal_connect_swapped (file_transfer, "handle-start-transfer", G_CALLBACK (handle_method), start_transfer);
g_signal_connect_swapped (file_transfer, "handle-add-files", G_CALLBACK (handle_method), add_files);
g_signal_connect_swapped (file_transfer, "handle-retrieve-files", G_CALLBACK (handle_method), retrieve_files);
g_signal_connect_swapped (file_transfer, "handle-stop-transfer", G_CALLBACK (handle_method), stop_transfer);
xdp_dbus_file_transfer_set_version (XDP_DBUS_FILE_TRANSFER (file_transfer), 1);
transfers = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, g_object_unref);
return G_DBUS_INTERFACE_SKELETON (file_transfer);
}
void
stop_file_transfers_in_thread_func (GTask *task,
gpointer source_object,
gpointer task_data,
GCancellable *cancellable)
{
const char *sender = (const char *)task_data;
GHashTableIter iter;
FileTransfer *transfer;
G_LOCK (transfers);
if (transfers)
{
g_hash_table_iter_init (&iter, transfers);
while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&transfer))
{
if (strcmp (sender, transfer->sender) == 0)
{
g_print ("removing transfer %s for dead peer %s\n", transfer->key, transfer->sender);
g_hash_table_iter_remove (&iter);
}
}
}
G_UNLOCK (transfers);
g_task_return_boolean (task, TRUE);
}
void
stop_file_transfers_for_sender (const char *sender)
{
GTask *task;
task = g_task_new (NULL, NULL, NULL, NULL);
g_task_set_task_data (task, g_strdup (sender), g_free);
g_task_run_in_thread (task, stop_file_transfers_in_thread_func);
g_object_unref (task);
}