tools: split wireplumber script execution mode to a separate wpexec tool

This is mainly for security reasons. This way, we can limit the
permissions of wpexec without limiting wireplumber, for instance
with pipewire's default module-access, which limits apps based on
their executable name
This commit is contained in:
George Kiagiadakis 2021-04-09 16:48:49 +03:00
parent e654f7709f
commit 1ba99739c2
5 changed files with 268 additions and 48 deletions

View file

@ -22,36 +22,11 @@ enum WpExitCode
};
static gchar * config_file = NULL;
static gchar * exec_script = NULL;
static GVariantBuilder exec_args_b =
G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_VARDICT);
static gboolean
parse_exec_script_arg (const gchar *option_name, const gchar *value,
gpointer data, GError **error)
{
g_auto(GStrv) tokens = g_strsplit (value, "=", 2);
if (!tokens[0] || *g_strstrip (tokens[0]) == '\0') {
g_set_error (error, WP_DOMAIN_DAEMON, WP_CODE_INVALID_ARGUMENT,
"invalid script argument '%s'; must be in key=value format", value);
return FALSE;
}
g_variant_builder_add (&exec_args_b, "{sv}", tokens[0], tokens[1] ?
g_variant_new_string (g_strstrip (tokens[1])) :
g_variant_new_boolean (TRUE));
return TRUE;
}
static GOptionEntry entries[] =
{
{ "config-file", 'c', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &config_file,
"The configuration file to load components from", NULL },
{ "execute", 'e', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &exec_script,
"Runs WirePlumber in interactive script execution mode, "
"executing the given script", NULL },
{ G_OPTION_REMAINING, 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK,
parse_exec_script_arg, NULL, NULL },
{ NULL }
};
@ -67,7 +42,6 @@ struct _WpInitTransition
enum {
STEP_LOAD_MODULE = WP_TRANSITION_STEP_CUSTOM_START,
STEP_LOAD_CONFIG,
STEP_LOAD_SCRIPT,
STEP_CONNECT,
STEP_ACTIVATE_PLUGINS,
STEP_ACTIVATE_SCRIPTS,
@ -87,16 +61,11 @@ wp_init_transition_get_next_step (WpTransition * transition, guint step)
{
switch (step) {
case WP_TRANSITION_STEP_NONE: return STEP_LOAD_MODULE;
case STEP_LOAD_MODULE: return STEP_LOAD_CONFIG;
case STEP_LOAD_CONFIG: return STEP_CONNECT;
case STEP_CONNECT: return STEP_ACTIVATE_PLUGINS;
case STEP_ACTIVATE_SCRIPTS: return WP_TRANSITION_STEP_NONE;
case STEP_LOAD_MODULE:
return exec_script ? STEP_LOAD_SCRIPT : STEP_LOAD_CONFIG;
case STEP_LOAD_CONFIG:
case STEP_LOAD_SCRIPT:
return STEP_CONNECT;
case STEP_ACTIVATE_PLUGINS: {
WpInitTransition *self = WP_INIT_TRANSITION (transition);
if (self->pending_plugins == 0)
@ -160,16 +129,6 @@ wp_init_transition_execute_step (WpTransition * transition, guint step)
break;
}
case STEP_LOAD_SCRIPT: {
GVariant *args = g_variant_builder_end (&exec_args_b);
if (!wp_core_load_component (core, exec_script, "script/lua", args, &error)) {
wp_transition_return_error (transition, error);
return;
}
wp_transition_advance (transition);
break;
}
case STEP_CONNECT:
g_signal_connect_object (core, "connected",
G_CALLBACK (wp_transition_advance), transition, G_CONNECT_SWAPPED);
@ -200,7 +159,7 @@ wp_init_transition_execute_step (WpTransition * transition, guint step)
wp_info_object (self, "Executing scripts...");
WpPlugin *p = wp_plugin_find (core, "lua-scripting");
g_autoptr (WpPlugin) p = wp_plugin_find (core, "lua-scripting");
wp_object_activate (WP_OBJECT (p), WP_PLUGIN_FEATURE_ENABLED, NULL,
(GAsyncReadyCallback) on_plugin_activated, self);
break;
@ -324,7 +283,7 @@ main (gint argc, gchar **argv)
d.loop = g_main_loop_new (NULL, FALSE);
d.core = wp_core_new (NULL, wp_properties_new (
PW_KEY_APP_NAME, "WirePlumber",
"wireplumber.interactive", exec_script ? "true" : "false",
"wireplumber.interactive", "false",
NULL));
g_signal_connect (d.core, "disconnected", G_CALLBACK (on_disconnected), &d);

View file

@ -8,3 +8,14 @@ executable('wpctl',
install: true,
dependencies : [gobject_dep, gio_dep, wp_dep, pipewire_dep],
)
executable('wpexec',
'wpexec.c',
c_args : [
'-D_GNU_SOURCE',
'-DG_LOG_USE_STRUCTURED',
'-DG_LOG_DOMAIN="wpexec"',
],
install: true,
dependencies : [gobject_dep, gio_dep, wp_dep, pipewire_dep],
)

250
src/tools/wpexec.c Normal file
View file

@ -0,0 +1,250 @@
/* WirePlumber
*
* Copyright © 2019-2021 Collabora Ltd.
* @author George Kiagiadakis <george.kiagiadakis@collabora.com>
*
* SPDX-License-Identifier: MIT
*/
#include <wp/wp.h>
#include <glib-unix.h>
#include <pipewire/keys.h>
#define WP_DOMAIN_DAEMON (wp_domain_daemon_quark ())
static G_DEFINE_QUARK (wireplumber-daemon, wp_domain_daemon);
enum WpExitCode
{
WP_CODE_OK = 0,
WP_CODE_OPERATION_FAILED,
WP_CODE_INVALID_ARGUMENT,
};
static gchar * exec_script = NULL;
static GVariantBuilder exec_args_b =
G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_VARDICT);
static gboolean
parse_exec_script_arg (const gchar *option_name, const gchar *value,
gpointer data, GError **error)
{
/* the first argument is the script */
if (!exec_script) {
exec_script = g_strdup (value);
return TRUE;
}
g_auto(GStrv) tokens = g_strsplit (value, "=", 2);
if (!tokens[0] || *g_strstrip (tokens[0]) == '\0') {
g_set_error (error, WP_DOMAIN_DAEMON, WP_CODE_INVALID_ARGUMENT,
"invalid script argument '%s'; must be in key=value format", value);
return FALSE;
}
g_variant_builder_add (&exec_args_b, "{sv}", tokens[0], tokens[1] ?
g_variant_new_string (g_strstrip (tokens[1])) :
g_variant_new_boolean (TRUE));
return TRUE;
}
static GOptionEntry entries[] =
{
{ G_OPTION_REMAINING, 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK,
parse_exec_script_arg, NULL, NULL },
{ NULL }
};
/*** WpInitTransition ***/
struct _WpInitTransition
{
WpTransition parent;
};
enum {
STEP_LOAD_MODULE = WP_TRANSITION_STEP_CUSTOM_START,
STEP_LOAD_SCRIPT,
STEP_CONNECT,
STEP_ACTIVATE_SCRIPT,
};
G_DECLARE_FINAL_TYPE (WpInitTransition, wp_init_transition,
WP, INIT_TRANSITION, WpTransition)
G_DEFINE_TYPE (WpInitTransition, wp_init_transition, WP_TYPE_TRANSITION)
static void
wp_init_transition_init (WpInitTransition * self)
{
}
static guint
wp_init_transition_get_next_step (WpTransition * transition, guint step)
{
switch (step) {
case WP_TRANSITION_STEP_NONE: return STEP_LOAD_MODULE;
case STEP_LOAD_MODULE: return STEP_LOAD_SCRIPT;
case STEP_LOAD_SCRIPT: return STEP_CONNECT;
case STEP_CONNECT: return STEP_ACTIVATE_SCRIPT;
case STEP_ACTIVATE_SCRIPT: return WP_TRANSITION_STEP_NONE;
default:
g_return_val_if_reached (WP_TRANSITION_STEP_ERROR);
}
}
static void
on_plugin_activated (WpObject * p, GAsyncResult * res, WpInitTransition *self)
{
GError *error = NULL;
if (!wp_object_activate_finish (p, res, &error)) {
wp_transition_return_error (WP_TRANSITION (self), error);
return;
}
wp_transition_advance (WP_TRANSITION (self));
}
static void
wp_init_transition_execute_step (WpTransition * transition, guint step)
{
WpInitTransition *self = WP_INIT_TRANSITION (transition);
WpCore *core = wp_transition_get_source_object (transition);
GError *error = NULL;
switch (step) {
case STEP_LOAD_MODULE:
if (!wp_core_load_component (core, "libwireplumber-module-lua-scripting",
"module", NULL, &error)) {
wp_transition_return_error (transition, error);
return;
}
wp_transition_advance (transition);
break;
case STEP_LOAD_SCRIPT: {
GVariant *args = g_variant_builder_end (&exec_args_b);
if (!wp_core_load_component (core, exec_script, "script/lua", args, &error)) {
wp_transition_return_error (transition, error);
return;
}
wp_transition_advance (transition);
break;
}
case STEP_CONNECT:
g_signal_connect_object (core, "connected",
G_CALLBACK (wp_transition_advance), transition, G_CONNECT_SWAPPED);
if (!wp_core_connect (core)) {
wp_transition_return_error (transition, g_error_new (WP_DOMAIN_DAEMON,
WP_CODE_OPERATION_FAILED, "Failed to connect to PipeWire"));
return;
}
break;
case STEP_ACTIVATE_SCRIPT: {
g_autoptr (WpPlugin) p = wp_plugin_find (core, "lua-scripting");
wp_object_activate (WP_OBJECT (p), WP_PLUGIN_FEATURE_ENABLED, NULL,
(GAsyncReadyCallback) on_plugin_activated, self);
break;
}
case WP_TRANSITION_STEP_ERROR:
break;
default:
g_assert_not_reached ();
}
}
static void
wp_init_transition_class_init (WpInitTransitionClass * klass)
{
WpTransitionClass * transition_class = (WpTransitionClass *) klass;
transition_class->get_next_step = wp_init_transition_get_next_step;
transition_class->execute_step = wp_init_transition_execute_step;
}
/*** WpExec ***/
typedef struct
{
WpCore *core;
GMainLoop *loop;
gint exit_code;
} WpExec;
static void
wpexec_clear (WpExec * self)
{
g_clear_pointer (&self->loop, g_main_loop_unref);
g_clear_object (&self->core);
}
G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC (WpExec, wpexec_clear)
static gboolean
signal_handler (gpointer data)
{
WpExec *d = data;
g_main_loop_quit (d->loop);
return G_SOURCE_REMOVE;
}
static gboolean
init_start (WpTransition * transition)
{
wp_transition_advance (transition);
return G_SOURCE_REMOVE;
}
static void
init_done (WpCore * core, GAsyncResult * res, WpExec * d)
{
g_autoptr (GError) error = NULL;
if (!wp_transition_finish (res, &error)) {
wp_message ("%s", error->message);
d->exit_code = WP_CODE_OPERATION_FAILED;
g_main_loop_quit (d->loop);
}
}
gint
main (gint argc, gchar **argv)
{
g_auto (WpExec) d = {0};
g_autoptr (GOptionContext) context = NULL;
g_autoptr (GError) error = NULL;
wp_init (WP_INIT_ALL);
context = g_option_context_new ("- WirePlumber script interpreter");
g_option_context_add_main_entries (context, entries, NULL);
if (!g_option_context_parse (context, &argc, &argv, &error)) {
wp_message ("%s", error->message);
return WP_CODE_INVALID_ARGUMENT;
}
/* init wireplumber core */
d.loop = g_main_loop_new (NULL, FALSE);
d.core = wp_core_new (NULL, wp_properties_new (
PW_KEY_APP_NAME, "wpexec",
"wireplumber.interactive", "true",
NULL));
g_signal_connect_swapped (d.core, "disconnected",
G_CALLBACK (g_main_loop_quit), d.loop);
/* watch for exit signals */
g_unix_signal_add (SIGINT, signal_handler, &d);
g_unix_signal_add (SIGTERM, signal_handler, &d);
g_unix_signal_add (SIGHUP, signal_handler, &d);
/* initialization transition */
g_idle_add ((GSourceFunc) init_start,
wp_transition_new (wp_init_transition_get_type (), d.core,
NULL, (GAsyncReadyCallback) init_done, &d));
/* run */
g_main_loop_run (d.loop);
wp_core_disconnect (d.core);
return d.exit_code;
}

View file

@ -1,4 +1,4 @@
#!/usr/bin/wireplumber -e
#!/usr/bin/wpexec
--
-- WirePlumber
--

View file

@ -1,4 +1,4 @@
#!/usr/bin/wireplumber -e
#!/usr/bin/wpexec
--
-- WirePlumber
--
@ -10,7 +10,7 @@
-- This is an example of an interactive script
--
-- Execute with:
-- wireplumber -e ./interactive.lua option1=value1 option2=value2 ...
-- wpexec ./interactive.lua option1=value1 option2=value2 ...
-- or:
-- ./interactive.lua option1=value1 option2=value2
-----------------------------------------------------------------------------