400 lines
11 KiB
Go
400 lines
11 KiB
Go
//go:build portal
|
|
|
|
/*
|
|
* Copyright (c) 2018 Unrud <unrud@outlook.com>
|
|
*
|
|
* This file is part of Remote-Touchpad.
|
|
*
|
|
* Remote-Touchpad is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* Remote-Touchpad is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
package inputcontrol
|
|
|
|
import (
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/hkdf"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
|
|
"github.com/godbus/dbus/v5"
|
|
)
|
|
|
|
const (
|
|
deviceKeyboard uint32 = 1
|
|
devicePointer uint32 = 2
|
|
|
|
btnReleased uint32 = 0
|
|
btnPressed uint32 = 1
|
|
|
|
untilRevoked uint32 = 2
|
|
|
|
// linux/input-event-codes.h
|
|
btnLeft int32 = 0x110
|
|
btnRight int32 = 0x111
|
|
btnMiddle int32 = 0x112
|
|
)
|
|
|
|
type portalController struct {
|
|
bus *dbus.Conn
|
|
portalDesktop dbus.BusObject
|
|
sessionHandle dbus.ObjectPath
|
|
}
|
|
|
|
func init() {
|
|
RegisterController("RemoteDesktop portal", InitPortalController, 1)
|
|
}
|
|
|
|
func InitPortalController() (Controller, error) {
|
|
bus, err := dbus.SessionBusPrivate()
|
|
if err != nil {
|
|
return nil, &UnsupportedPlatformError{err}
|
|
}
|
|
cleanupBus := true
|
|
defer func() {
|
|
if cleanupBus {
|
|
bus.Close()
|
|
}
|
|
}()
|
|
err = bus.Auth(nil)
|
|
if err != nil {
|
|
return nil, &UnsupportedPlatformError{err}
|
|
}
|
|
err = bus.Hello()
|
|
if err != nil {
|
|
return nil, &UnsupportedPlatformError{err}
|
|
}
|
|
portalDesktop := bus.Object("org.freedesktop.portal.Desktop",
|
|
"/org/freedesktop/portal/desktop")
|
|
remoteDesktopVersionV, err := portalDesktop.GetProperty(
|
|
"org.freedesktop.portal.RemoteDesktop.version")
|
|
if err != nil {
|
|
return nil, &UnsupportedPlatformError{
|
|
fmt.Errorf("getting 'version' failed: %w", err),
|
|
}
|
|
}
|
|
remoteDesktopVersion, ok := remoteDesktopVersionV.Value().(uint32)
|
|
if !ok {
|
|
return nil, &UnsupportedPlatformError{
|
|
errors.New("unexpected 'version' type"),
|
|
}
|
|
}
|
|
restoreTokenStore, err := func() (*secretStore, error) {
|
|
if remoteDesktopVersion < 2 {
|
|
return nil, nil
|
|
}
|
|
cacheDirectory, err := os.UserCacheDir()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := os.MkdirAll(cacheDirectory, 0o700); err != nil {
|
|
return nil, err
|
|
}
|
|
secret, err := retrieveSecret(bus)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return newSecretStore(secret,
|
|
filepath.Join(cacheDirectory, "remote-touchpad.portal-restore-token.bin"))
|
|
}()
|
|
if err != nil {
|
|
log.Printf("Skipping restore token: %v", err)
|
|
}
|
|
availableDeviceTypesV, err := portalDesktop.GetProperty(
|
|
"org.freedesktop.portal.RemoteDesktop.AvailableDeviceTypes")
|
|
if err != nil {
|
|
return nil, &UnsupportedPlatformError{
|
|
fmt.Errorf("getting 'AvailableDeviceTypes' failed: %w", err),
|
|
}
|
|
}
|
|
availableDeviceTypes, ok := availableDeviceTypesV.Value().(uint32)
|
|
if !ok {
|
|
return nil, &UnsupportedPlatformError{
|
|
errors.New("unexpected 'AvailableDeviceTypes' return type"),
|
|
}
|
|
}
|
|
if availableDeviceTypes&deviceKeyboard == 0 ||
|
|
availableDeviceTypes&devicePointer == 0 {
|
|
return nil, &UnsupportedPlatformError{
|
|
errors.New("keyboard or pointer source type not supported"),
|
|
}
|
|
}
|
|
createSessionResults, err := checkResponse(getResponse(bus, portalDesktop,
|
|
"org.freedesktop.portal.RemoteDesktop.CreateSession", 0,
|
|
map[string]dbus.Variant{"session_handle_token": dbus.MakeVariant("t")},
|
|
))
|
|
if err != nil {
|
|
return nil, &UnsupportedPlatformError{
|
|
fmt.Errorf("calling 'CreateSession' failed: %w", err),
|
|
}
|
|
}
|
|
sessionHandleString, ok := createSessionResults["session_handle"].Value().(string)
|
|
if !ok {
|
|
return nil, &UnsupportedPlatformError{
|
|
errors.New("unexpected 'session_handle' type in 'CreateSession' return value"),
|
|
}
|
|
}
|
|
sessionHandle := dbus.ObjectPath(sessionHandleString)
|
|
selectDevicesOptions := map[string]dbus.Variant{
|
|
"types": dbus.MakeVariant(deviceKeyboard | devicePointer),
|
|
}
|
|
if restoreTokenStore != nil {
|
|
if restoreToken, err := restoreTokenStore.Load(); err != nil {
|
|
if !errors.Is(err, os.ErrNotExist) {
|
|
log.Printf("Failed to load restore token: %v", err)
|
|
}
|
|
} else if len(restoreToken) > 0 {
|
|
selectDevicesOptions["restore_token"] = dbus.MakeVariant(string(restoreToken))
|
|
}
|
|
selectDevicesOptions["persist_mode"] = dbus.MakeVariant(untilRevoked)
|
|
}
|
|
_, err = checkResponse(getResponse(bus, portalDesktop,
|
|
"org.freedesktop.portal.RemoteDesktop.SelectDevices", 0,
|
|
sessionHandle, selectDevicesOptions,
|
|
))
|
|
if err != nil {
|
|
return nil, &UnsupportedPlatformError{
|
|
fmt.Errorf("calling 'SelectDevices' failed: %w", err),
|
|
}
|
|
}
|
|
startResponseStatus, startResults, err := getResponse(bus, portalDesktop,
|
|
"org.freedesktop.portal.RemoteDesktop.Start", 0,
|
|
sessionHandle, "", map[string]dbus.Variant{},
|
|
)
|
|
if err != nil {
|
|
return nil, &UnsupportedPlatformError{
|
|
fmt.Errorf("calling 'Start' failed: %w", err),
|
|
}
|
|
}
|
|
if startResponseStatus != 0 {
|
|
return nil, errors.New("keyboard or pointer access denied")
|
|
}
|
|
if restoreToken, _ := startResults["restore_token"].Value().(string); restoreTokenStore != nil {
|
|
if err := restoreTokenStore.Store([]byte(restoreToken)); err != nil {
|
|
log.Printf("Failed to store restore token: %v", err)
|
|
}
|
|
}
|
|
devices, ok := startResults["devices"].Value().(uint32)
|
|
if !ok {
|
|
return nil, &UnsupportedPlatformError{
|
|
errors.New("unexpected 'devices' type in 'Start' return value"),
|
|
}
|
|
}
|
|
if devices&deviceKeyboard == 0 || devices&devicePointer == 0 {
|
|
return nil, errors.New("keyboard or pointer access denied")
|
|
}
|
|
cleanupBus = false
|
|
return &portalController{
|
|
bus: bus, portalDesktop: portalDesktop,
|
|
sessionHandle: sessionHandle,
|
|
}, nil
|
|
}
|
|
|
|
func retrieveSecret(bus *dbus.Conn) ([]byte, error) {
|
|
portalDesktop := bus.Object("org.freedesktop.portal.Desktop",
|
|
"/org/freedesktop/portal/desktop")
|
|
secretReader, secretWriter, err := os.Pipe()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer secretReader.Close()
|
|
defer secretWriter.Close()
|
|
if _, err := checkResponse(getResponse(bus, portalDesktop,
|
|
"org.freedesktop.portal.Secret.RetrieveSecret", 0,
|
|
dbus.UnixFD(secretWriter.Fd()), map[string]dbus.Variant{},
|
|
)); err != nil {
|
|
return nil, fmt.Errorf("calling 'RetrieveSecret' failed: %w", err)
|
|
}
|
|
if err := secretWriter.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
secret, err := io.ReadAll(secretReader)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(secret) < 16 {
|
|
return nil, fmt.Errorf("'RetrieveSecret' returned too few bytes (%d)", len(secret))
|
|
}
|
|
return secret, err
|
|
}
|
|
|
|
type secretStore struct {
|
|
aesgcm cipher.AEAD
|
|
filename string
|
|
}
|
|
|
|
func newSecretStore(key []byte, filename string) (*secretStore, error) {
|
|
derivedKey, err := hkdf.Key(sha256.New, key, nil, "", 32)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
block, err := aes.NewCipher(derivedKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
aesgcm, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &secretStore{
|
|
aesgcm: aesgcm,
|
|
filename: filename,
|
|
}, nil
|
|
}
|
|
|
|
func (s *secretStore) Load() ([]byte, error) {
|
|
data, err := os.ReadFile(s.filename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(data) < s.aesgcm.NonceSize() {
|
|
return nil, errors.New("invalid ciphertext")
|
|
}
|
|
nonce := data[:s.aesgcm.NonceSize()]
|
|
ciphertext := data[len(nonce):]
|
|
return s.aesgcm.Open(nil, nonce, ciphertext, nil)
|
|
}
|
|
|
|
func (s *secretStore) Store(data []byte) error {
|
|
nonce := make([]byte, s.aesgcm.NonceSize())
|
|
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
|
return err
|
|
}
|
|
ciphertext := s.aesgcm.Seal(nil, nonce, data, nil)
|
|
return os.WriteFile(s.filename, slices.Concat(nonce, ciphertext), 0o600)
|
|
}
|
|
|
|
func getResponse(bus *dbus.Conn, object dbus.BusObject, method string,
|
|
flags dbus.Flags, args ...interface{},
|
|
) (uint32, map[string]dbus.Variant, error) {
|
|
ch := make(chan *dbus.Signal, 512)
|
|
bus.Signal(ch)
|
|
defer bus.RemoveSignal(ch)
|
|
var requestPath dbus.ObjectPath
|
|
if err := object.Call(method, flags, args...).Store(&requestPath); err != nil {
|
|
return 0, nil, err
|
|
}
|
|
for {
|
|
s := <-ch
|
|
if s.Path == requestPath && s.Name == "org.freedesktop.portal.Request.Response" {
|
|
if len(s.Body) != 2 {
|
|
return 0, nil, fmt.Errorf("unexpected 'Response' return length (%d)", len(s.Body))
|
|
}
|
|
responseStatus, ok := s.Body[0].(uint32)
|
|
if !ok {
|
|
return 0, nil, errors.New("unexpected 'Response' return type")
|
|
}
|
|
results, ok := s.Body[1].(map[string]dbus.Variant)
|
|
if !ok {
|
|
return 0, nil, errors.New("unexpected 'Response' return type")
|
|
}
|
|
return responseStatus, results, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func checkResponse(responseStatus uint32, results map[string]dbus.Variant, err error) (map[string]dbus.Variant, error) {
|
|
if err == nil && responseStatus != 0 {
|
|
err = fmt.Errorf("unexpected 'Response' status (%d)", responseStatus)
|
|
}
|
|
return results, err
|
|
}
|
|
|
|
func (p *portalController) Close() error {
|
|
return p.bus.Close()
|
|
}
|
|
|
|
func (p *portalController) keyboardKeys(keys []Keysym) error {
|
|
for _, keysym := range keys {
|
|
for _, state := range [...]uint32{btnPressed, btnReleased} {
|
|
if err := p.portalDesktop.Call(
|
|
"org.freedesktop.portal.RemoteDesktop.NotifyKeyboardKeysym", 0,
|
|
p.sessionHandle, map[string]dbus.Variant{}, keysym, state,
|
|
).Store(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *portalController) KeyboardText(text string) error {
|
|
keys := make([]Keysym, 0, len(text))
|
|
for _, runeValue := range text {
|
|
keysym, err := RuneToKeysym(runeValue)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
keys = append(keys, keysym)
|
|
}
|
|
return p.keyboardKeys(keys)
|
|
}
|
|
|
|
func (p *portalController) KeyboardKey(key Key) error {
|
|
keysym, err := KeyToKeysym(key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
keys := [...]Keysym{keysym}
|
|
return p.keyboardKeys(keys[:])
|
|
}
|
|
|
|
func (p *portalController) PointerButton(button PointerButton, press bool) error {
|
|
var btn int32
|
|
switch button {
|
|
case PointerButtonLeft:
|
|
btn = btnLeft
|
|
case PointerButtonMiddle:
|
|
btn = btnMiddle
|
|
case PointerButtonRight:
|
|
btn = btnRight
|
|
default:
|
|
return fmt.Errorf("unsupported pointer button: %#v", button)
|
|
}
|
|
state := btnReleased
|
|
if press {
|
|
state = btnPressed
|
|
}
|
|
if err := p.portalDesktop.Call("org.freedesktop.portal.RemoteDesktop.NotifyPointerButton", 0,
|
|
p.sessionHandle, map[string]dbus.Variant{}, btn, state,
|
|
).Store(); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *portalController) PointerMove(deltaX, deltaY int) error {
|
|
if err := p.portalDesktop.Call("org.freedesktop.portal.RemoteDesktop.NotifyPointerMotion", 0,
|
|
p.sessionHandle, map[string]dbus.Variant{}, float64(deltaX), float64(deltaY),
|
|
).Store(); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *portalController) PointerScroll(deltaHorizontal, deltaVertical int, finish bool) error {
|
|
if err := p.portalDesktop.Call("org.freedesktop.portal.RemoteDesktop.NotifyPointerAxis", 0,
|
|
p.sessionHandle, map[string]dbus.Variant{"finish": dbus.MakeVariant(finish)}, float64(deltaHorizontal), float64(deltaVertical),
|
|
).Store(); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|