Skip to content

Commit

Permalink
usersession/userd: add OpenDesktopEntry2 D-Bus method
Browse files Browse the repository at this point in the history
  • Loading branch information
jhenstridge committed Sep 12, 2023
1 parent e1247fc commit 704d9d8
Show file tree
Hide file tree
Showing 15 changed files with 256 additions and 32 deletions.
26 changes: 20 additions & 6 deletions cmd/snap/cmd_desktop_launch.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,19 @@ func cmdlineArgsToUris(args []string) ([]string, error) {
return uris, nil
}

func collectLaunchEnv() map[string]string {
env := map[string]string{}
for _, key := range []string{
"DESKTOP_STARTUP_ID",
"XDG_ACTIVATION_TOKEN",
} {
if val := os.Getenv(key); val != "" {
env[key] = val
}
}
return env
}

func (x *cmdDesktopLaunch) Execute([]string) error {
if filepath.Clean(x.DesktopFile) != x.DesktopFile {
return fmt.Errorf("desktop file has unclean path: %q", x.DesktopFile)
Expand All @@ -86,11 +99,17 @@ func (x *cmdDesktopLaunch) Execute([]string) error {
return fmt.Errorf("only launching snap applications from %s is supported", dirs.SnapDesktopFilesDir)
}

uris, err := cmdlineArgsToUris(x.Positional.FilesOrUris)
if err != nil {
return err
}

// If running a desktop file from a confined snap process,
// then run via the privileged launcher.
if os.Getenv("SNAP") != "" {
// Only the application file name is required for launching.
desktopFile := filepath.Base(x.DesktopFile)
env := collectLaunchEnv()

// Attempt to launch the desktop file via the
// privileged launcher, this will check that this snap
Expand All @@ -100,7 +119,7 @@ func (x *cmdDesktopLaunch) Execute([]string) error {
return fmt.Errorf(i18n.G("unable to access privileged desktop launcher: unable to get session bus: %v"), err)
}
o := conn.Object("io.snapcraft.Launcher", "/io/snapcraft/PrivilegedDesktopLauncher")
call := o.Call("io.snapcraft.PrivilegedDesktopLauncher.OpenDesktopEntry", 0, desktopFile)
call := o.Call("io.snapcraft.PrivilegedDesktopLauncher.OpenDesktopEntry2", 0, desktopFile, x.Action, uris, env)
if call.Err != nil {
return fmt.Errorf(i18n.G("failed to launch %s via the privileged desktop launcher: %v"), desktopFile, call.Err)
}
Expand All @@ -112,11 +131,6 @@ func (x *cmdDesktopLaunch) Execute([]string) error {
return err
}

uris, err := cmdlineArgsToUris(x.Positional.FilesOrUris)
if err != nil {
return err
}

var args []string
if x.Action == "" {
args, err = de.ExpandSnapExec(uris)
Expand Down
24 changes: 21 additions & 3 deletions cmd/snap/cmd_desktop_launch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,16 @@ func (s *DesktopLaunchSuite) SetUpTest(c *C) {
os.Setenv("BAMF_DESKTOP_FILE_HINT", bamfDesktopFileHint)
})
os.Unsetenv("BAMF_DESKTOP_FILE_HINT")
desktopStartupID := os.Getenv("DESKTOP_STARTUP_ID")
s.AddCleanup(func() {
os.Setenv("DESKTOP_STARTUP_ID", desktopStartupID)
})
os.Unsetenv("DESKTOP_STARTUP_ID")
xdgActivationToken := os.Getenv("XDG_ACTIVATION_TOKEN")
s.AddCleanup(func() {
os.Setenv("XDG_ACTIVATION_TOKEN", xdgActivationToken)
})
os.Unsetenv("XDG_ACTIVATION_TOKEN")
}

func (s *DesktopLaunchSuite) TestLaunch(c *C) {
Expand Down Expand Up @@ -167,12 +177,18 @@ func (s *DesktopLaunchSuite) TestDBusLaunch(c *C) {
dbus.FieldDestination: dbus.MakeVariant("io.snapcraft.Launcher"),
dbus.FieldPath: dbus.MakeVariant(dbus.ObjectPath("/io/snapcraft/PrivilegedDesktopLauncher")),
dbus.FieldInterface: dbus.MakeVariant("io.snapcraft.PrivilegedDesktopLauncher"),
dbus.FieldMember: dbus.MakeVariant("OpenDesktopEntry"),
dbus.FieldSignature: dbus.MakeVariant(dbus.ParseSignatureMust("s")),
dbus.FieldMember: dbus.MakeVariant("OpenDesktopEntry2"),
dbus.FieldSignature: dbus.MakeVariant(dbus.ParseSignatureMust("ssasa{ss}")),
})

c.Assert(msg.Body, HasLen, 1)
c.Assert(msg.Body, HasLen, 4)
c.Check(msg.Body[0], Equals, "foo_foo.desktop")
c.Check(msg.Body[1], Equals, "action1")
c.Check(msg.Body[2], DeepEquals, []string{"file:///test.txt"})
c.Check(msg.Body[3], DeepEquals, map[string]string{
"DESKTOP_STARTUP_ID": "x11-startup-id",
"XDG_ACTIVATION_TOKEN": "wayland-startup-id",
})

reply := &dbus.Message{
Type: dbus.TypeMethodReply,
Expand All @@ -194,6 +210,8 @@ func (s *DesktopLaunchSuite) TestDBusLaunch(c *C) {
defer restore()

os.Setenv("SNAP", "launcher-snap")
os.Setenv("DESKTOP_STARTUP_ID", "x11-startup-id")
os.Setenv("XDG_ACTIVATION_TOKEN", "wayland-startup-id")

_, err = snap.Parser(snap.Client()).ParseArgs([]string{"routine", "desktop-launch", "--desktop", s.desktopFile, "--action", "action1", "--", "/test.txt"})
c.Check(err, IsNil)
Expand Down
2 changes: 1 addition & 1 deletion interfaces/builtin/desktop_launch.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ dbus (send)
bus=session
path=/io/snapcraft/PrivilegedDesktopLauncher
interface=io.snapcraft.PrivilegedDesktopLauncher
member=OpenDesktopEntry
member={OpenDesktopEntry,OpenDesktopEntry2}
peer=(label=unconfined),
`

Expand Down
2 changes: 1 addition & 1 deletion interfaces/builtin/desktop_launch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func (s *desktopLaunchSuite) TestConnectedPlugSnippet(c *C) {
c.Assert(err, IsNil)
c.Assert(apparmorSpec.SecurityTags(), DeepEquals, []string{"snap.other.app"})
c.Assert(apparmorSpec.SnippetForTag("snap.other.app"), testutil.Contains, `Can identify and launch other snaps.`)
c.Assert(apparmorSpec.SnippetForTag("snap.other.app"), testutil.Contains, `member=OpenDesktopEntry`)
c.Assert(apparmorSpec.SnippetForTag("snap.other.app"), testutil.Contains, `member={OpenDesktopEntry,OpenDesktopEntry2}`)
c.Assert(apparmorSpec.SnippetForTag("snap.other.app"), testutil.Contains, `peer=(label=unconfined),`)
}

Expand Down
27 changes: 24 additions & 3 deletions tests/main/interfaces-desktop-launch/task.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ execute: |
snap connections test-launcher | MATCH "desktop-launch +test-launcher:desktop-launch +:desktop-launch +manual"
echo "The launcher snap can launch other snaps via userd"
tests.session -u test exec test-launcher.dbus \
tests.session -u test exec test-launcher.dbus-v1 \
test-app_test-app.desktop
echo "The app snap records that it has been launched"
Expand All @@ -52,15 +52,36 @@ execute: |
echo "The app was invoked with the arguments in the desktop file"
MATCH "^args=arg-before arg-after$" < "$launch_data"
echo "The v2 API supports launching files and startup notification"
rm "$launch_data"
tests.session -u test exec \
env DESKTOP_STARTUP_ID=x11-startup XDG_ACTIVATION_TOKEN=wayland-startup \
test-launcher.dbus-v2 test-app_test-app.desktop \
file:///test1.txt file:///test2.txt
retry -n 5 --wait 1 test -s "$launch_data"
MATCH "^args=arg-before /test1.txt /test2.txt arg-after$" < "$launch_data"
MATCH "^DESKTOP_STARTUP_ID=x11-startup$" < "$launch_data"
MATCH "^XDG_ACTIVATION_TOKEN=wayland-startup$" < "$launch_data"
echo "The v2 API supports launching actions"
rm "$launch_data"
tests.session -u test exec test-launcher.dbus-v2 \
-a foo-action test-app_test-app.desktop
retry -n 5 --wait 1 test -s "$launch_data"
MATCH "^args=action$" < "$launch_data"
if ! os.query is-core; then
exit 0
fi
echo "The launcher snap can also invoke the snap via the desktop file Exec line"
rm "$launch_data"
tests.session -u test exec test-launcher.exec \
test-app_test-app.desktop
tests.session -u test exec \
env DESKTOP_STARTUP_ID=x11-startup XDG_ACTIVATION_TOKEN=wayland-startup \
test-launcher.exec test-app_test-app.desktop
retry -n 5 --wait 1 test -s "$launch_data"
MATCH "^args=arg-before arg-after$" < "$launch_data"
MATCH "^DESKTOP_STARTUP_ID=x11-startup$" < "$launch_data"
MATCH "^XDG_ACTIVATION_TOKEN=wayland-startup$" < "$launch_data"
echo "The desktop-launch helper reports errors from the D-Bus service"
not tests.session -u test exec test-launcher.cmd \
Expand Down
2 changes: 2 additions & 0 deletions tests/main/interfaces-desktop-launch/test-app/bin/app.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ WAYLAND_DISPLAY=${WAYLAND_DISPLAY}
XDG_CURRENT_DESKTOP=${XDG_CURRENT_DESKTOP}
XDG_SESSION_DESKTOP=${XDG_SESSION_DESKTOP}
XDG_SESSION_TYPE=${XDG_SESSION_TYPE}
DESKTOP_STARTUP_ID=${DESKTOP_STARTUP_ID}
XDG_ACTIVATION_TOKEN=${XDG_ACTIVATION_TOKEN}
EOF
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@ Type=Application
Name=test-app
Comment=A desktop file for test-app
Exec=test-app arg-before %U arg-after
Actions=foo-action

[Desktop Action foo-action]
Name=test action
Exec=test-app action
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/sh

exec busctl --user call \
io.snapcraft.Launcher /io/snapcraft/PrivilegedDesktopLauncher \
io.snapcraft.PrivilegedDesktopLauncher OpenDesktopEntry "s" \
"$1"
24 changes: 24 additions & 0 deletions tests/main/interfaces-desktop-launch/test-launcher/bin/dbus-v2.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/bin/bash

action=""
while getopts "a:" arg; do
case "$arg" in
a)
action="$OPTARG"
;;
*)
echo "Unexpected option $arg" >&2
exit 1
esac
done
shift $((OPTIND-1))

desktop="$1"
shift

exec busctl --user call \
io.snapcraft.Launcher /io/snapcraft/PrivilegedDesktopLauncher \
io.snapcraft.PrivilegedDesktopLauncher OpenDesktopEntry2 "ssasa{ss}" \
"$desktop" "$action" $# "$@" \
2 DESKTOP_STARTUP_ID "$DESKTOP_STARTUP_ID" \
XDG_ACTIVATION_TOKEN "$XDG_ACTIVATION_TOKEN"

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ set -e

# Extract command line from desktop file
desktop_file="/var/lib/snapd/desktop/applications/$1"
cmdline="$(sed -n 's/^Exec=\(.*\)$/\1/p' "$desktop_file")"
cmdline="$(sed -n '0,/^Exec=/ s/^Exec=\(.*\)$/\1/p' "$desktop_file")"
# filter out the file argument
cmdline="$(echo "$cmdline" | sed 's/%[uUfF]//g')"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ apps:
cmd:
command: bin/cmd.sh
plugs: [desktop-launch]
dbus:
command: bin/dbus.sh
dbus-v1:
command: bin/dbus-v1.sh
plugs: [desktop-launch]
dbus-v2:
command: bin/dbus-v2.sh
plugs: [desktop-launch]
exec:
command: bin/exec.sh
Expand Down
2 changes: 2 additions & 0 deletions usersession/userd/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ func MockSnapFromSender(f func(*dbus.Conn, dbus.Sender) (string, error)) func()
}

var (
AppendEnvironment = appendEnvironment
DesktopFileSearchPath = desktopFileSearchPath
DesktopFileIDToFilename = desktopFileIDToFilename
ValidateURIs = validateURIs
VerifyDesktopFileLocation = verifyDesktopFileLocation
)

Expand Down
81 changes: 73 additions & 8 deletions usersession/userd/privileged_desktop_launcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package userd

import (
"fmt"
"net/url"
"os"
"os/exec"
"path/filepath"
Expand All @@ -47,6 +48,12 @@ const privilegedLauncherIntrospectionXML = `
<method name='OpenDesktopEntry'>
<arg type='s' name='desktop_file_id' direction='in'/>
</method>
<method name='OpenDesktopEntry2'>
<arg type='s' name='desktop_file_id' direction='in'/>
<arg type='s' name='action' direction='in'/>
<arg type='as' name='uris' direction='in' />
<arg type='a{ss}' name='environment' direction='in' />
</method>
</interface>`

// PrivilegedDesktopLauncher implements the 'io.snapcraft.PrivilegedDesktopLauncher' DBus interface.
Expand Down Expand Up @@ -74,6 +81,10 @@ func (s *PrivilegedDesktopLauncher) IntrospectionData() string {
// DBus interface. The desktopFileID is described here:
// https://standards.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#desktop-file-id
func (s *PrivilegedDesktopLauncher) OpenDesktopEntry(desktopFileID string, sender dbus.Sender) *dbus.Error {
return s.OpenDesktopEntry2(desktopFileID, "", nil, nil, sender)
}

func (s *PrivilegedDesktopLauncher) OpenDesktopEntry2(desktopFileID string, action string, uris []string, environment map[string]string, sender dbus.Sender) *dbus.Error {
desktopFile, err := desktopFileIDToFilename(desktopFileID)
if err != nil {
return dbus.MakeFailedError(err)
Expand All @@ -84,31 +95,43 @@ func (s *PrivilegedDesktopLauncher) OpenDesktopEntry(desktopFileID string, sende
return dbus.MakeFailedError(err)
}

if err := validateURIs(uris); err != nil {
return dbus.MakeFailedError(err)
}

de, err := desktopentry.Read(desktopFile)
if err != nil {
return dbus.MakeFailedError(err)
}

args, err := de.ExpandExec(nil)
var execArgs []string
if action == "" {
execArgs, err = de.ExpandExec(uris)
} else {
execArgs, err = de.ExpandActionExec(action, uris)
}
if err != nil {
return dbus.MakeFailedError(err)
}

err = systemd.EnsureAtLeast(236)
if err == nil {
args := []string{"--user"}
if err = systemd.EnsureAtLeast(236); err == nil {
// systemd 236 introduced the --collect option to systemd-run,
// which specifies that the unit should be garbage collected
// even if it fails.
// https://github.com/systemd/systemd/pull/7314
args = append([]string{"systemd-run", "--user", "--collect", "--"}, args...)
} else if systemd.IsSystemdTooOld(err) {
args = append([]string{"systemd-run", "--user", "--"}, args...)
} else {
args = append(args, "--collect")
} else if !systemd.IsSystemdTooOld(err) {
// systemd not available
return dbus.MakeFailedError(err)
}
if args, err = appendEnvironment(args, environment); err != nil {
return dbus.MakeFailedError(err)
}
args = append(args, "--")
args = append(args, execArgs...)

cmd := exec.Command(args[0], args[1:]...)
cmd := exec.Command("systemd-run", args...)

if err := cmd.Run(); err != nil {
return dbus.MakeFailedError(fmt.Errorf("cannot run %q: %v", args, err))
Expand All @@ -117,6 +140,48 @@ func (s *PrivilegedDesktopLauncher) OpenDesktopEntry(desktopFileID string, sende
return nil
}

// validateURIs ensures that all of the uris passed are absolute URIs,
// and if they are file URIs that their path component is absolute.
func validateURIs(uris []string) error {
for _, arg := range uris {
if arg == "" {
return fmt.Errorf("passed an empty parameter")
}
uri, err := url.Parse(arg)
if err != nil {
return fmt.Errorf("one of the parameters is not an URI: %s", arg)
}
if !uri.IsAbs() {
return fmt.Errorf("passed a non-absolute URI: %s", arg)
}
if uri.Scheme == "file" {
if uri.Host != "" {
return fmt.Errorf("passed a file URI with a non-empty host: %s", arg)
}
if !filepath.IsAbs(uri.Path) {
return fmt.Errorf("passed a file URI with a relative path: %s", arg)
}
}
}
return nil
}

// appendEnvironment extends a systemd-run command line to set allowed
// environment variables.
func appendEnvironment(args []string, environment map[string]string) ([]string, error) {
for key, value := range environment {
switch key {
case "DESKTOP_STARTUP_ID", "XDG_ACTIVATION_TOKEN":
// Allow startup notification related
// environment variables
args = append(args, fmt.Sprintf("--setenv=%s=%s", key, value))
default:
return nil, fmt.Errorf("unknown variables in environment")
}
}
return args, nil
}

var regularFileExists = osutil.RegularFileExists

// desktopFileSearchPath returns the list of directories where desktop
Expand Down
Loading

0 comments on commit 704d9d8

Please sign in to comment.