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 }