remote-touchpad/main.go

279 lines
8.3 KiB
Go

/*
* Copyright (c) 2018-2019 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 main
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"errors"
"flag"
"fmt"
"github.com/unrud/remote-touchpad/inputcontrol"
"github.com/unrud/remote-touchpad/terminal"
"golang.org/x/net/websocket"
"log"
mathrand "math/rand"
"net"
"net/http"
"os"
"strconv"
"strings"
"time"
"unicode/utf8"
)
const (
defaultSecretLength int = 8
authenticationRateLimit time.Duration = time.Second / 10
authenticationRateBurst int = 10
challengeLength int = 8
defaultBind string = ":0"
version string = "1.4.8"
prettyAppName string = "Remote Touchpad"
)
type config struct {
UpdateRate uint `json:"updateRate"`
ScrollSpeed float64 `json:"scrollSpeed"`
MoveSpeed float64 `json:"moveSpeed"`
MouseScrollSpeed float64 `json:"mouseScrollSpeed"`
MouseMoveSpeed float64 `json:"mouseMoveSpeed"`
}
func processCommand(controller inputcontrol.Controller, command string) error {
if len(command) == 0 {
return errors.New("empty command")
}
if command == "S" {
return controller.PointerScroll(0, 0, true)
}
if command[0] == 't' {
text := command[1:]
if !utf8.ValidString(text) {
return errors.New("invalid utf-8")
}
return controller.KeyboardText(text)
}
arguments := strings.Split(command[1:], ";")
if command[0] == 'k' && len(arguments) != 1 ||
command[0] != 'k' && len(arguments) != 2 {
return errors.New("wrong number of arguments")
}
x, err := strconv.ParseInt(arguments[0], 10, 32)
if err != nil {
return err
}
if command[0] == 'k' {
if x < 0 || x >= int64(inputcontrol.KeyLimit) {
return errors.New("unsupported key")
}
return controller.KeyboardKey(inputcontrol.Key(x))
}
y, err := strconv.ParseInt(arguments[1], 10, 32)
if err != nil {
return err
}
if command[0] == 'm' {
return controller.PointerMove(int(x), int(y))
}
if command[0] == 's' {
return controller.PointerScroll(int(x), int(y), false)
}
if command[0] == 'S' {
return controller.PointerScroll(int(x), int(y), true)
}
if command[0] == 'b' {
if x < 0 || x >= int64(inputcontrol.PointerButtonLimit) {
return errors.New("unsupported pointer button")
}
b := true
if y == 0 {
b = false
}
return controller.PointerButton(inputcontrol.PointerButton(x), b)
}
return errors.New("unsupported command")
}
type challenge struct {
message, expectedResponse string
}
func (c challenge) verify(response string) bool {
return c.expectedResponse == response
}
func authenticationChallengeGenerator(secret string, challenges chan<- challenge) {
unsecureSource := mathrand.NewSource(time.Now().UnixNano())
unsecureRand := mathrand.New(unsecureSource)
b := make([]byte, challengeLength)
for {
if _, err := unsecureRand.Read(b[:]); err != nil {
log.Fatal(err)
}
message := base64.StdEncoding.EncodeToString(b[:])
mac := hmac.New(sha256.New, []byte(message))
mac.Write([]byte(secret))
challenges <- challenge{
message: message,
expectedResponse: base64.StdEncoding.EncodeToString(mac.Sum(nil)),
}
time.Sleep(authenticationRateLimit)
}
}
func secureRandBase64(length int) string {
b := make([]byte, length)
if _, err := rand.Read(b[:]); err != nil {
log.Fatal(err)
}
return base64.StdEncoding.EncodeToString(b[:])
}
func main() {
terminal.SetTitle(prettyAppName)
var bind, certFile, keyFile, secret string
var showVersion, savePlaintextRestoreToken bool
var config config
flag.BoolVar(&showVersion, "version", false, "show program's version number and exit")
flag.StringVar(&bind, "bind", defaultBind, "bind server to [HOSTNAME]:PORT")
flag.StringVar(&secret, "secret", "", "shared secret for client authentication")
flag.StringVar(&certFile, "cert", "", "file containing TLS certificate")
flag.StringVar(&keyFile, "key", "", "file containing TLS private key")
flag.UintVar(&config.UpdateRate, "update-rate", 30, "number of updates per second")
flag.Float64Var(&config.MoveSpeed, "move-speed", 1, "move speed multiplier")
flag.Float64Var(&config.ScrollSpeed, "scroll-speed", 1, "scroll speed multiplier")
flag.Float64Var(&config.MouseMoveSpeed, "mouse-move-speed", 1, "mouse move speed multiplier")
flag.Float64Var(&config.MouseScrollSpeed, "mouse-scroll-speed", 1, "mouse scroll speed multiplier")
flag.BoolVar(&savePlaintextRestoreToken, "save-plaintext-restore-token", false, "save a restore token in plaintext in order to avoid the confirmation dialogue that appears with some implementations of the portals backend on wayland on linux from appearing on the next run of remote-touchpad")
flag.Parse()
if showVersion {
fmt.Println(version)
return
}
if certFile != "" && keyFile == "" {
log.Fatal("TLS private key file missing")
}
if certFile == "" && keyFile != "" {
log.Fatal("TLS certificate file missing")
}
tls := certFile != "" && keyFile != ""
if secret == "" {
secret = secureRandBase64(defaultSecretLength)
}
if len(inputcontrol.Controllers) == 0 {
log.Fatal("compiled without controller")
}
var controller inputcontrol.Controller
var controllerName string
var platformErrs []error
for _, controllerInfo := range inputcontrol.Controllers {
controllerName = controllerInfo.Name
var err error
controller, err = controllerInfo.Init(savePlaintextRestoreToken)
if err == nil {
break
} else {
var unsupportedErr *inputcontrol.UnsupportedPlatformError
wrappedErr := fmt.Errorf("%v controller: %w", controllerName, err)
if errors.As(err, &unsupportedErr) {
platformErrs = append(platformErrs, wrappedErr)
} else {
log.Fatal(wrappedErr)
}
}
}
if controller == nil {
log.Fatal(fmt.Errorf("unsupported platform:\n%w", errors.Join(platformErrs...)))
}
defer controller.Close()
authenticationChallenges := make(chan challenge, authenticationRateBurst)
go authenticationChallengeGenerator(secret, authenticationChallenges)
listener, err := net.Listen("tcp", bind)
if err != nil {
log.Fatal(err)
}
addr := listener.Addr().(*net.TCPAddr)
host := ""
bindHost, _, err := net.SplitHostPort(bind)
if err != nil {
log.Fatal(err)
}
for _, b := range addr.IP {
if b != 0 {
host = bindHost
break
}
}
if host == "" {
host = findDefaultHost()
}
port := addr.Port
mux := http.NewServeMux()
mux.Handle("/", http.FileServer(http.FS(webdataFS)))
mux.Handle("/ws", websocket.Handler(func(ws *websocket.Conn) {
var message string
challenge := <-authenticationChallenges
websocket.Message.Send(ws, challenge.message)
if err := websocket.Message.Receive(ws, &message); err != nil {
return
}
if !challenge.verify(message) {
return
}
websocket.JSON.Send(ws, config)
for {
if err := websocket.Message.Receive(ws, &message); err != nil {
return
}
if err := processCommand(controller, message); err != nil {
log.Print(fmt.Errorf("%s controller: %w", controllerName, err))
return
}
}
}))
domain := host
if port != 80 && !tls || port != 443 && tls {
domain = net.JoinHostPort(host, strconv.Itoa(port))
}
scheme := "http"
if tls {
scheme = "https"
}
url := fmt.Sprintf("%s://%s/#%s", scheme, domain, secret)
fmt.Println(url)
if qrCode, err := terminal.GenerateQRCode(url, terminal.SupportsColor(os.Stdout.Fd())); err == nil {
fmt.Print(qrCode)
} else {
log.Printf("QR code error: %v", err)
}
if !tls {
fmt.Println("▌ WARNING: TLS is not enabled ▐")
fmt.Println("▌Don't use in an untrusted network!▐")
}
if tls {
err = http.ServeTLS(listener, mux, certFile, keyFile)
} else {
err = http.Serve(listener, mux)
}
log.Fatal(err)
}