github.com/rigado/snapd@v2.42.5-go-mod+incompatible/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  	"path/filepath"
    28  	"strings"
    29  	"syscall"
    30  	"time"
    31  
    32  	"github.com/godbus/dbus"
    33  
    34  	"github.com/snapcore/snapd/dirs"
    35  	"github.com/snapcore/snapd/i18n"
    36  	"github.com/snapcore/snapd/osutil/sys"
    37  	"github.com/snapcore/snapd/strutil"
    38  	"github.com/snapcore/snapd/usersession/userd/ui"
    39  )
    40  
    41  const launcherIntrospectionXML = `
    42  <interface name="org.freedesktop.DBus.Peer">
    43  	<method name='Ping'>
    44  	</method>
    45  	<method name='GetMachineId'>
    46                 <arg type='s' name='machine_uuid' direction='out'/>
    47  	</method>
    48  </interface>
    49  <interface name='io.snapcraft.Launcher'>
    50  	<method name='OpenURL'>
    51  		<arg type='s' name='url' direction='in'/>
    52  	</method>
    53  	<method name="OpenFile">
    54  		<arg type="s" name="parent_window" direction="in"/>
    55  		<arg type="h" name="fd" direction="in"/>
    56  	</method>
    57  </interface>`
    58  
    59  var (
    60  	allowedURLSchemes = []string{"http", "https", "mailto", "snap", "help"}
    61  )
    62  
    63  // Launcher implements the 'io.snapcraft.Launcher' DBus interface.
    64  type Launcher struct {
    65  	conn *dbus.Conn
    66  }
    67  
    68  // Name returns the name of the interface this object implements
    69  func (s *Launcher) Name() string {
    70  	return "io.snapcraft.Launcher"
    71  }
    72  
    73  // BasePath returns the base path of the object
    74  func (s *Launcher) BasePath() dbus.ObjectPath {
    75  	return "/io/snapcraft/Launcher"
    76  }
    77  
    78  // IntrospectionData gives the XML formatted introspection description
    79  // of the DBus service.
    80  func (s *Launcher) IntrospectionData() string {
    81  	return launcherIntrospectionXML
    82  }
    83  
    84  func makeAccessDeniedError(err error) *dbus.Error {
    85  	return &dbus.Error{
    86  		Name: "org.freedesktop.DBus.Error.AccessDenied",
    87  		Body: []interface{}{err.Error()},
    88  	}
    89  }
    90  
    91  // OpenURL implements the 'OpenURL' method of the 'io.snapcraft.Launcher'
    92  // DBus interface. Before the provided url is passed to xdg-open the scheme is
    93  // validated against a list of allowed schemes. All other schemes are denied.
    94  func (s *Launcher) OpenURL(addr string, sender dbus.Sender) *dbus.Error {
    95  	u, err := url.Parse(addr)
    96  	if err != nil {
    97  		return &dbus.ErrMsgInvalidArg
    98  	}
    99  
   100  	if !strutil.ListContains(allowedURLSchemes, u.Scheme) {
   101  		return makeAccessDeniedError(fmt.Errorf("Supplied URL scheme %q is not allowed", u.Scheme))
   102  	}
   103  
   104  	snap, err := snapFromSender(s.conn, sender)
   105  	if err != nil {
   106  		return dbus.MakeFailedError(err)
   107  	}
   108  
   109  	xdg_data_dirs := []string{}
   110  	xdg_data_dirs = append(xdg_data_dirs, fmt.Sprintf(filepath.Join(dirs.SnapMountDir, snap, "current/usr/share")))
   111  	for _, dir := range strings.Split(os.Getenv("XDG_DATA_DIRS"), ":") {
   112  		xdg_data_dirs = append(xdg_data_dirs, dir)
   113  	}
   114  
   115  	cmd := exec.Command("xdg-open", addr)
   116  	cmd.Env = os.Environ()
   117  	cmd.Env = append(cmd.Env, fmt.Sprintf("XDG_DATA_DIRS=%s", strings.Join(xdg_data_dirs, ":")))
   118  
   119  	if err := cmd.Run(); err != nil {
   120  		return dbus.MakeFailedError(fmt.Errorf("cannot open supplied URL"))
   121  	}
   122  
   123  	return nil
   124  }
   125  
   126  // fdToFilename determines the path associated with an open file descriptor.
   127  //
   128  // The file descriptor cannot be opened using O_PATH and must refer to
   129  // a regular file or to a directory. The symlink at /proc/self/fd/<fd>
   130  // is read to determine the filename. The descriptor is also fstat'ed
   131  // and the resulting device number and inode number are compared to
   132  // stat on the path determined earlier. The numbers must match.
   133  func fdToFilename(fd int) (string, error) {
   134  	flags, err := sys.FcntlGetFl(fd)
   135  	if err != nil {
   136  		return "", err
   137  	}
   138  	// File descriptors opened with O_PATH do not imply access to
   139  	// the file in question.
   140  	if flags&sys.O_PATH != 0 {
   141  		return "", fmt.Errorf("cannot use file descriptors opened using O_PATH")
   142  	}
   143  
   144  	// Determine the file name associated with the passed file descriptor.
   145  	filename, err := os.Readlink(fmt.Sprintf("/proc/self/fd/%d", fd))
   146  	if err != nil {
   147  		return "", err
   148  	}
   149  
   150  	var fileStat, fdStat syscall.Stat_t
   151  	if err := syscall.Stat(filename, &fileStat); err != nil {
   152  		return "", err
   153  	}
   154  	if err := syscall.Fstat(fd, &fdStat); err != nil {
   155  		return "", err
   156  	}
   157  
   158  	// Sanity check to ensure we've got the right file
   159  	if fdStat.Dev != fileStat.Dev || fdStat.Ino != fileStat.Ino {
   160  		return "", fmt.Errorf("cannot determine file name")
   161  	}
   162  
   163  	fileType := fileStat.Mode & syscall.S_IFMT
   164  	if fileType != syscall.S_IFREG && fileType != syscall.S_IFDIR {
   165  		return "", fmt.Errorf("cannot open anything other than regular files or directories")
   166  	}
   167  
   168  	return filename, nil
   169  }
   170  
   171  func (s *Launcher) OpenFile(parentWindow string, clientFd dbus.UnixFD, sender dbus.Sender) *dbus.Error {
   172  	// godbus transfers ownership of this file descriptor to us
   173  	fd := int(clientFd)
   174  	defer syscall.Close(fd)
   175  
   176  	filename, err := fdToFilename(fd)
   177  	if err != nil {
   178  		return dbus.MakeFailedError(err)
   179  	}
   180  
   181  	snap, err := snapFromSender(s.conn, sender)
   182  	if err != nil {
   183  		return dbus.MakeFailedError(err)
   184  	}
   185  	dialog, err := ui.New()
   186  	if err != nil {
   187  		return dbus.MakeFailedError(err)
   188  	}
   189  	answeredYes := dialog.YesNo(
   190  		i18n.G("Allow opening file?"),
   191  		fmt.Sprintf(i18n.G("Allow snap %q to open file %q?"), snap, filename),
   192  		&ui.DialogOptions{
   193  			Timeout: 5 * 60 * time.Second,
   194  			Footer:  i18n.G("This dialog will close automatically after 5 minutes of inactivity."),
   195  		},
   196  	)
   197  	if !answeredYes {
   198  		return dbus.MakeFailedError(fmt.Errorf("permission denied"))
   199  	}
   200  
   201  	if err = exec.Command("xdg-open", filename).Run(); err != nil {
   202  		return dbus.MakeFailedError(fmt.Errorf("cannot open supplied URL"))
   203  	}
   204  
   205  	return nil
   206  }