github.com/david-imola/snapd@v0.0.0-20210611180407-2de8ddeece6d/usersession/xdgopenproxy/portal_launcher.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 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 xdgopenproxy 21 22 import ( 23 "fmt" 24 "syscall" 25 "time" 26 27 "github.com/godbus/dbus" 28 ) 29 30 const ( 31 desktopPortalBusName = "org.freedesktop.portal.Desktop" 32 desktopPortalObjectPath = "/org/freedesktop/portal/desktop" 33 desktopPortalOpenURIIface = "org.freedesktop.portal.OpenURI" 34 desktopPortalRequestIface = "org.freedesktop.portal.Request" 35 ) 36 37 // portalLauncher is a launcher that forwards the requests to xdg-desktop-portal DBus API 38 type portalLauncher struct{} 39 40 // desktopPortal gets a reference to the xdg-desktop-portal D-Bus service 41 func (p *portalLauncher) desktopPortal(bus *dbus.Conn) (dbus.BusObject, error) { 42 // We call StartServiceByName since old versions of 43 // xdg-desktop-portal do not include the AssumedAppArmorLabel 44 // key in their service activation file. 45 var startResult uint32 46 err := bus.BusObject().Call("org.freedesktop.DBus.StartServiceByName", 0, desktopPortalBusName, uint32(0)).Store(&startResult) 47 if dbusErr, ok := err.(dbus.Error); ok { 48 // If it is not possible to activate the service 49 // (i.e. there is no .service file or the systemd unit 50 // has been masked), assume it is already 51 // running. Subsequent method calls will fail if this 52 // assumption is false. 53 if dbusErr.Name == "org.freedesktop.DBus.Error.ServiceUnknown" || dbusErr.Name == "org.freedesktop.systemd1.Masked" { 54 err = nil 55 startResult = 2 // DBUS_START_REPLY_ALREADY_RUNNING 56 } 57 } 58 if err != nil { 59 return nil, err 60 } 61 switch startResult { 62 case 1: // DBUS_START_REPLY_SUCCESS 63 case 2: // DBUS_START_REPLY_ALREADY_RUNNING 64 default: 65 return nil, fmt.Errorf("unexpected response from StartServiceByName (code %v)", startResult) 66 } 67 return bus.Object(desktopPortalBusName, desktopPortalObjectPath), nil 68 } 69 70 // portalResponseSuccess is a numeric value indicating a success carrying out 71 // the request, returned by the `response` member of 72 // org.freedesktop.portal.Request.Response signal 73 const portalResponseSuccess = 0 74 75 // timeout for asking the user to make a choice, same value as in usersession/userd/launcher.go 76 var defaultPortalRequestTimeout = 5 * time.Minute 77 78 func (p *portalLauncher) portalCall(bus *dbus.Conn, call func() (dbus.ObjectPath, error)) error { 79 // see https://flatpak.github.io/xdg-desktop-portal/portal-docs.html for 80 // details of the interaction, in short: 81 // 1. caller issues a request to the desktop portal 82 // 2. desktop portal responds with a handle to a dbus object capturing the Request 83 // 3. caller waits for the org.freedesktop.portal.Request.Response 84 // 3a. caller can terminate the request earlier by calling close 85 86 // set up signal handling before we call the portal, so that we do not 87 // miss the signals 88 signals := make(chan *dbus.Signal, 1) 89 bus.Signal(signals) 90 defer func() { 91 bus.RemoveSignal(signals) 92 close(signals) 93 }() 94 95 // TODO: this should use dbus.Conn.AddMatchSignal, but that 96 // does not exist in the external copies of godbus on some 97 // supported platforms. 98 const matchRule = "type='signal',sender='" + desktopPortalBusName + "',interface='" + desktopPortalRequestIface + "',member='Response'" 99 if err := bus.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, matchRule).Store(); err != nil { 100 return err 101 } 102 defer bus.BusObject().Call("org.freedesktop.DBus.RemoveMatch", 0, matchRule) 103 104 requestPath, err := call() 105 if err != nil { 106 return err 107 } 108 request := bus.Object(desktopPortalBusName, requestPath) 109 110 timeout := time.NewTimer(defaultPortalRequestTimeout) 111 defer timeout.Stop() 112 for { 113 select { 114 case <-timeout.C: 115 request.Call(desktopPortalRequestIface+".Close", 0).Store() 116 return &responseError{msg: "timeout waiting for user response"} 117 case signal := <-signals: 118 if signal.Path != requestPath || signal.Name != desktopPortalRequestIface+".Response" { 119 // This isn't the signal we're waiting for 120 continue 121 } 122 123 var response uint32 124 var results map[string]interface{} // don't care 125 if err := dbus.Store(signal.Body, &response, &results); err != nil { 126 return &responseError{msg: fmt.Sprintf("cannot unpack response: %v", err)} 127 } 128 if response == portalResponseSuccess { 129 return nil 130 } 131 return &responseError{msg: fmt.Sprintf("request declined by the user (code %v)", response)} 132 } 133 } 134 } 135 136 func (p *portalLauncher) OpenFile(bus *dbus.Conn, filename string) error { 137 portal, err := p.desktopPortal(bus) 138 if err != nil { 139 return err 140 } 141 142 fd, err := syscall.Open(filename, syscall.O_RDONLY, 0) 143 if err != nil { 144 return &responseError{msg: err.Error()} 145 } 146 defer syscall.Close(fd) 147 148 return p.portalCall(bus, func() (dbus.ObjectPath, error) { 149 var ( 150 parent string 151 options map[string]dbus.Variant 152 request dbus.ObjectPath 153 ) 154 err := portal.Call(desktopPortalOpenURIIface+".OpenFile", 0, parent, dbus.UnixFD(fd), options).Store(&request) 155 return request, err 156 }) 157 } 158 159 func (p *portalLauncher) OpenURI(bus *dbus.Conn, uri string) error { 160 portal, err := p.desktopPortal(bus) 161 if err != nil { 162 return err 163 } 164 165 return p.portalCall(bus, func() (dbus.ObjectPath, error) { 166 var ( 167 parent string 168 options map[string]dbus.Variant 169 request dbus.ObjectPath 170 ) 171 err := portal.Call(desktopPortalOpenURIIface+".OpenURI", 0, parent, uri, options).Store(&request) 172 return request, err 173 }) 174 }