github.com/rigado/snapd@v2.42.5-go-mod+incompatible/usersession/userd/settings.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2017 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package userd 21 22 import ( 23 "fmt" 24 "os/exec" 25 "path/filepath" 26 "strings" 27 "time" 28 29 "github.com/godbus/dbus" 30 "github.com/snapcore/snapd/dirs" 31 "github.com/snapcore/snapd/i18n" 32 "github.com/snapcore/snapd/osutil" 33 "github.com/snapcore/snapd/snap" 34 "github.com/snapcore/snapd/usersession/userd/ui" 35 ) 36 37 // Timeout when the confirmation dialog for an xdg-setging 38 // automatically closes. Keep in sync with the core snaps 39 // xdg-settings wrapper which also sets this value to 300. 40 var defaultConfirmDialogTimeout = 300 * time.Second 41 42 const settingsIntrospectionXML = ` 43 <interface name="org.freedesktop.DBus.Peer"> 44 <method name='Ping'> 45 </method> 46 <method name='GetMachineId'> 47 <arg type='s' name='machine_uuid' direction='out'/> 48 </method> 49 </interface> 50 <interface name='io.snapcraft.Settings'> 51 <method name='Check'> 52 <arg type='s' name='setting' direction='in'/> 53 <arg type='s' name='check' direction='in'/> 54 <arg type='s' name='result' direction='out'/> 55 </method> 56 <method name='Get'> 57 <arg type='s' name='setting' direction='in'/> 58 <arg type='s' name='result' direction='out'/> 59 </method> 60 <method name='Set'> 61 <arg type='s' name='setting' direction='in'/> 62 <arg type='s' name='value' direction='in'/> 63 </method> 64 </interface>` 65 66 // TODO: allow setting default-url-scheme-handler ? 67 var settingsWhitelist = []string{ 68 "default-web-browser", 69 } 70 71 func allowedSetting(setting string) bool { 72 if !strings.HasSuffix(setting, ".desktop") { 73 return false 74 } 75 base := strings.TrimSuffix(setting, ".desktop") 76 77 return snap.ValidAppName(base) 78 } 79 80 func settingWhitelisted(setting string) *dbus.Error { 81 for _, whitelisted := range settingsWhitelist { 82 if setting == whitelisted { 83 return nil 84 } 85 } 86 return dbus.MakeFailedError(fmt.Errorf("cannot use setting %q: not allowed", setting)) 87 } 88 89 // Settings implements the 'io.snapcraft.Settings' DBus interface. 90 type Settings struct { 91 conn *dbus.Conn 92 } 93 94 // Name returns the name of the interface this object implements 95 func (s *Settings) Name() string { 96 return "io.snapcraft.Settings" 97 } 98 99 // BasePath returns the base path of the object 100 func (s *Settings) BasePath() dbus.ObjectPath { 101 return "/io/snapcraft/Settings" 102 } 103 104 // IntrospectionData gives the XML formatted introspection description 105 // of the DBus service. 106 func (s *Settings) IntrospectionData() string { 107 return settingsIntrospectionXML 108 } 109 110 // some notes: 111 // - we only set/get desktop files 112 // - all desktop files of snaps are prefixed with: ${snap}_ 113 // - on get/check/set we need to add/strip this prefix 114 115 // Check implements the 'Check' method of the 'io.snapcraft.Settings' 116 // DBus interface. 117 // 118 // Example usage: dbus-send --session --dest=io.snapcraft.Settings --type=method_call --print-reply /io/snapcraft/Settings io.snapcraft.Settings.Check string:'default-web-browser' string:'firefox.desktop' 119 func (s *Settings) Check(setting, check string, sender dbus.Sender) (string, *dbus.Error) { 120 // avoid information leak: see https://github.com/snapcore/snapd/pull/4073#discussion_r146682758 121 snap, err := snapFromSender(s.conn, sender) 122 if err != nil { 123 return "", dbus.MakeFailedError(err) 124 } 125 if err := settingWhitelisted(setting); err != nil { 126 return "", err 127 } 128 if !allowedSetting(check) { 129 return "", dbus.MakeFailedError(fmt.Errorf("cannot check setting %q to value %q: value not allowed", setting, check)) 130 } 131 132 // FIXME: this works only for desktop files 133 desktopFile := fmt.Sprintf("%s_%s", snap, check) 134 135 cmd := exec.Command("xdg-settings", "check", setting, desktopFile) 136 output, err := cmd.CombinedOutput() 137 if err != nil { 138 return "", dbus.MakeFailedError(fmt.Errorf("cannot check setting %s: %s", setting, osutil.OutputErr(output, err))) 139 } 140 141 return strings.TrimSpace(string(output)), nil 142 } 143 144 // Get implements the 'Get' method of the 'io.snapcraft.Settings' 145 // DBus interface. 146 // 147 // Example usage: dbus-send --session --dest=io.snapcraft.Settings --type=method_call --print-reply /io/snapcraft/Settings io.snapcraft.Settings.Get string:'default-web-browser' 148 func (s *Settings) Get(setting string, sender dbus.Sender) (string, *dbus.Error) { 149 if err := settingWhitelisted(setting); err != nil { 150 return "", err 151 } 152 153 cmd := exec.Command("xdg-settings", "get", setting) 154 output, err := cmd.CombinedOutput() 155 if err != nil { 156 return "", dbus.MakeFailedError(fmt.Errorf("cannot get setting %s: %s", setting, osutil.OutputErr(output, err))) 157 } 158 159 // avoid information leak: see https://github.com/snapcore/snapd/pull/4073#discussion_r146682758 160 snap, err := snapFromSender(s.conn, sender) 161 if err != nil { 162 return "", dbus.MakeFailedError(err) 163 } 164 if !strings.HasPrefix(string(output), snap+"_") { 165 return "NOT-THIS-SNAP.desktop", nil 166 } 167 168 desktopFile := strings.SplitN(string(output), "_", 2)[1] 169 return strings.TrimSpace(desktopFile), nil 170 } 171 172 // Set implements the 'Set' method of the 'io.snapcraft.Settings' 173 // DBus interface. 174 // 175 // Example usage: dbus-send --session --dest=io.snapcraft.Settings --type=method_call --print-reply /io/snapcraft/Settings io.snapcraft.Settings.Set string:'default-web-browser' string:'chromium-browser.desktop' 176 func (s *Settings) Set(setting, new string, sender dbus.Sender) *dbus.Error { 177 if err := settingWhitelisted(setting); err != nil { 178 return err 179 } 180 // see https://github.com/snapcore/snapd/pull/4073#discussion_r146682758 181 snap, err := snapFromSender(s.conn, sender) 182 if err != nil { 183 return dbus.MakeFailedError(err) 184 } 185 186 if !allowedSetting(new) { 187 return dbus.MakeFailedError(fmt.Errorf("cannot set setting %q to value %q: value not allowed", setting, new)) 188 } 189 new = fmt.Sprintf("%s_%s", snap, new) 190 df := filepath.Join(dirs.SnapDesktopFilesDir, new) 191 if !osutil.FileExists(df) { 192 return dbus.MakeFailedError(fmt.Errorf("cannot find desktop file %q", df)) 193 } 194 195 // FIXME: we need to know the parent PID or our dialog may pop under 196 // the existing windows. We might get it with the help of 197 // the xdg-settings tool inside the core snap. It would have 198 // to get the PID of the process asking for the settings 199 // then xdg-settings can sent this to us and we can intospect 200 // the X windows for _NET_WM_PID and use the windowID to 201 // attach to zenity - not sure how this translate to the 202 // wayland world though :/ 203 dialog, err := ui.New() 204 if err != nil { 205 return dbus.MakeFailedError(fmt.Errorf("cannot ask for settings change: %v", err)) 206 } 207 answeredYes := dialog.YesNo( 208 i18n.G("Allow settings change?"), 209 fmt.Sprintf(i18n.G("Allow snap %q to change %q to %q ?"), snap, setting, new), 210 &ui.DialogOptions{ 211 Timeout: defaultConfirmDialogTimeout, 212 Footer: i18n.G("This dialog will close automatically after 5 minutes of inactivity."), 213 }, 214 ) 215 if !answeredYes { 216 return dbus.MakeFailedError(fmt.Errorf("cannot change configuration: user declined change")) 217 } 218 219 cmd := exec.Command("xdg-settings", "set", setting, new) 220 output, err := cmd.CombinedOutput() 221 if err != nil { 222 return dbus.MakeFailedError(fmt.Errorf("cannot set setting %s: %s", setting, osutil.OutputErr(output, err))) 223 } 224 225 return nil 226 }