279 lines
8.3 KiB
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)
|
|
}
|