/* * 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" "log" mathrand "math/rand" "net" "net/http" "os" "strconv" "strings" "time" "unicode/utf8" "github.com/unrud/remote-touchpad/inputcontrol" "github.com/unrud/remote-touchpad/terminal" "golang.org/x/net/websocket" ) const ( defaultSecretLength int = 8 authenticationRateLimit time.Duration = time.Second / 10 authenticationRateBurst int = 10 challengeLength int = 8 defaultBind string = ":0" version string = "1.5.4" 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 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.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() 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) }