forked from snapcore/snapd
-
Notifications
You must be signed in to change notification settings - Fork 0
/
privileged_desktop_launcher.go
307 lines (266 loc) · 10.1 KB
/
privileged_desktop_launcher.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2021 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package userd
import (
"bufio"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"github.com/godbus/dbus"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/strutil/shlex"
"github.com/snapcore/snapd/systemd"
)
const privilegedLauncherIntrospectionXML = `
<interface name="org.freedesktop.DBus.Peer">
<method name='Ping'>
</method>
<method name='GetMachineId'>
<arg type='s' name='machine_uuid' direction='out'/>
</method>
</interface>
<interface name='io.snapcraft.PrivilegedDesktopLauncher'>
<method name='OpenDesktopEntry'>
<arg type='s' name='desktop_file_id' direction='in'/>
</method>
</interface>`
// PrivilegedDesktopLauncher implements the 'io.snapcraft.PrivilegedDesktopLauncher' DBus interface.
type PrivilegedDesktopLauncher struct {
conn *dbus.Conn
}
// Name returns the name of the interface this object implements
func (s *PrivilegedDesktopLauncher) Interface() string {
return "io.snapcraft.PrivilegedDesktopLauncher"
}
// BasePath returns the base path of the object
func (s *PrivilegedDesktopLauncher) ObjectPath() dbus.ObjectPath {
return "/io/snapcraft/PrivilegedDesktopLauncher"
}
// IntrospectionData gives the XML formatted introspection description
// of the DBus service.
func (s *PrivilegedDesktopLauncher) IntrospectionData() string {
return privilegedLauncherIntrospectionXML
}
// OpenDesktopEntry implements the 'OpenDesktopEntry' method of the 'io.snapcraft.DesktopLauncher'
// 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 {
desktopFile, err := desktopFileIDToFilename(desktopFileID)
if err != nil {
return dbus.MakeFailedError(err)
}
err = verifyDesktopFileLocation(desktopFile)
if err != nil {
return dbus.MakeFailedError(err)
}
command, icon, err := readExecCommandFromDesktopFile(desktopFile)
if err != nil {
return dbus.MakeFailedError(err)
}
args, err := parseExecCommand(command, icon)
if err != nil {
return dbus.MakeFailedError(err)
}
ver, err := systemd.Version()
if err != nil {
return dbus.MakeFailedError(err)
}
// 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
if ver >= 236 {
args = append([]string{"systemd-run", "--user", "--collect", "--"}, args...)
} else {
args = append([]string{"systemd-run", "--user", "--"}, args...)
}
cmd := exec.Command(args[0], args[1:]...)
if err := cmd.Run(); err != nil {
return dbus.MakeFailedError(fmt.Errorf("cannot run %q: %v", command, err))
}
return nil
}
var regularFileExists = osutil.RegularFileExists
// desktopFileSearchPath returns the list of directories where desktop
// files may be located. It implements the lookup rules documented in
// the XDG Base Directory specification.
func desktopFileSearchPath() []string {
var desktopDirs []string
// First check $XDG_DATA_HOME, which defaults to $HOME/.local/share
dataHome := os.Getenv("XDG_DATA_HOME")
if dataHome == "" {
homeDir := os.Getenv("HOME")
if homeDir != "" {
dataHome = filepath.Join(homeDir, ".local/share")
}
}
if dataHome != "" {
desktopDirs = append(desktopDirs, filepath.Join(dataHome, "applications"))
}
// Next check $XDG_DATA_DIRS, with default from spec
dataDirs := os.Getenv("XDG_DATA_DIRS")
if dataDirs == "" {
dataDirs = "/usr/local/share/:/usr/share/"
}
for _, dir := range strings.Split(dataDirs, ":") {
if dir == "" {
continue
}
desktopDirs = append(desktopDirs, filepath.Join(dir, "applications"))
}
return desktopDirs
}
// findDesktopFile recursively tries each subdirectory that can be formed from the (split) desktop file ID.
// Per https://standards.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#desktop-file-id,
// if desktop entries have dashes in the name ('-'), this could be an indication of subdirectories, so search
// for those too. Eg, given foo-bar_baz_norf.desktop the following are searched for:
// o .../foo-bar_baz-norf.desktop
// o .../foo/bar_baz-norf.desktop
// o .../foo/bar_baz/norf.desktop
// o .../foo-bar_baz/norf.desktop
// We're not required to diagnose multiple files matching the desktop file ID.
func findDesktopFile(baseDir string, splitFileId []string) (string, error) {
desktopFile := filepath.Join(baseDir, strings.Join(splitFileId, "-"))
exists, isReg, _ := regularFileExists(desktopFile)
if exists && isReg {
return desktopFile, nil
}
// Iterate through the potential subdirectories formed by the first i elements of the desktop file ID.
for i := 1; i != len(splitFileId); i++ {
prefix := strings.Join(splitFileId[:i], "-")
// Don't treat empty or "." components as directory
// prefixes. The ".." case is already filtered out by
// the isValidDesktopFileID regexp.
if prefix == "" || prefix == "." {
continue
}
desktopFile, err := findDesktopFile(filepath.Join(baseDir, prefix), splitFileId[i:])
if err == nil {
return desktopFile, nil
}
}
return "", fmt.Errorf("could not find desktop file")
}
// isValidDesktopFileID is based on the "File naming" section of the
// Desktop Entry Specification, without the restriction on components
// not starting with a digit (which desktop files created by snapd may
// not satisfy).
var isValidDesktopFileID = regexp.MustCompile(`^[A-Za-z0-9-_]+(\.[A-Za-z0-9-_]+)*.desktop$`).MatchString
// desktopFileIDToFilename determines the path associated with a desktop file ID.
func desktopFileIDToFilename(desktopFileID string) (string, error) {
if !isValidDesktopFileID(desktopFileID) {
return "", fmt.Errorf("cannot find desktop file for %q", desktopFileID)
}
splitDesktopID := strings.Split(desktopFileID, "-")
for _, baseDir := range desktopFileSearchPath() {
if desktopFile, err := findDesktopFile(baseDir, splitDesktopID); err == nil {
return desktopFile, nil
}
}
return "", fmt.Errorf("cannot find desktop file for %q", desktopFileID)
}
// verifyDesktopFileLocation checks the desktop file location:
// we only consider desktop files in dirs.SnapDesktopFilesDir
func verifyDesktopFileLocation(desktopFile string) error {
if filepath.Clean(desktopFile) != desktopFile {
return fmt.Errorf("desktop file has unclean path: %q", desktopFile)
}
if !strings.HasPrefix(desktopFile, dirs.SnapDesktopFilesDir+"/") {
// We currently only support launching snap applications from desktop files in
// /var/lib/snapd/desktop/applications.
return fmt.Errorf("only launching snap applications from %s is supported", dirs.SnapDesktopFilesDir)
}
return nil
}
// readExecCommandFromDesktopFile parses the desktop file to get the Exec entry and
// checks that the BAMF_DESKTOP_FILE_HINT is present and refers to the desktop file.
func readExecCommandFromDesktopFile(desktopFile string) (exec string, icon string, err error) {
file, err := os.Open(desktopFile)
if err != nil {
return exec, icon, err
}
defer file.Close()
scanner := bufio.NewScanner(file)
var inDesktopSection, seenDesktopSection bool
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "[Desktop Entry]" {
if seenDesktopSection {
return "", "", fmt.Errorf("desktop file %q has multiple [Desktop Entry] sections", desktopFile)
}
seenDesktopSection = true
inDesktopSection = true
} else if strings.HasPrefix(line, "[Desktop Action ") {
// TODO: add support for desktop action sections
inDesktopSection = false
} else if strings.HasPrefix(line, "[") {
inDesktopSection = false
} else if inDesktopSection {
if strings.HasPrefix(line, "Exec=") {
exec = strings.TrimPrefix(line, "Exec=")
} else if strings.HasPrefix(line, "Icon=") {
icon = strings.TrimPrefix(line, "Icon=")
}
}
}
expectedPrefix := fmt.Sprintf("env BAMF_DESKTOP_FILE_HINT=%s %s", desktopFile, dirs.SnapBinariesDir)
if !strings.HasPrefix(exec, expectedPrefix) {
return "", "", fmt.Errorf("desktop file %q has an unsupported 'Exec' value: %q", desktopFile, exec)
}
return exec, icon, nil
}
// Parse the Exec command by stripping any exec variables.
// Passing exec variables (eg, %foo) between confined snaps is unsupported. Currently,
// we do not have support for passing them in the D-Bus API but there are security
// implications that must be thought through regarding the influence of the launching
// snap over the launcher wrt exec variables. For now we simply filter them out.
// https://standards.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#exec-variables
func parseExecCommand(command string, icon string) ([]string, error) {
origArgs, err := shlex.Split(command)
if err != nil {
return nil, err
}
args := make([]string, 0, len(origArgs))
for _, arg := range origArgs {
// We want to keep literal '%' (expressed as '%%') but filter our exec variables
// like '%foo'
if strings.HasPrefix(arg, "%%") {
arg = arg[1:]
} else if strings.HasPrefix(arg, "%") {
switch arg {
case "%f", "%F", "%u", "%U":
// If we were launching a file with
// the application, these variables
// would expand to file names or URIs.
// As we're not, they are simply
// removed from the argument list.
case "%i":
args = append(args, "--icon", icon)
default:
return nil, fmt.Errorf("cannot run %q due to use of %q", command, arg)
}
continue
}
args = append(args, arg)
}
return args, nil
}