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  }