/* * Copyright (c) 2018-2019 Unrud * * 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 . */ 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) }