github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/usersession/userd/launcher.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  	"net/url"
    25  	"os"
    26  	"os/exec"
    27  	"syscall"
    28  	"time"
    29  
    30  	"github.com/godbus/dbus"
    31  
    32  	"github.com/snapcore/snapd/i18n"
    33  	"github.com/snapcore/snapd/osutil/sys"
    34  	"github.com/snapcore/snapd/release"
    35  	"github.com/snapcore/snapd/strutil"
    36  	"github.com/snapcore/snapd/usersession/userd/ui"
    37  )
    38  
    39  const launcherIntrospectionXML = `
    40  <interface name="org.freedesktop.DBus.Peer">
    41  	<method name='Ping'>
    42  	</method>
    43  	<method name='GetMachineId'>
    44                 <arg type='s' name='machine_uuid' direction='out'/>
    45  	</method>
    46  </interface>
    47  <interface name='io.snapcraft.Launcher'>
    48  	<method name='OpenURL'>
    49  		<arg type='s' name='url' direction='in'/>
    50  	</method>
    51  	<method name="OpenFile">
    52  		<arg type="s" name="parent_window" direction="in"/>
    53  		<arg type="h" name="fd" direction="in"/>
    54  	</method>
    55  </interface>`
    56  
    57  // allowedURLSchemes are those that can be passed to xdg-open so that it may
    58  // launch the handler for the url scheme on behalf of the snap (and therefore
    59  // outside of the calling snap's confinement). Historically we've been
    60  // conservative about adding url schemes but the thinking was refined in
    61  // https://github.com/snapcore/snapd/pull/7731#pullrequestreview-362900171
    62  //
    63  // The current criteria for adding url schemes is:
    64  // * understanding and documenting the scheme in this file
    65  // * the scheme itself does not cause xdg-open to open files (eg, file:// or
    66  //   matching '^[[:alpha:]+\.\-]+:' (from xdg-open source))
    67  // * verifying that the recipient of the url (ie, what xdg-open calls) won't
    68  //   process file paths/etc that can be leveraged to break out of the sandbox
    69  //   (but understanding how the url can drive the recipient application is
    70  //   important)
    71  //
    72  // This code uses golang's net/url.Parse() which will help ensure the url is
    73  // ok before passing to xdg-open. xdg-open itself properly quotes the url so
    74  // shell metacharacters are blocked.
    75  var (
    76  	allowedURLSchemes = []string{
    77  		// apt: the scheme allows specifying a package for xdg-open to pass to an
    78  		//   apt-handling application, like gnome-software, apturl, etc which are all
    79  		//   protected by policykit
    80  		//   - scheme: apt:<name of package>
    81  		//   - https://github.com/snapcore/snapd/pull/7731
    82  		"apt",
    83  		// help: the scheme allows for specifying a help URL. This code ensures that
    84  		//   the url is parseable
    85  		//   - scheme: help://topic
    86  		//   - https://github.com/snapcore/snapd/pull/6493
    87  		"help",
    88  		// http/https: the scheme allows specifying a web URL. This code ensures that
    89  		//   the url is parseable
    90  		//   - scheme: http(s)://example.com
    91  		"http",
    92  		"https",
    93  		// mailto: the scheme allows for specifying an email address
    94  		//   - scheme: mailto:foo@example.com
    95  		"mailto",
    96  		// msteams: the scheme is a thin wrapper around https.
    97  		//   - scheme: msteams:...
    98  		//   - https://github.com/snapcore/snapd/pull/8761
    99  		"msteams",
   100  		// TODO: document slack URL scheme.
   101  		"slack",
   102  		// snap: the scheme allows specifying a package for xdg-open to pass to a
   103  		//   snap-handling installer application, like snap-store, etc which are
   104  		//   protected by policykit/snap login
   105  		//   - https://github.com/snapcore/snapd/pull/5181
   106  		"snap",
   107  		// zoommtg: the scheme is a modified web url scheme
   108  		//   - scheme: https://medium.com/zoom-developer-blog/zoom-url-schemes-748b95fd9205
   109  		//     (eg, zoommtg://zoom.us/...)
   110  		//   - https://github.com/snapcore/snapd/pull/8304
   111  		"zoommtg",
   112  		// zoomphonecall: another zoom URL scheme, for dialing phone numbers
   113  		//   - https://github.com/snapcore/snapd/pull/8910
   114  		"zoomphonecall",
   115  		// zoomus: alternative name for zoommtg
   116  		//   - https://github.com/snapcore/snapd/pull/8910
   117  		"zoomus",
   118  	}
   119  )
   120  
   121  // Launcher implements the 'io.snapcraft.Launcher' DBus interface.
   122  type Launcher struct {
   123  	conn *dbus.Conn
   124  }
   125  
   126  // Interface returns the name of the interface this object implements
   127  func (s *Launcher) Interface() string {
   128  	return "io.snapcraft.Launcher"
   129  }
   130  
   131  // ObjectPath returns the path that the object is exported as
   132  func (s *Launcher) ObjectPath() dbus.ObjectPath {
   133  	return "/io/snapcraft/Launcher"
   134  }
   135  
   136  // IntrospectionData gives the XML formatted introspection description
   137  // of the DBus service.
   138  func (s *Launcher) IntrospectionData() string {
   139  	return launcherIntrospectionXML
   140  }
   141  
   142  func makeAccessDeniedError(err error) *dbus.Error {
   143  	return &dbus.Error{
   144  		Name: "org.freedesktop.DBus.Error.AccessDenied",
   145  		Body: []interface{}{err.Error()},
   146  	}
   147  }
   148  
   149  func checkOnClassic() *dbus.Error {
   150  	if !release.OnClassic {
   151  		return makeAccessDeniedError(fmt.Errorf("not supported on Ubuntu Core"))
   152  	}
   153  	return nil
   154  }
   155  
   156  // OpenURL implements the 'OpenURL' method of the 'io.snapcraft.Launcher'
   157  // DBus interface. Before the provided url is passed to xdg-open the scheme is
   158  // validated against a list of allowed schemes. All other schemes are denied.
   159  func (s *Launcher) OpenURL(addr string, sender dbus.Sender) *dbus.Error {
   160  	if err := checkOnClassic(); err != nil {
   161  		return err
   162  	}
   163  
   164  	u, err := url.Parse(addr)
   165  	if err != nil {
   166  		return &dbus.ErrMsgInvalidArg
   167  	}
   168  
   169  	if !strutil.ListContains(allowedURLSchemes, u.Scheme) {
   170  		return makeAccessDeniedError(fmt.Errorf("Supplied URL scheme %q is not allowed", u.Scheme))
   171  	}
   172  
   173  	if err := exec.Command("xdg-open", addr).Run(); err != nil {
   174  		return dbus.MakeFailedError(fmt.Errorf("cannot open supplied URL"))
   175  	}
   176  
   177  	return nil
   178  }
   179  
   180  // fdToFilename determines the path associated with an open file descriptor.
   181  //
   182  // The file descriptor cannot be opened using O_PATH and must refer to
   183  // a regular file or to a directory. The symlink at /proc/self/fd/<fd>
   184  // is read to determine the filename. The descriptor is also fstat'ed
   185  // and the resulting device number and inode number are compared to
   186  // stat on the path determined earlier. The numbers must match.
   187  func fdToFilename(fd int) (string, error) {
   188  	flags, err := sys.FcntlGetFl(fd)
   189  	if err != nil {
   190  		return "", err
   191  	}
   192  	// File descriptors opened with O_PATH do not imply access to
   193  	// the file in question.
   194  	if flags&sys.O_PATH != 0 {
   195  		return "", fmt.Errorf("cannot use file descriptors opened using O_PATH")
   196  	}
   197  
   198  	// Determine the file name associated with the passed file descriptor.
   199  	filename, err := os.Readlink(fmt.Sprintf("/proc/self/fd/%d", fd))
   200  	if err != nil {
   201  		return "", err
   202  	}
   203  
   204  	var fileStat, fdStat syscall.Stat_t
   205  	if err := syscall.Stat(filename, &fileStat); err != nil {
   206  		return "", err
   207  	}
   208  	if err := syscall.Fstat(fd, &fdStat); err != nil {
   209  		return "", err
   210  	}
   211  
   212  	// Sanity check to ensure we've got the right file
   213  	if fdStat.Dev != fileStat.Dev || fdStat.Ino != fileStat.Ino {
   214  		return "", fmt.Errorf("cannot determine file name")
   215  	}
   216  
   217  	fileType := fileStat.Mode & syscall.S_IFMT
   218  	if fileType != syscall.S_IFREG && fileType != syscall.S_IFDIR {
   219  		return "", fmt.Errorf("cannot open anything other than regular files or directories")
   220  	}
   221  
   222  	return filename, nil
   223  }
   224  
   225  func (s *Launcher) OpenFile(parentWindow string, clientFd dbus.UnixFD, sender dbus.Sender) *dbus.Error {
   226  	// godbus transfers ownership of this file descriptor to us
   227  	fd := int(clientFd)
   228  	defer syscall.Close(fd)
   229  
   230  	if err := checkOnClassic(); err != nil {
   231  		return err
   232  	}
   233  
   234  	filename, err := fdToFilename(fd)
   235  	if err != nil {
   236  		return dbus.MakeFailedError(err)
   237  	}
   238  
   239  	snap, err := snapFromSender(s.conn, sender)
   240  	if err != nil {
   241  		return dbus.MakeFailedError(err)
   242  	}
   243  	dialog, err := ui.New()
   244  	if err != nil {
   245  		return dbus.MakeFailedError(err)
   246  	}
   247  	answeredYes := dialog.YesNo(
   248  		i18n.G("Allow opening file?"),
   249  		fmt.Sprintf(i18n.G("Allow snap %q to open file %q?"), snap, filename),
   250  		&ui.DialogOptions{
   251  			Timeout: 5 * 60 * time.Second,
   252  			Footer:  i18n.G("This dialog will close automatically after 5 minutes of inactivity."),
   253  		},
   254  	)
   255  	if !answeredYes {
   256  		return dbus.MakeFailedError(fmt.Errorf("permission denied"))
   257  	}
   258  
   259  	if err = exec.Command("xdg-open", filename).Run(); err != nil {
   260  		return dbus.MakeFailedError(fmt.Errorf("cannot open supplied URL"))
   261  	}
   262  
   263  	return nil
   264  }