github.com/ethanhsieh/snapd@v0.0.0-20210615102523-3db9b8e4edc5/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  	if err := checkOnClassic(); err != nil {
   238  		return "", err
   239  	}
   240  
   241  	settingMain := &settingSpec{setting: setting}
   242  	desktopFile, err := desktopFileFromValueForSetting(s, "check", settingMain, check, sender)
   243  	if err != nil {
   244  		return "", err
   245  	}
   246  
   247  	cmd := exec.Command("xdg-settings", "check", setting, desktopFile)
   248  	output, err := checkOutput(cmd, "check", settingMain)
   249  	if err != nil {
   250  		return "", err
   251  	}
   252  
   253  	return strings.TrimSpace(output), nil
   254  }
   255  
   256  // CheckSub implements the 'CheckSub' method of the 'io.snapcraft.Settings'
   257  // DBus interface.
   258  //
   259  // 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'
   260  func (s *Settings) CheckSub(setting string, subproperty string, check string, sender dbus.Sender) (string, *dbus.Error) {
   261  	if err := checkOnClassic(); err != nil {
   262  		return "", err
   263  	}
   264  
   265  	settingSub := &settingSpec{setting: setting, subproperty: subproperty}
   266  	desktopFile, err := desktopFileFromValueForSetting(s, "check", settingSub, check, sender)
   267  	if err != nil {
   268  		return "", err
   269  	}
   270  
   271  	cmd := exec.Command("xdg-settings", "check", setting, subproperty, desktopFile)
   272  	output, err := checkOutput(cmd, "check", settingSub)
   273  	if err != nil {
   274  		return "", err
   275  	}
   276  
   277  	return strings.TrimSpace(output), nil
   278  }
   279  
   280  // Get implements the 'Get' method of the 'io.snapcraft.Settings'
   281  // DBus interface.
   282  //
   283  // 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'
   284  func (s *Settings) Get(setting string, sender dbus.Sender) (string, *dbus.Error) {
   285  	if err := checkOnClassic(); err != nil {
   286  		return "", err
   287  	}
   288  
   289  	settingMain := &settingSpec{setting: setting}
   290  	if err := settingMain.validate(); err != nil {
   291  		return "", err
   292  	}
   293  
   294  	cmd := exec.Command("xdg-settings", "get", setting)
   295  	output, err := checkOutput(cmd, "get", settingMain)
   296  	if err != nil {
   297  		return "", err
   298  	}
   299  
   300  	return desktopFileFromOutput(s, output, sender)
   301  }
   302  
   303  // GetSub implements the 'GetSub' method of the 'io.snapcraft.Settings'
   304  // DBus interface.
   305  //
   306  // 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'
   307  func (s *Settings) GetSub(setting string, subproperty string, sender dbus.Sender) (string, *dbus.Error) {
   308  	if err := checkOnClassic(); err != nil {
   309  		return "", err
   310  	}
   311  
   312  	settingSub := &settingSpec{setting: setting, subproperty: subproperty}
   313  	if err := settingSub.validate(); err != nil {
   314  		return "", err
   315  	}
   316  
   317  	cmd := exec.Command("xdg-settings", "get", setting, subproperty)
   318  	output, err := checkOutput(cmd, "get", settingSub)
   319  	if err != nil {
   320  		return "", err
   321  	}
   322  
   323  	return desktopFileFromOutput(s, output, sender)
   324  }
   325  
   326  // Set implements the 'Set' method of the 'io.snapcraft.Settings'
   327  // DBus interface.
   328  //
   329  // 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'
   330  func (s *Settings) Set(setting string, new string, sender dbus.Sender) *dbus.Error {
   331  	if err := checkOnClassic(); err != nil {
   332  		return err
   333  	}
   334  
   335  	settingMain := &settingSpec{setting: setting}
   336  	desktopFile, err := desktopFileFromValueForSetting(s, "set", settingMain, new, sender)
   337  	if err != nil {
   338  		return err
   339  	}
   340  
   341  	if err := setDialog(s, settingMain, desktopFile, sender); err != nil {
   342  		return err
   343  	}
   344  
   345  	cmd := exec.Command("xdg-settings", "set", setting, desktopFile)
   346  	if _, err := checkOutput(cmd, "set", settingMain); err != nil {
   347  		return err
   348  	}
   349  
   350  	return nil
   351  }
   352  
   353  // SetSub implements the 'SetSub' method of the 'io.snapcraft.Settings'
   354  // DBus interface.
   355  //
   356  // 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'
   357  func (s *Settings) SetSub(setting string, subproperty string, new string, sender dbus.Sender) *dbus.Error {
   358  	if err := checkOnClassic(); err != nil {
   359  		return err
   360  	}
   361  
   362  	settingSub := &settingSpec{setting: setting, subproperty: subproperty}
   363  	desktopFile, err := desktopFileFromValueForSetting(s, "set", settingSub, new, sender)
   364  	if err != nil {
   365  		return err
   366  	}
   367  
   368  	if err := setDialog(s, settingSub, desktopFile, sender); err != nil {
   369  		return err
   370  	}
   371  
   372  	cmd := exec.Command("xdg-settings", "set", setting, subproperty, desktopFile)
   373  	if _, err := checkOutput(cmd, "set", settingSub); err != nil {
   374  		return err
   375  	}
   376  
   377  	return nil
   378  }