github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/usersession/userd/settings.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2017-2020 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 31 "github.com/snapcore/snapd/dirs" 32 "github.com/snapcore/snapd/i18n" 33 "github.com/snapcore/snapd/osutil" 34 "github.com/snapcore/snapd/snap" 35 "github.com/snapcore/snapd/usersession/userd/ui" 36 ) 37 38 // Timeout when the confirmation dialog for an xdg-setging 39 // automatically closes. Keep in sync with the core snaps 40 // xdg-settings wrapper which also sets this value to 300. 41 var defaultConfirmDialogTimeout = 300 * time.Second 42 43 const settingsIntrospectionXML = ` 44 <interface name="org.freedesktop.DBus.Peer"> 45 <method name='Ping'> 46 </method> 47 <method name='GetMachineId'> 48 <arg type='s' name='machine_uuid' direction='out'/> 49 </method> 50 </interface> 51 <interface name='io.snapcraft.Settings'> 52 <method name='Check'> 53 <arg type='s' name='setting' direction='in'/> 54 <arg type='s' name='check' direction='in'/> 55 <arg type='s' name='result' direction='out'/> 56 </method> 57 <method name='CheckSub'> 58 <arg type='s' name='setting' direction='in'/> 59 <arg type='s' name='subproperty' direction='in'/> 60 <arg type='s' name='check' direction='in'/> 61 <arg type='s' name='result' direction='out'/> 62 </method> 63 <method name='Get'> 64 <arg type='s' name='setting' direction='in'/> 65 <arg type='s' name='result' direction='out'/> 66 </method> 67 <method name='GetSub'> 68 <arg type='s' name='setting' direction='in'/> 69 <arg type='s' name='subproperty' direction='in'/> 70 <arg type='s' name='result' direction='out'/> 71 </method> 72 <method name='Set'> 73 <arg type='s' name='setting' direction='in'/> 74 <arg type='s' name='value' direction='in'/> 75 </method> 76 <method name='SetSub'> 77 <arg type='s' name='setting' direction='in'/> 78 <arg type='s' name='subproperty' direction='in'/> 79 <arg type='s' name='value' direction='in'/> 80 </method> 81 </interface>` 82 83 var validSettings = []string{ 84 "default-web-browser", 85 "default-url-scheme-handler", 86 } 87 88 func allowedSetting(setting string) bool { 89 if !strings.HasSuffix(setting, ".desktop") { 90 return false 91 } 92 base := strings.TrimSuffix(setting, ".desktop") 93 94 return snap.ValidAppName(base) 95 } 96 97 // settingSpec specifies a setting with an optional subproperty 98 type settingSpec struct { 99 setting string 100 subproperty string 101 } 102 103 func (s *settingSpec) String() string { 104 if s.subproperty != "" { 105 return fmt.Sprintf("%q subproperty %q", s.setting, s.subproperty) 106 } else { 107 return fmt.Sprintf("%q", s.setting) 108 } 109 } 110 111 func (s *settingSpec) validate() *dbus.Error { 112 for _, valid := range validSettings { 113 if s.setting == valid { 114 return nil 115 } 116 } 117 return dbus.MakeFailedError(fmt.Errorf("invalid setting %q", s.setting)) 118 } 119 120 // Settings implements the 'io.snapcraft.Settings' DBus interface. 121 type Settings struct { 122 conn *dbus.Conn 123 } 124 125 // Interface returns the name of the interface this object implements 126 func (s *Settings) Interface() string { 127 return "io.snapcraft.Settings" 128 } 129 130 // ObjectPath returns the path that the object is exported as 131 func (s *Settings) ObjectPath() dbus.ObjectPath { 132 return "/io/snapcraft/Settings" 133 } 134 135 // IntrospectionData gives the XML formatted introspection description 136 // of the DBus service. 137 func (s *Settings) IntrospectionData() string { 138 return settingsIntrospectionXML 139 } 140 141 // some notes: 142 // - we only set/get desktop files 143 // - all desktop files of snaps are prefixed with: ${snap}_ 144 // - on get/check/set we need to add/strip this prefix 145 146 func safeSnapFromSender(s *Settings, sender dbus.Sender) (string, *dbus.Error) { 147 // avoid information leak: see https://github.com/snapcore/snapd/pull/4073#discussion_r146682758 148 snap, err := snapFromSender(s.conn, sender) 149 if err != nil { 150 return "", dbus.MakeFailedError(err) 151 } 152 return snap, nil 153 } 154 155 func desktopFileFromValueForSetting(s *Settings, command string, setspec *settingSpec, dotDesktopValue string, sender dbus.Sender) (string, *dbus.Error) { 156 snap, err := safeSnapFromSender(s, sender) 157 if err != nil { 158 return "", err 159 } 160 if err := setspec.validate(); err != nil { 161 return "", err 162 } 163 164 if !allowedSetting(dotDesktopValue) { 165 return "", dbus.MakeFailedError(fmt.Errorf("cannot %s %s setting to invalid value %q", command, setspec, dotDesktopValue)) 166 } 167 168 // FIXME: this works only for desktop files 169 desktopFile := fmt.Sprintf("%s_%s", snap, dotDesktopValue) 170 return desktopFile, nil 171 } 172 173 func desktopFileFromOutput(s *Settings, output string, sender dbus.Sender) (string, *dbus.Error) { 174 snap, err := safeSnapFromSender(s, sender) 175 if err != nil { 176 return "", err 177 } 178 if !strings.HasPrefix(output, snap+"_") { 179 return "NOT-THIS-SNAP.desktop", nil 180 } 181 182 desktopFile := strings.SplitN(output, "_", 2)[1] 183 return strings.TrimSpace(desktopFile), nil 184 } 185 186 func setDialog(s *Settings, setspec *settingSpec, desktopFile string, sender dbus.Sender) *dbus.Error { 187 df := filepath.Join(dirs.SnapDesktopFilesDir, desktopFile) 188 if !osutil.FileExists(df) { 189 return dbus.MakeFailedError(fmt.Errorf("cannot find desktop file %q", df)) 190 } 191 192 // FIXME: we need to know the parent PID or our dialog may pop under 193 // the existing windows. We might get it with the help of 194 // the xdg-settings tool inside the core snap. It would have 195 // to get the PID of the process asking for the settings 196 // then xdg-settings can sent this to us and we can intospect 197 // the X windows for _NET_WM_PID and use the windowID to 198 // attach to zenity - not sure how this translate to the 199 // wayland world though :/ 200 dialog, uiErr := ui.New() 201 if uiErr != nil { 202 return dbus.MakeFailedError(fmt.Errorf("cannot ask for settings change: %v", uiErr)) 203 } 204 205 snap, err := safeSnapFromSender(s, sender) 206 if err != nil { 207 return err 208 } 209 210 answeredYes := dialog.YesNo( 211 i18n.G("Allow settings change?"), 212 fmt.Sprintf(i18n.G("Allow snap %q to change %s to %q ?"), snap, setspec, desktopFile), 213 &ui.DialogOptions{ 214 Timeout: defaultConfirmDialogTimeout, 215 Footer: i18n.G("This dialog will close automatically after 5 minutes of inactivity."), 216 }, 217 ) 218 if !answeredYes { 219 return dbus.MakeFailedError(fmt.Errorf("cannot change configuration: user declined change")) 220 } 221 return nil 222 } 223 224 func checkOutput(cmd *exec.Cmd, command string, setspec *settingSpec) (string, *dbus.Error) { 225 output, err := cmd.CombinedOutput() 226 if err != nil { 227 return "", dbus.MakeFailedError(fmt.Errorf("cannot %s %s setting: %s", command, setspec, osutil.OutputErr(output, err))) 228 } 229 return string(output), nil 230 } 231 232 // Check implements the 'Check' method of the 'io.snapcraft.Settings' 233 // DBus interface. 234 // 235 // 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' 236 func (s *Settings) Check(setting string, check string, sender dbus.Sender) (string, *dbus.Error) { 237 settingMain := &settingSpec{setting: setting} 238 desktopFile, err := desktopFileFromValueForSetting(s, "check", settingMain, check, sender) 239 if err != nil { 240 return "", err 241 } 242 243 cmd := exec.Command("xdg-settings", "check", setting, desktopFile) 244 output, err := checkOutput(cmd, "check", settingMain) 245 if err != nil { 246 return "", err 247 } 248 249 return strings.TrimSpace(output), nil 250 } 251 252 // CheckSub implements the 'CheckSub' method of the 'io.snapcraft.Settings' 253 // DBus interface. 254 // 255 // Example usage: dbus-send --session --dest=io.snapcraft.Settings --type=method_call --print-reply /io/snapcraft/Settings io.snapcraft.Settings.CheckSub string:'default-url-scheme-handler' string:'irc' string:'ircclient.desktop' 256 func (s *Settings) CheckSub(setting string, subproperty string, check string, sender dbus.Sender) (string, *dbus.Error) { 257 settingSub := &settingSpec{setting: setting, subproperty: subproperty} 258 desktopFile, err := desktopFileFromValueForSetting(s, "check", settingSub, check, sender) 259 if err != nil { 260 return "", err 261 } 262 263 cmd := exec.Command("xdg-settings", "check", setting, subproperty, desktopFile) 264 output, err := checkOutput(cmd, "check", settingSub) 265 if err != nil { 266 return "", err 267 } 268 269 return strings.TrimSpace(output), nil 270 } 271 272 // Get implements the 'Get' method of the 'io.snapcraft.Settings' 273 // DBus interface. 274 // 275 // 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' 276 func (s *Settings) Get(setting string, sender dbus.Sender) (string, *dbus.Error) { 277 settingMain := &settingSpec{setting: setting} 278 if err := settingMain.validate(); err != nil { 279 return "", err 280 } 281 282 cmd := exec.Command("xdg-settings", "get", setting) 283 output, err := checkOutput(cmd, "get", settingMain) 284 if err != nil { 285 return "", err 286 } 287 288 return desktopFileFromOutput(s, output, sender) 289 } 290 291 // GetSub implements the 'GetSub' method of the 'io.snapcraft.Settings' 292 // DBus interface. 293 // 294 // Example usage: dbus-send --session --dest=io.snapcraft.Settings --type=method_call --print-reply /io/snapcraft/Settings io.snapcraft.Settings.GetSub string:'default-url-scheme-handler' string:'irc' 295 func (s *Settings) GetSub(setting string, subproperty string, sender dbus.Sender) (string, *dbus.Error) { 296 settingSub := &settingSpec{setting: setting, subproperty: subproperty} 297 if err := settingSub.validate(); err != nil { 298 return "", err 299 } 300 301 cmd := exec.Command("xdg-settings", "get", setting, subproperty) 302 output, err := checkOutput(cmd, "get", settingSub) 303 if err != nil { 304 return "", err 305 } 306 307 return desktopFileFromOutput(s, output, sender) 308 } 309 310 // Set implements the 'Set' method of the 'io.snapcraft.Settings' 311 // DBus interface. 312 // 313 // 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' 314 func (s *Settings) Set(setting string, new string, sender dbus.Sender) *dbus.Error { 315 settingMain := &settingSpec{setting: setting} 316 desktopFile, err := desktopFileFromValueForSetting(s, "set", settingMain, new, sender) 317 if err != nil { 318 return err 319 } 320 321 if err := setDialog(s, settingMain, desktopFile, sender); err != nil { 322 return err 323 } 324 325 cmd := exec.Command("xdg-settings", "set", setting, desktopFile) 326 if _, err := checkOutput(cmd, "set", settingMain); err != nil { 327 return err 328 } 329 330 return nil 331 } 332 333 // SetSub implements the 'SetSub' method of the 'io.snapcraft.Settings' 334 // DBus interface. 335 // 336 // Example usage: dbus-send --session --dest=io.snapcraft.Settings --type=method_call --print-reply /io/snapcraft/Settings io.snapcraft.Settings.SetSub string:'default-url-scheme-handler' string:'irc' string:'ircclient.desktop' 337 func (s *Settings) SetSub(setting string, subproperty string, new string, sender dbus.Sender) *dbus.Error { 338 settingSub := &settingSpec{setting: setting, subproperty: subproperty} 339 desktopFile, err := desktopFileFromValueForSetting(s, "set", settingSub, new, sender) 340 if err != nil { 341 return err 342 } 343 344 if err := setDialog(s, settingSub, desktopFile, sender); err != nil { 345 return err 346 } 347 348 cmd := exec.Command("xdg-settings", "set", setting, subproperty, desktopFile) 349 if _, err := checkOutput(cmd, "set", settingSub); err != nil { 350 return err 351 } 352 353 return nil 354 }