From a58227910beb35c28d51b6c4b5886367148089a8 Mon Sep 17 00:00:00 2001 From: Vladimir Smirnov Date: Wed, 9 Oct 2024 20:54:47 +0200 Subject: [PATCH] Slightly refactor nanokvm code * Add ability to properly configure logrus (e.x. specify output file) * Make the code slightly more ideomatic (sort imports, etc) * Replace some of the shell calls with equivalent native code * Fix update process * Improve error handling (handle 2 previously unhandled errors) * Add locking for HID operations and make a HID-operations struct a singleton --- server/config/config.go | 111 +++++++++++----- server/config/types.go | 45 ++++++- server/go.mod | 2 +- server/logger/formatter.go | 3 +- server/logger/logger.go | 35 +++++- server/main.go | 4 +- server/middleware/jwt.go | 10 +- server/proto/request.go | 39 +++--- server/proto/response.go | 9 +- server/router/application.go | 6 +- server/router/auth.go | 3 +- server/router/hid.go | 3 +- server/router/network.go | 3 +- server/router/router.go | 5 +- server/router/storage.go | 3 +- server/router/stream.go | 3 +- server/router/vm.go | 8 +- server/router/ws.go | 3 +- server/service/application/application.go | 146 +++++++++++++++------- server/service/application/lib.go | 61 +++++---- server/service/application/service.go | 18 +-- server/service/auth/account.go | 14 ++- server/service/auth/auth.go | 5 +- server/service/hid/hid.go | 36 ++++++ server/service/hid/keyboard.go | 20 +-- server/service/hid/mouse.go | 49 ++++---- server/service/hid/operation.go | 48 ++++--- server/service/hid/paste.go | 16 ++- server/service/hid/reset.go | 62 +++++++-- server/service/hid/service.go | 8 +- server/service/network/tailscale.go | 109 ++++++++++------ server/service/network/wol.go | 18 +-- server/service/storage/image.go | 25 ++-- server/service/stream/frame-detect.go | 32 ++--- server/service/stream/mjpeg.go | 13 +- server/service/vm/gpio.go | 73 ++++------- server/service/vm/info.go | 8 +- server/service/vm/screen.go | 8 +- server/service/vm/script.go | 22 ++-- server/service/vm/service.go | 12 +- server/service/vm/terminal.go | 22 ++-- server/service/vm/virtual-device.go | 38 +++--- server/service/ws/watch.go | 3 +- server/service/ws/ws.go | 20 +-- server/utils/chmod.go | 23 ++++ server/utils/encrypt.go | 3 +- server/utils/http.go | 30 +++-- server/utils/move-file.go | 78 ++++++++++++ server/utils/unzip.go | 61 +++++++++ web/src/api/application.ts | 2 +- 50 files changed, 962 insertions(+), 416 deletions(-) create mode 100644 server/service/hid/hid.go create mode 100644 server/utils/chmod.go create mode 100644 server/utils/move-file.go create mode 100644 server/utils/unzip.go diff --git a/server/config/config.go b/server/config/config.go index d15aff9..02acdce 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -1,30 +1,54 @@ package config import ( - "bytes" "crypto/rand" "encoding/base64" "errors" "fmt" - "github.com/spf13/viper" + "log" "os" + "strings" "sync" "time" + + "github.com/spf13/viper" + "gopkg.in/yaml.v3" +) + +const ( + hwVersionFile = "/etc/kvm/hw" + + gpioPower = "/sys/class/gpio/gpio503/value" + gpioPowerLED = "/sys/class/gpio/gpio504/value" + + gpioResetAlpha = "/sys/class/gpio/gpio507/value" + gpioHDDLedAlpha = "/sys/class/gpio/gpio505/value" + + gpioResetBeta = "/sys/class/gpio/gpio505/value" ) var ( config Config once sync.Once -) -var defaultConfig = []byte(`proto: http -port: - http: 80 - https: 443 -cert: - crt: server.crt - key: server.key -`) + defaultConfig = &Config{ + Protocol: "http", + Port: Port{ + Http: 80, + Https: 443, + }, + Cert: Cert{ + Crt: "server.crt", + Key: "server.key", + }, + Logger: LoggerConfig{ + Level: "info", + File: "stdout", + }, + Authentication: "enable", + SecretKey: generateRandomString(), + } +) func GetInstance() *Config { once.Do(read) @@ -40,48 +64,68 @@ func read() { if err := viper.ReadInConfig(); err != nil { if errors.As(err, &viper.ConfigFileNotFoundError{}) { create() - fmt.Printf("File /etc/kvm/server.yaml not exists. Use default configuration.\n") + log.Println("File /etc/kvm/server.yaml not found. Create a new one with default configuration.") } else { - fmt.Printf("Read file /etc/kvm/server.yaml failed. Use default configuration.\n") + log.Println("Failed to read config file /etc/kvm/server.yaml. Using default configuration.") } } if err := viper.Unmarshal(&config); err != nil { - panic(fmt.Sprintf("Can't read configuration file /etc/kvm/nanokvm.yaml.\n%s", err)) + log.Fatalf("Failed to parse configuration file /etc/kvm/server.yaml: %s", err) } validate() if config.SecretKey == "" { - config.SecretKey = generateRandomString() + config.SecretKey = defaultConfig.SecretKey } if config.Authentication == "disable" { - fmt.Println("NOTICE: Authentication is disabled! Please ensure your service is secure!") + log.Println("NOTICE: Authentication is disabled! Please ensure your service is secure!") } - fmt.Printf("load config success\n") + log.Println("config loaded successfully") + config.HW.Version = getHwVersion() + config.HW.GPIOPower = gpioPower + config.HW.GPIOPowerLED = gpioPowerLED + switch config.HW.Version { + case HWVersionAlpha: + config.HW.GPIOHDDLed = gpioHDDLedAlpha + config.HW.GPIOReset = gpioResetAlpha + case HWVersionBeta: + config.HW.GPIOReset = gpioResetBeta + default: + log.Fatalf("Unsupported hardware version: %s", config.HW.Version) + } } func create() { - _ = os.MkdirAll("/etc/kvm", 0644) + _ = os.MkdirAll("/etc/kvm", 0o644) - file, err := os.OpenFile("/etc/kvm/server.yaml", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) + file, err := os.OpenFile("/etc/kvm/server.yaml", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) if err != nil { - fmt.Printf("open config failed: %s\n", err) + log.Printf("open config failed: %s", err) return } - defer file.Close() + defer func() { + _ = file.Close() + }() - _, err = file.Write(defaultConfig) + data, err := yaml.Marshal(defaultConfig) if err != nil { - fmt.Printf("save config failed: %s\n", err) + log.Printf("failed to marshal default config: %s", err) + return + } + + _, err = file.Write(data) + if err != nil { + log.Printf("failed to save config: %s", err) return } err = file.Sync() if err != nil { - fmt.Printf("sync config failed: %s\n", err) + log.Printf("failed to sync config: %s", err) return } } @@ -93,19 +137,14 @@ func validate() { _ = os.Remove("/etc/kvm/server.yaml") - if err := viper.ReadConfig(bytes.NewBuffer(defaultConfig)); err != nil { - panic("load default config failed") - } - if err := viper.Unmarshal(&config); err != nil { - panic("Can't read configuration file /etc/kvm/server.yaml.") + log.Fatalf("Failed to read configuration file /etc/kvm/server.yaml: %v", err) } } func generateRandomString() string { b := make([]byte, 64) _, err := rand.Read(b) - if err != nil { currentTime := time.Now().UnixNano() timeString := fmt.Sprintf("%d", currentTime) @@ -114,3 +153,15 @@ func generateRandomString() string { return base64.URLEncoding.EncodeToString(b) } + +func getHwVersion() HWVersion { + content, err := os.ReadFile(hwVersionFile) + if err == nil { + version := strings.ReplaceAll(string(content), "\n", "") + if version == "beta" { + return HWVersionBeta + } + } + + return HWVersionAlpha +} diff --git a/server/config/types.go b/server/config/types.go index aef2153..9a5422b 100644 --- a/server/config/types.go +++ b/server/config/types.go @@ -1,12 +1,45 @@ package config +type HWVersion int + +const ( + HWVersionAlpha HWVersion = iota + HWVersionBeta +) + +func (h HWVersion) String() string { + switch h { + case HWVersionAlpha: + return "Alpha" + case HWVersionBeta: + return "Beta" + default: + return "Unknown" + } +} + type Config struct { - Protocol string `yaml:"proto"` - Port Port `yaml:"port"` - Cert Cert `yaml:"cert"` - Log string `yaml:"logger"` - Authentication string `yaml:"authentication"` - SecretKey string `yaml:"secretKey"` + Protocol string `yaml:"proto"` + Port Port `yaml:"port"` + Cert Cert `yaml:"cert"` + Logger LoggerConfig `yaml:"logger"` + Authentication string `yaml:"authentication"` + SecretKey string `yaml:"secretKey"` + + HW HW `yaml:"-"` +} + +type HW struct { + Version HWVersion `yaml:"-"` + GPIOReset string `yaml:"-"` + GPIOPower string `yaml:"-"` + GPIOPowerLED string `yaml:"-"` + GPIOHDDLed string `yaml:"-"` +} + +type LoggerConfig struct { + Level string `yaml:"level"` + File string `yaml:"file"` } type Port struct { diff --git a/server/go.mod b/server/go.mod index f1b2924..7c0de76 100644 --- a/server/go.mod +++ b/server/go.mod @@ -14,6 +14,7 @@ require ( github.com/spf13/viper v1.19.0 github.com/unrolled/secure v1.15.0 golang.org/x/crypto v0.23.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -56,5 +57,4 @@ require ( golang.org/x/text v0.15.0 // indirect google.golang.org/protobuf v1.34.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/server/logger/formatter.go b/server/logger/formatter.go index 2ea8572..a995d1a 100644 --- a/server/logger/formatter.go +++ b/server/logger/formatter.go @@ -3,8 +3,9 @@ package logger import ( "bytes" "fmt" - "github.com/sirupsen/logrus" "path/filepath" + + "github.com/sirupsen/logrus" ) type formatter struct{} diff --git a/server/logger/logger.go b/server/logger/logger.go index 317cfb5..8edea27 100644 --- a/server/logger/logger.go +++ b/server/logger/logger.go @@ -1,21 +1,48 @@ package logger import ( - "NanoKVM-Server/config" - "github.com/sirupsen/logrus" "os" + "path/filepath" + + "NanoKVM-Server/config" + + "github.com/sirupsen/logrus" ) +func openLogFile(filename string) (*os.File, error) { + absPath, err := filepath.Abs(filename) + if err != nil { + return nil, err + } + + file, err := os.OpenFile(absPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) + if err != nil { + return nil, err + } + + return file, nil +} + func Init() { conf := config.GetInstance() - level, err := logrus.ParseLevel(conf.Log) + level, err := logrus.ParseLevel(conf.Logger.Level) if err != nil { level = logrus.ErrorLevel } logrus.SetLevel(level) - logrus.SetOutput(os.Stdout) + if conf.Logger.File == "" || conf.Logger.File == "stdout" { + logrus.SetOutput(os.Stdout) + } else { + fh, err := openLogFile(conf.Logger.File) + if err != nil { + logrus.Error("open log file failed:", err) + logrus.SetOutput(os.Stdout) + } else { + logrus.SetOutput(fh) + } + } logrus.SetReportCaller(true) logrus.SetFormatter(&formatter{}) diff --git a/server/main.go b/server/main.go index 105c76e..10e6fc0 100644 --- a/server/main.go +++ b/server/main.go @@ -1,11 +1,13 @@ package main import ( + "fmt" + "NanoKVM-Server/config" "NanoKVM-Server/logger" "NanoKVM-Server/middleware" "NanoKVM-Server/router" - "fmt" + "github.com/gin-gonic/gin" cors "github.com/rs/cors/wrapper/gin" ) diff --git a/server/middleware/jwt.go b/server/middleware/jwt.go index f95358b..61d7616 100644 --- a/server/middleware/jwt.go +++ b/server/middleware/jwt.go @@ -1,12 +1,14 @@ package middleware import ( - "NanoKVM-Server/config" + "net/http" + "time" + "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" log "github.com/sirupsen/logrus" - "net/http" - "time" + + "NanoKVM-Server/config" ) type Token struct { @@ -38,7 +40,6 @@ func CheckToken() gin.HandlerFunc { c.JSON(http.StatusUnauthorized, "unauthorized") c.Abort() - return } } @@ -64,7 +65,6 @@ func ParseJWT(jwtToken string) (*Token, error) { secretKey := conf.SecretKey return []byte(secretKey), nil }) - if err != nil { log.Debugf("parse jwt error: %s", err) return nil, err diff --git a/server/proto/request.go b/server/proto/request.go index 1759745..f0f3b39 100644 --- a/server/proto/request.go +++ b/server/proto/request.go @@ -1,54 +1,49 @@ package proto import ( + "os" + "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" log "github.com/sirupsen/logrus" - "os" ) var env = os.Getenv(gin.EnvGinMode) -// ValidateRequest Verification request parameters -func ValidateRequest(req interface{}) (err error) { +// ValidateRequest Validates request parameters. +func ValidateRequest(req interface{}) error { validate := validator.New() - if err = validate.Struct(req); err != nil { + if err := validate.Struct(req); err != nil { log.Errorf("validate request failed, err: %s", err) - return + return err } if env == "" || env == "debug" { log.Debugf("request: %+v\n", req) } - return + return nil } -// ParseQueryRequest Parsing GET requests -func ParseQueryRequest(c *gin.Context, req interface{}) (err error) { +// ParseQueryRequest Validates GET requests. +func ParseQueryRequest(c *gin.Context, req interface{}) error { + var err error if err = c.ShouldBindQuery(req); err != nil { log.Errorf("parse request failed, err: %s", err) - return + return err } - if err = ValidateRequest(req); err != nil { - return - } - - return + return ValidateRequest(req) } -// ParseFormRequest Parsing POST Requests -func ParseFormRequest(c *gin.Context, req interface{}) (err error) { +// ParseFormRequest Validates POST Requests. +func ParseFormRequest(c *gin.Context, req interface{}) error { + var err error if err = c.ShouldBind(req); err != nil { log.Errorf("parse request failed, err: %s", err) - return + return err } - if err = ValidateRequest(req); err != nil { - return - } - - return + return ValidateRequest(req) } diff --git a/server/proto/response.go b/server/proto/response.go index c373844..1b64ec8 100644 --- a/server/proto/response.go +++ b/server/proto/response.go @@ -1,8 +1,9 @@ package proto import ( - "github.com/gin-gonic/gin" "net/http" + + "github.com/gin-gonic/gin" ) type Response struct { @@ -26,14 +27,14 @@ func (r *Response) Err(code int, msg string) { r.Msg = msg } -// OkRsp Successful response without data +// OkRsp Successful response without data. func (r *Response) OkRsp(c *gin.Context) { r.Ok() c.JSON(http.StatusOK, r) } -// OkRspWithData Successful response with data +// OkRspWithData Successful response with data. func (r *Response) OkRspWithData(c *gin.Context, data interface{}) { r.Ok() r.Data = data @@ -41,7 +42,7 @@ func (r *Response) OkRspWithData(c *gin.Context, data interface{}) { c.JSON(http.StatusOK, r) } -// ErrRsp Failed response +// ErrRsp Failed response. func (r *Response) ErrRsp(c *gin.Context, code int, msg string) { r.Err(code, msg) diff --git a/server/router/application.go b/server/router/application.go index cd3b597..14007e7 100644 --- a/server/router/application.go +++ b/server/router/application.go @@ -1,12 +1,15 @@ package router import ( + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" + "NanoKVM-Server/middleware" "NanoKVM-Server/service/application" - "github.com/gin-gonic/gin" ) func applicationRouter(r *gin.Engine) { + log.Debugf("application router init") service := application.NewService() api := r.Group("/api").Use(middleware.CheckToken()) @@ -14,4 +17,5 @@ func applicationRouter(r *gin.Engine) { api.POST("/application/update", service.Update) // update application api.GET("/application/lib", service.GetLib) // check if lib exists api.POST("/application/lib", service.UpdateLib) // update lib + log.Debugf("application router init done") } diff --git a/server/router/auth.go b/server/router/auth.go index f99bdc3..42585d3 100644 --- a/server/router/auth.go +++ b/server/router/auth.go @@ -1,9 +1,10 @@ package router import ( + "github.com/gin-gonic/gin" + "NanoKVM-Server/middleware" "NanoKVM-Server/service/auth" - "github.com/gin-gonic/gin" ) func authRouter(r *gin.Engine) { diff --git a/server/router/hid.go b/server/router/hid.go index b0ffac7..ba4bb32 100644 --- a/server/router/hid.go +++ b/server/router/hid.go @@ -1,9 +1,10 @@ package router import ( + "github.com/gin-gonic/gin" + "NanoKVM-Server/middleware" "NanoKVM-Server/service/hid" - "github.com/gin-gonic/gin" ) func hidRouter(r *gin.Engine) { diff --git a/server/router/network.go b/server/router/network.go index 8a1e0a5..ef5d836 100644 --- a/server/router/network.go +++ b/server/router/network.go @@ -1,9 +1,10 @@ package router import ( + "github.com/gin-gonic/gin" + "NanoKVM-Server/middleware" "NanoKVM-Server/service/network" - "github.com/gin-gonic/gin" ) func networkRouter(r *gin.Engine) { diff --git a/server/router/router.go b/server/router/router.go index 4f175b4..c61e171 100644 --- a/server/router/router.go +++ b/server/router/router.go @@ -2,10 +2,11 @@ package router import ( "fmt" - "github.com/gin-gonic/contrib/static" - "github.com/gin-gonic/gin" "os" "path/filepath" + + "github.com/gin-gonic/contrib/static" + "github.com/gin-gonic/gin" ) func Init(r *gin.Engine) { diff --git a/server/router/storage.go b/server/router/storage.go index 1d072a8..f7fdf89 100644 --- a/server/router/storage.go +++ b/server/router/storage.go @@ -1,9 +1,10 @@ package router import ( + "github.com/gin-gonic/gin" + "NanoKVM-Server/middleware" "NanoKVM-Server/service/storage" - "github.com/gin-gonic/gin" ) func storageRouter(r *gin.Engine) { diff --git a/server/router/stream.go b/server/router/stream.go index 4a1d7e4..0589cfe 100644 --- a/server/router/stream.go +++ b/server/router/stream.go @@ -1,9 +1,10 @@ package router import ( + "github.com/gin-gonic/gin" + "NanoKVM-Server/middleware" "NanoKVM-Server/service/stream" - "github.com/gin-gonic/gin" ) func streamRouter(r *gin.Engine) { diff --git a/server/router/vm.go b/server/router/vm.go index f287acf..7efc101 100644 --- a/server/router/vm.go +++ b/server/router/vm.go @@ -1,9 +1,10 @@ package router import ( + "github.com/gin-gonic/gin" + "NanoKVM-Server/middleware" "NanoKVM-Server/service/vm" - "github.com/gin-gonic/gin" ) func vmRouter(r *gin.Engine) { @@ -12,9 +13,8 @@ func vmRouter(r *gin.Engine) { api.GET("/vm/info", service.GetInfo) // get device information - api.POST("/vm/gpio", service.SetGpio) // update gpio - api.GET("/vm/gpio", service.GetGpio) // get gpio - + api.POST("/vm/gpio", service.SetGpio) // update gpio + api.GET("/vm/gpio", service.GetGpio) // get gpio api.POST("/vm/screen", service.SetScreen) // update screen api.GET("/vm/terminal", service.Terminal) // web terminal diff --git a/server/router/ws.go b/server/router/ws.go index 4640c29..4946724 100644 --- a/server/router/ws.go +++ b/server/router/ws.go @@ -1,9 +1,10 @@ package router import ( + "github.com/gin-gonic/gin" + "NanoKVM-Server/middleware" "NanoKVM-Server/service/ws" - "github.com/gin-gonic/gin" ) func wsRouter(r *gin.Engine) { diff --git a/server/service/application/application.go b/server/service/application/application.go index b729867..fded7b3 100644 --- a/server/service/application/application.go +++ b/server/service/application/application.go @@ -1,45 +1,63 @@ package application import ( - "NanoKVM-Server/proto" - "NanoKVM-Server/utils" "fmt" - "github.com/gin-gonic/gin" - log "github.com/sirupsen/logrus" "io" "net/http" "os" "os/exec" "strings" "time" + + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" + + "NanoKVM-Server/proto" + "NanoKVM-Server/utils" +) + +const ( + maxTries = 3 ) func (s *Service) GetVersion(c *gin.Context) { var rsp proto.Response + log.Debugf("get version api triggered") // current version currentVersion := "1.0.0" - content, err := os.ReadFile(VersionFile) + content, err := os.ReadFile(versionFile) if err == nil { currentVersion = strings.ReplaceAll(string(content), "\n", "") } + log.Debugf("current version: %s", currentVersion) // latest version - url := fmt.Sprintf("%s?now=%d", VersionURL, time.Now().Unix()) + url := fmt.Sprintf("%s?now=%d", versionURL, time.Now().Unix()) resp, err := http.Get(url) if err != nil { + log.Debugf("get latest version failed: %v", err) rsp.ErrRsp(c, -2, "Unable to access sipeed.com. Please check your network.") return } - defer resp.Body.Close() + defer func() { + _ = resp.Body.Close() + }() - if resp.StatusCode != http.StatusOK { - rsp.ErrRsp(c, -3, "get version failed") + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Errorf("read body failed: %v", err) + rsp.ErrRsp(c, -4, "read body failed") return } - body, err := io.ReadAll(resp.Body) - latestVersion := strings.Replace(string(body), "\n", "", -1) + if resp.StatusCode != http.StatusOK { + log.Errorf("server responded with status code: %d", resp.StatusCode) + rsp.ErrRsp(c, -3, fmt.Sprintf("get version failed, server returns status code: %d, body: %s", resp.StatusCode, string(body))) + return + } + + latestVersion := strings.ReplaceAll(string(body), "\n", "") rsp.OkRspWithData(c, &proto.GetVersionRsp{ Current: currentVersion, @@ -49,69 +67,109 @@ func (s *Service) GetVersion(c *gin.Context) { func (s *Service) Update(c *gin.Context) { var rsp proto.Response + log.Debugf("update application api triggered") if err := updateApp(); err != nil { - rsp.ErrRsp(c, -1, "update failed") + log.Debugf("update failed: %s", err) + rsp.ErrRsp(c, -1, fmt.Sprintf("update failed: %s", err)) return } rsp.OkRsp(c) log.Debugf("update application success") + // Sleep for a second before restarting the device + time.Sleep(1 * time.Second) + _ = exec.Command("sh", "-c", "/etc/init.d/S95nanokvm restart").Run() } func updateApp() error { - cleanCmd := exec.Command("sh", "-c", fmt.Sprintf("rm -rf %s", Temporary)) - _ = cleanCmd.Run() - _ = os.MkdirAll(Temporary, 0755) - defer cleanCmd.Run() + log.Debugf("update application") + _ = os.RemoveAll(temporary) + _ = os.MkdirAll(temporary, 0o755) + defer func() { + _ = os.RemoveAll(temporary) + }() if err := downloadLib(); err != nil { + log.Errorf("download lib failed: %s", err) return err } if err := downloadApp(); err != nil { + log.Errorf("download app failed: %s", err) return err } - commands := []string{ - fmt.Sprintf("mv -f %s/%s %s/latest/kvm_system/dl_lib/", Temporary, LibName, Temporary), // move lib - fmt.Sprintf("rm -rf %s && mv %s %s", Backup, Workspace, Backup), // backup old version - fmt.Sprintf("rm -rf %s && mv %s/latest %s", Workspace, Temporary, Workspace), // update - fmt.Sprintf("chmod -R 755 %s", Workspace), // modify permission + err := utils.MoveFile(temporary+"/"+libName, temporary+"/latest/kvm_system/dl_lib/"+libName) // move lib + if err != nil { + log.Errorf("rename lib failed: %s", err) + return err } - for _, command := range commands { - err := exec.Command("sh", "-c", command).Run() - if err != nil { - return err - } + // backup old version + err = os.RemoveAll(backup) + if err != nil { + log.Errorf("remove backup failed: %s", err) + return err + } + + err = utils.MoveFilesRecursively(workspace, backup) + if err != nil { + log.Errorf("backuping old libraries failed: %s", err) + return err + } + + // update + err = utils.MoveFilesRecursively(temporary+"/latest", workspace) + if err != nil { + log.Errorf("failed to move update back in place: %s", err) + return err + } + + // modify permissions + err = utils.ChmodRecursively(workspace, 0o755) + if err != nil { + log.Errorf("chmod failed: %s", err) + return err } return nil } func downloadApp() error { - url := fmt.Sprintf("%s?now=%d", ApplicationURL, time.Now().Unix()) - req, err := http.NewRequest("GET", url, nil) - if err != nil { - log.Errorf("new request err: %s", err) - return err - } + var err error + log.Debugf("downloading latest application...") + url := fmt.Sprintf("%s?now=%d", applicationURL, time.Now().Unix()) + for i := range maxTries { + log.Debugf("attempt #%d/%d", i+1, maxTries) + if i > 0 { + time.Sleep(time.Second * 3) // wait for 3 seconds before retrying the download attempt + } - zipFile := fmt.Sprintf("%s/latest.zip", Temporary) - err = utils.Download(req, zipFile) - if err != nil { - return err - } + var req *http.Request + req, err = http.NewRequest("GET", url, nil) + if err != nil { + log.Errorf("new request err: %s", err) + continue + } - command := fmt.Sprintf("unzip %s -d %s", zipFile, Temporary) - err = exec.Command("sh", "-c", command).Run() - if err != nil { - log.Errorf("unzip app failed: %s", err) - return err - } + zipFile := temporary + "/latest.zip" + log.Debugf("update will be saved to: %s", zipFile) + err = utils.Download(req, zipFile) + if err != nil { + log.Errorf("downloading latest application failed, try again...") + continue + } - return nil + err = utils.Unzip(zipFile, temporary) + if err != nil { + log.Errorf("unzip app failed: %s", err) + continue + } + + return nil + } + return err } diff --git a/server/service/application/lib.go b/server/service/application/lib.go index fb93bd1..58a24a3 100644 --- a/server/service/application/lib.go +++ b/server/service/application/lib.go @@ -1,16 +1,19 @@ package application import ( - "NanoKVM-Server/proto" - "NanoKVM-Server/utils" "errors" "fmt" - "github.com/gin-gonic/gin" - log "github.com/sirupsen/logrus" "net/http" "os" "os/exec" "strings" + "time" + + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" + + "NanoKVM-Server/proto" + "NanoKVM-Server/utils" ) func (s *Service) GetLib(c *gin.Context) { @@ -39,18 +42,17 @@ func (s *Service) UpdateLib(c *gin.Context) { return } - cleanCmd := exec.Command("sh", "-c", fmt.Sprintf("rm -rf %s", Temporary)) - _ = cleanCmd.Run() - _ = os.MkdirAll(Temporary, 0755) - defer cleanCmd.Run() + _ = os.MkdirAll(temporary, 0o755) + defer func() { + _ = os.RemoveAll(temporary) + }() if err := downloadLib(); err != nil { rsp.ErrRsp(c, -1, "download lib failed") return } - command := fmt.Sprintf("mv -f %s/%s %s/", Temporary, LibName, LibDir) // update lib - err := exec.Command("sh", "-c", command).Run() + err := utils.MoveFile(temporary+"/"+libName, libDir+"/") // update lib if err != nil { rsp.ErrRsp(c, -2, "update lib failed") return @@ -63,7 +65,7 @@ func (s *Service) UpdateLib(c *gin.Context) { } func isLibExist() (bool, error) { - libPath := fmt.Sprintf("%s/%s", LibDir, LibName) + libPath := fmt.Sprintf("%s/%s", libDir, libName) _, err := os.Stat(libPath) if err == nil { @@ -78,22 +80,37 @@ func isLibExist() (bool, error) { } func downloadLib() error { + log.Debugf("downloading libs...") content, err := os.ReadFile("/device_key") if err != nil { - log.Errorf("read devcie key err: %s", err) + log.Errorf("error reading device key: %s", err) return err } deviceKey := strings.ReplaceAll(string(content), "\n", "") - url := fmt.Sprintf("%s?uid=%s", LibURL, deviceKey) - req, err := http.NewRequest("GET", url, nil) - if err != nil { - log.Errorf("new request err: %s", err) - return err + for i := range maxTries { + log.Debugf("attempt #%d/%d", i+1, maxTries) + if i > 0 { + time.Sleep(time.Second * 3) // wait for 3 seconds before retrying the download attempt + } + + var req *http.Request + url := fmt.Sprintf("%s?uid=%s", libURL, deviceKey) + req, err = http.NewRequest("GET", url, nil) + if err != nil { + log.Errorf("error creating new request: %s", err) + continue + } + req.Header.Set("token", "MaixVision2024") + + target := fmt.Sprintf("%s/%s", temporary, libName) + + err = utils.Download(req, target) + if err != nil { + log.Errorf("downloading lib failed: %s", err) + continue + } + return nil } - req.Header.Set("token", "MaixVision2024") - - target := fmt.Sprintf("%s/%s", Temporary, LibName) - - return utils.Download(req, target) + return err } diff --git a/server/service/application/service.go b/server/service/application/service.go index dea03c0..46a0376 100644 --- a/server/service/application/service.go +++ b/server/service/application/service.go @@ -1,16 +1,16 @@ package application const ( - VersionURL = "https://cdn.sipeed.com/nanokvm/latest" - ApplicationURL = "https://cdn.sipeed.com/nanokvm/latest.zip" - LibURL = "https://maixvision.sipeed.com/api/v1/nanokvm/encryption" + versionURL = "https://cdn.sipeed.com/nanokvm/latest" + applicationURL = "https://cdn.sipeed.com/nanokvm/latest.zip" + libURL = "https://maixvision.sipeed.com/api/v1/nanokvm/encryption" - Temporary = "/tmp/kvmcache" - Workspace = "/kvmapp" - Backup = "/root/old" - LibDir = "/kvmapp/kvm_system/dl_lib" - LibName = "libmaixcam_lib.so" - VersionFile = "/kvmapp/version" + temporary = "/tmp/kvmcache" + workspace = "/kvmapp" + backup = "/root/old" + libDir = "/kvmapp/kvm_system/dl_lib" + libName = "libmaixcam_lib.so" + versionFile = "/kvmapp/version" ) type Service struct{} diff --git a/server/service/auth/account.go b/server/service/auth/account.go index 31c97f9..6969cbb 100644 --- a/server/service/auth/account.go +++ b/server/service/auth/account.go @@ -1,12 +1,14 @@ package auth import ( - "NanoKVM-Server/utils" "encoding/json" "errors" - log "github.com/sirupsen/logrus" "os" "path/filepath" + + log "github.com/sirupsen/logrus" + + "NanoKVM-Server/utils" ) const AccountFile = "/etc/kvm/pwd" @@ -56,14 +58,18 @@ func setAccount(username string, password string) error { Username: username, Password: password, }) + if err != nil { + log.Errorf("failed to marshal account information to json: %s", err) + return err + } - err = os.MkdirAll(filepath.Dir(AccountFile), 0644) + err = os.MkdirAll(filepath.Dir(AccountFile), 0o644) if err != nil { log.Errorf("create directory %s failed: %s", AccountFile, err) return err } - err = os.WriteFile(AccountFile, account, 0644) + err = os.WriteFile(AccountFile, account, 0o644) if err != nil { log.Errorf("write password failed: %s", err) return err diff --git a/server/service/auth/auth.go b/server/service/auth/auth.go index d40a404..afe84ba 100644 --- a/server/service/auth/auth.go +++ b/server/service/auth/auth.go @@ -1,12 +1,13 @@ package auth import ( + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" + "NanoKVM-Server/config" "NanoKVM-Server/middleware" "NanoKVM-Server/proto" "NanoKVM-Server/utils" - "github.com/gin-gonic/gin" - log "github.com/sirupsen/logrus" ) func (s *Service) Login(c *gin.Context) { diff --git a/server/service/hid/hid.go b/server/service/hid/hid.go new file mode 100644 index 0000000..a580101 --- /dev/null +++ b/server/service/hid/hid.go @@ -0,0 +1,36 @@ +package hid + +import ( + "os" + "sync" +) + +var ( + hid *Hid + hidOnce sync.Once +) + +type Hid struct { + g0 *os.File + g1 *os.File + g2 *os.File + kbMutex sync.Mutex + mouseMutex sync.Mutex +} + +func (h *Hid) Lock() { + h.kbMutex.Lock() + h.mouseMutex.Lock() +} + +func (h *Hid) Unlock() { + h.kbMutex.Unlock() + h.mouseMutex.Unlock() +} + +func GetHid() *Hid { + hidOnce.Do(func() { + hid = &Hid{} + }) + return hid +} diff --git a/server/service/hid/keyboard.go b/server/service/hid/keyboard.go index ed54059..40a377a 100644 --- a/server/service/hid/keyboard.go +++ b/server/service/hid/keyboard.go @@ -1,12 +1,14 @@ package hid -func Keyboard(queue <-chan []int) { +func (h *Hid) Keyboard(queue <-chan []int) { for event := range queue { - writeKeyboard(event) + h.kbMutex.Lock() + h.writeKeyboard(event) + h.kbMutex.Unlock() } } -func writeKeyboard(event []int) { +func (h *Hid) writeKeyboard(event []int) { var data []byte if event[0] > 0 { @@ -17,28 +19,28 @@ func writeKeyboard(event []int) { data = []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} } - Write(Hidg0, data) + h.Write(h.g0, data) } func getModifier(event []int) byte { var modifier byte = 0x00 if event[1] == 1 { - modifier = modifier | ModifierLCtrl + modifier |= ModifierLCtrl } if event[2] == 1 { - modifier = modifier | ModifierLShift + modifier |= ModifierLShift } if event[3] == 1 { - modifier = modifier | ModifierLAlt + modifier |= ModifierLAlt } else if event[3] == 2 { - modifier = modifier | ModifierRAlt + modifier |= ModifierRAlt } if event[4] == 1 { - modifier = modifier | ModifierLGUI + modifier |= ModifierLGUI } return modifier diff --git a/server/service/hid/mouse.go b/server/service/hid/mouse.go index 4517720..c82d5c1 100644 --- a/server/service/hid/mouse.go +++ b/server/service/hid/mouse.go @@ -3,31 +3,34 @@ package hid import ( "encoding/binary" "errors" - log "github.com/sirupsen/logrus" "os" "time" + + log "github.com/sirupsen/logrus" ) -func Mouse(queue <-chan []int) { +func (h *Hid) Mouse(queue <-chan []int) { for event := range queue { + h.mouseMutex.Lock() switch event[0] { case MouseDown: - mouseDown(event) + h.mouseDown(event) case MouseUp: - mouseUp() + h.mouseUp() case MouseMoveAbsolute: - mouseMoveAbsolute(event) + h.mouseMoveAbsolute(event) case MouseMoveRelative: - mouseMoveRelative(event) + h.mouseMoveRelative(event) case MouseScroll: - scroll(event) + h.scroll(event) default: log.Debugf("invalid mouse event: %+v", event) } + h.mouseMutex.Unlock() } } -func mouseDown(event []int) { +func (h *Hid) mouseDown(event []int) { var button byte switch event[1] { @@ -43,52 +46,52 @@ func mouseDown(event []int) { } data := []byte{button, 0, 0, 0} - writeWithTimeout(Hidg1, data) + h.writeWithTimeout(h.g1, data) } -func mouseUp() { +func (h *Hid) mouseUp() { data := []byte{0, 0, 0, 0} - writeWithTimeout(Hidg1, data) + h.writeWithTimeout(h.g1, data) } -func scroll(event []int) { +func (h *Hid) scroll(event []int) { direction := 0x01 if event[3] > 0 { direction = -0x1 } data := []byte{0, 0, 0, byte(direction)} - writeWithTimeout(Hidg1, data) + h.writeWithTimeout(h.g1, data) } -func mouseMoveAbsolute(event []int) { +func (h *Hid) mouseMoveAbsolute(event []int) { x := make([]byte, 2) y := make([]byte, 2) binary.LittleEndian.PutUint16(x, uint16(event[2])) binary.LittleEndian.PutUint16(y, uint16(event[3])) data := []byte{0, x[0], x[1], y[0], y[1], 0} - writeWithTimeout(Hidg2, data) - + h.writeWithTimeout(h.g2, data) } -func mouseMoveRelative(event []int) { +func (h *Hid) mouseMoveRelative(event []int) { data := []byte{byte(event[1]), byte(event[2]), byte(event[3]), 0} - writeWithTimeout(Hidg1, data) + h.writeWithTimeout(h.g1, data) } -func writeWithTimeout(file *os.File, data []byte) { +func (h *Hid) writeWithTimeout(file *os.File, data []byte) { deadline := time.Now().Add(9 * time.Millisecond) _ = file.SetWriteDeadline(deadline) _, err := file.Write(data) if err != nil { - if errors.Is(err, os.ErrClosed) { - Open() + switch { + case errors.Is(err, os.ErrClosed): log.Debugf("hid already closed, reopen it...") - } else if errors.Is(err, os.ErrDeadlineExceeded) { + h.Open() + case errors.Is(err, os.ErrDeadlineExceeded): log.Debugf("write to hid timeout") - } else { + default: log.Errorf("write to hid failed: %s", err) } diff --git a/server/service/hid/operation.go b/server/service/hid/operation.go index 4f8daf3..d07814f 100644 --- a/server/service/hid/operation.go +++ b/server/service/hid/operation.go @@ -2,39 +2,44 @@ package hid import ( "errors" - log "github.com/sirupsen/logrus" "os" + + log "github.com/sirupsen/logrus" ) -var ( - Hidg0 *os.File - Hidg1 *os.File - Hidg2 *os.File -) - -func Open() { - Close() - +func (h *Hid) OpenNoLock() { var err error + h.CloseNoLock() - Hidg0, err = os.OpenFile("/dev/hidg0", os.O_WRONLY, 0666) + h.g0, err = os.OpenFile("/dev/hidg0", os.O_WRONLY, 0o666) if err != nil { log.Errorf("open /dev/hidg0 failed: %s", err) } - Hidg1, err = os.OpenFile("/dev/hidg1", os.O_WRONLY, 0666) + h.g1, err = os.OpenFile("/dev/hidg1", os.O_WRONLY, 0o666) if err != nil { log.Errorf("open /dev/hidg1 failed: %s", err) } - Hidg2, err = os.OpenFile("/dev/hidg2", os.O_WRONLY, 0666) + h.g2, err = os.OpenFile("/dev/hidg2", os.O_WRONLY, 0o666) if err != nil { log.Errorf("open /dev/hidg2 failed: %s", err) } } -func Close() { - for _, file := range []*os.File{Hidg0, Hidg1, Hidg2} { +func (h *Hid) Open() { + h.kbMutex.Lock() + defer h.kbMutex.Unlock() + h.mouseMutex.Lock() + defer h.mouseMutex.Unlock() + + h.CloseNoLock() + + h.OpenNoLock() +} + +func (h *Hid) CloseNoLock() { + for _, file := range []*os.File{h.g0, h.g1, h.g2} { if file != nil { _ = file.Sync() _ = file.Close() @@ -42,12 +47,21 @@ func Close() { } } -func Write(file *os.File, data []byte) { +func (h *Hid) Close() { + h.kbMutex.Lock() + defer h.kbMutex.Unlock() + h.mouseMutex.Lock() + defer h.mouseMutex.Unlock() + + h.CloseNoLock() +} + +func (h *Hid) Write(file *os.File, data []byte) { _, err := file.Write(data) if err != nil { if errors.Is(err, os.ErrClosed) { - Open() log.Debugf("hid already closed, reopen it...") + h.Open() } else { log.Errorf("write to hid failed: %s", err) } diff --git a/server/service/hid/paste.go b/server/service/hid/paste.go index 5ffbe20..926e00c 100644 --- a/server/service/hid/paste.go +++ b/server/service/hid/paste.go @@ -1,10 +1,12 @@ package hid import ( - "NanoKVM-Server/proto" + "time" + "github.com/gin-gonic/gin" log "github.com/sirupsen/logrus" - "time" + + "NanoKVM-Server/proto" ) type Char struct { @@ -30,6 +32,7 @@ func (s *Service) Paste(c *gin.Context) { return } + s.hid.kbMutex.Lock() for _, char := range req.Content { key, ok := charMap[char] if !ok { @@ -43,18 +46,19 @@ func (s *Service) Paste(c *gin.Context) { // only handle shift. Need to handle all modifiers? if key.Modifiers > 0 { keyShift := []byte{0x00, 0x00, byte(225), 0x00, 0x00, 0x00, 0x00, 0x00} - Write(Hidg0, keyShift) + s.hid.Write(s.hid.g0, keyShift) } - Write(Hidg0, keyDown) - Write(Hidg0, keyUp) + s.hid.Write(s.hid.g0, keyDown) + s.hid.Write(s.hid.g0, keyUp) if key.Modifiers > 0 { - Write(Hidg0, keyUp) + s.hid.Write(s.hid.g0, keyUp) } time.Sleep(50 * time.Millisecond) } + s.hid.kbMutex.Unlock() rsp.OkRsp(c) log.Debugf("hid paste success, total %d", len(req.Content)) diff --git a/server/service/hid/reset.go b/server/service/hid/reset.go index 1dfa40c..5a72c9b 100644 --- a/server/service/hid/reset.go +++ b/server/service/hid/reset.go @@ -1,29 +1,75 @@ package hid import ( - "NanoKVM-Server/proto" + "os" + "github.com/gin-gonic/gin" log "github.com/sirupsen/logrus" - "os/exec" + + "NanoKVM-Server/proto" ) func (s *Service) Reset(c *gin.Context) { var rsp proto.Response + s.hid.kbMutex.Lock() + defer s.hid.kbMutex.Unlock() // reset USB - commands := []string{ - "echo > /sys/kernel/config/usb_gadget/g0/UDC", - "ls /sys/class/udc/ | cat > /sys/kernel/config/usb_gadget/g0/UDC", + f, err := os.Open("/sys/kernel/config/usb_gadget/g0/UDC") + if err != nil { + log.Errorf("open /sys/kernel/config/usb_gadget/g0/UDC failed: %s", err) + rsp.ErrRsp(c, -1, "open usb gadget file failed") + return + } + err = f.Truncate(0) + if err != nil { + _ = f.Close() + log.Errorf("truncate /sys/kernel/config/usb_gadget/g0/UDC failed: %s", err) + rsp.ErrRsp(c, -1, "truncate usb gadget file failed") + return + } + _, err = f.Seek(0, 0) + if err != nil { + _ = f.Close() + log.Errorf("seek to 0 failed: %s", err) + rsp.ErrRsp(c, -1, "seek to 0 in usb gadget file failed") + return + } + _, err = f.WriteString("\n") + if err != nil { + _ = f.Close() + log.Errorf("write to /sys/kernel/config/usb_gadget/g0/UDC failed: %s", err) + rsp.ErrRsp(c, -1, "write to usb gadget file failed") + return + } + _ = f.Close() + + devices, err := os.ReadDir("/sys/class/udc/") + if err != nil { + log.Errorf("read udc directory failed: %s", err) + rsp.ErrRsp(c, -1, "read udc directory failed") + return } - for _, command := range commands { - err := exec.Command("sh", "-c", command).Run() + f, err = os.Open("/sys/kernel/config/usb_gadget/g0/UDC") + if err != nil { + log.Errorf("open /sys/kernel/config/usb_gadget/g0/UDC failed: %s", err) + rsp.ErrRsp(c, -1, "open usb gadget file failed") + return + } + for _, device := range devices { + _, err = f.WriteString(device.Name() + "\n") if err != nil { - rsp.ErrRsp(c, -2, "execute command failed") + _ = f.Close() + log.Errorf("write to /sys/kernel/config/usb_gadget/g0/UDC failed: %s", err) + rsp.ErrRsp(c, -1, "write to usb gadget file failed") return } + } + _ = f.Close() + rsp.OkRsp(c) log.Debugf("reset hid success") } diff --git a/server/service/hid/service.go b/server/service/hid/service.go index 81b9dc4..211e134 100644 --- a/server/service/hid/service.go +++ b/server/service/hid/service.go @@ -1,7 +1,11 @@ package hid -type Service struct{} +type Service struct { + hid *Hid +} func NewService() *Service { - return &Service{} + return &Service{ + hid: GetHid(), + } } diff --git a/server/service/network/tailscale.go b/server/service/network/tailscale.go index 1d390d6..8aeaad8 100644 --- a/server/service/network/tailscale.go +++ b/server/service/network/tailscale.go @@ -1,13 +1,9 @@ package network import ( - "NanoKVM-Server/proto" - "NanoKVM-Server/utils" "bufio" "encoding/json" "fmt" - "github.com/gin-gonic/gin" - log "github.com/sirupsen/logrus" "io" "net" "net/http" @@ -15,13 +11,41 @@ import ( "os/exec" "regexp" "strings" + + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" + + "NanoKVM-Server/proto" + "NanoKVM-Server/utils" ) const ( - TailscalePath = "/usr/bin/tailscale" - TailscaledPath = "/usr/sbin/tailscaled" + tailscalePath = "/usr/bin/tailscale" + tailscaledPath = "/usr/sbin/tailscaled" + + backendStateRunning = "Running" + backendStateNeedsLogin = "NeedsLogin" + backendStateStopped = "Stopped" + + responseStateRunning = "running" + responseStateNeedsLogin = "notLogin" + responseStateStopped = "stopped" + responseStateNotInstalled = "notInstall" ) +var backendToResponseStates = map[string]string{ + backendStateRunning: responseStateRunning, + backendStateNeedsLogin: responseStateNeedsLogin, + backendStateStopped: responseStateStopped, +} + +func stateToJson(state string) string { + if resp, ok := backendToResponseStates[state]; ok { + return resp + } + return "" +} + type TailscaleStatus struct { BackendState string `json:"BackendState"` @@ -43,48 +67,52 @@ func (s *Service) InstallTailscale(c *gin.Context) { return } - var ( + const ( downloadUrl = "http://cdn.sipeed.com/nanokvm/resources/tailscale_riscv64.zip" workspace = "/root/.tailscale" + ) + var ( zipFile = fmt.Sprintf("%s/tailscale_riscv64.zip", workspace) tailscale = fmt.Sprintf("%s/tailscale_riscv64/tailscale", workspace) tailscaled = fmt.Sprintf("%s/tailscale_riscv64/tailscaled", workspace) ) // download - _ = os.MkdirAll(workspace, 0755) - defer exec.Command("sh", "-c", fmt.Sprintf("rm -rf %s", workspace)).Run() + _ = os.MkdirAll(workspace, 0o755) + defer func() { + _ = os.RemoveAll(workspace) + }() req, err := http.NewRequest("GET", downloadUrl, nil) if err != nil { - log.Errorf("new request err: %s", err) + log.Errorf("failed to create new request: %s", err) rsp.ErrRsp(c, -1, "request failed") return } err = utils.Download(req, zipFile) if err != nil { + log.Errorf("download failed: %s", err) rsp.ErrRsp(c, -2, "download failed") return } // install - command := fmt.Sprintf("unzip %s -d %s", zipFile, workspace) - err = exec.Command("sh", "-c", command).Run() + err = utils.Unzip(zipFile, workspace) if err != nil { log.Errorf("unzip failed: %s", err) rsp.ErrRsp(c, -3, "unzip failed") return } - err = os.Rename(tailscale, TailscalePath) + err = utils.MoveFile(tailscale, tailscalePath) if err != nil { - log.Debugf("rename %s failed: %s", tailscale, err) + log.Errorf("rename %s failed: %s", tailscale, err) } - err = os.Rename(tailscaled, TailscaledPath) + err = utils.MoveFile(tailscaled, tailscaledPath) if err != nil { - log.Debugf("rename %s failed: %s", tailscaled, err) + log.Errorf("rename %s failed: %s", tailscaled, err) } _ = runTailscale() @@ -107,9 +135,9 @@ func (s *Service) LoginTailscale(c *gin.Context) { return } - if status.BackendState == "Running" { + if status.BackendState == backendStateRunning { rsp.OkRspWithData(c, &proto.LoginTailscaleRsp{ - Status: "running", + Status: responseStateRunning, }) return } @@ -121,13 +149,17 @@ func (s *Service) LoginTailscale(c *gin.Context) { return } - defer stderr.Close() + defer func() { + _ = stderr.Close() + }() - go cmd.Run() + go func() { + _ = cmd.Run() + }() url := parseLoginUrl(stderr) rsp.OkRspWithData(c, &proto.LoginTailscaleRsp{ - Status: "notLogin", + Status: responseStateNeedsLogin, Url: url, }) log.Debugf("tailscale login url: %s", url) @@ -152,26 +184,20 @@ func (s *Service) GetTailscaleStatus(c *gin.Context) { var data proto.GetTailscaleStatusRsp if exist := isTailscaleExist(); !exist { - data.Status = "notInstall" + data.Status = responseStateNotInstalled rsp.OkRspWithData(c, data) return } status, err := getTailscaleStatus() if err != nil { - data.Status = "notLogin" + data.Status = responseStateNeedsLogin rsp.OkRspWithData(c, data) return } - switch status.BackendState { - case "NeedsLogin": - data.Status = "notLogin" - case "Running": - data.Status = "running" - case "Stopped": - data.Status = "stopped" - default: + data.Status = stateToJson(status.BackendState) + if data.Status == "" { rsp.ErrRsp(c, -1, "unknown state") return } @@ -215,11 +241,12 @@ func (s *Service) UpdateTailscaleStatus(c *gin.Context) { data := &proto.UpdateTailscaleStatusRsp{} - if status.BackendState == "Running" { - data.Status = "running" - } else if status.BackendState == "Stopped" { - data.Status = "stopped" - } else { + switch status.BackendState { + case backendStateRunning: + data.Status = responseStateRunning + case backendStateStopped: + data.Status = responseStateStopped + default: rsp.ErrRsp(c, -4, "unknown tailscale status") return } @@ -229,15 +256,15 @@ func (s *Service) UpdateTailscaleStatus(c *gin.Context) { } func isTailscaleExist() bool { - _, err1 := os.Stat(TailscalePath) - _, err2 := os.Stat(TailscaledPath) + _, err1 := os.Stat(tailscalePath) + _, err2 := os.Stat(tailscaledPath) return err1 == nil && err2 == nil } func runTailscale() error { - for _, filePath := range []string{TailscalePath, TailscaledPath} { - if err := utils.EnsurePermission(filePath, 0100); err != nil { + for _, filePath := range []string{tailscalePath, tailscaledPath} { + if err := utils.EnsurePermission(filePath, 0o100); err != nil { return err } } @@ -288,7 +315,7 @@ func parseLoginUrl(r io.Reader) string { } if strings.Contains(line, "https") { - reg := regexp.MustCompile("\\s+") + reg := regexp.MustCompile(`\s+`) return reg.ReplaceAllString(line, "") } } diff --git a/server/service/network/wol.go b/server/service/network/wol.go index fc50270..c526422 100644 --- a/server/service/network/wol.go +++ b/server/service/network/wol.go @@ -1,14 +1,16 @@ package network import ( - "NanoKVM-Server/proto" "fmt" - "github.com/gin-gonic/gin" - log "github.com/sirupsen/logrus" "os" "os/exec" "path/filepath" "strings" + + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" + + "NanoKVM-Server/proto" ) const ( @@ -83,7 +85,7 @@ func (s *Service) DeleteMac(c *gin.Context) { } data := strings.Join(newMacs, "\n") - err = os.WriteFile(WolHistory, []byte(data), 0644) + err = os.WriteFile(WolHistory, []byte(data), 0o644) if err != nil { log.Errorf("write %s failed: %s", WolHistory, err) rsp.ErrRsp(c, -3, "write failed") @@ -99,18 +101,20 @@ func saveMac(mac string) { return } - err := os.MkdirAll(filepath.Dir(WolHistory), 0644) + err := os.MkdirAll(filepath.Dir(WolHistory), 0o644) if err != nil { log.Errorf("create dir failed: %s", err) return } - file, err := os.OpenFile(WolHistory, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + file, err := os.OpenFile(WolHistory, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) if err != nil { log.Errorf("open %s failed: %s", WolHistory, err) return } - defer file.Close() + defer func() { + _ = file.Close() + }() content := fmt.Sprintf("%s\n", mac) _, err = file.WriteString(content) diff --git a/server/service/storage/image.go b/server/service/storage/image.go index ba0adaa..0c4ea4d 100644 --- a/server/service/storage/image.go +++ b/server/service/storage/image.go @@ -1,26 +1,28 @@ package storage import ( - "NanoKVM-Server/proto" - "github.com/gin-gonic/gin" - log "github.com/sirupsen/logrus" "os" "os/exec" "path/filepath" "strings" + + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" + + "NanoKVM-Server/proto" ) const ( - ImageDirectory = "/data" - ImageNone = "/dev/mmcblk0p3" - MountDevice = "/sys/kernel/config/usb_gadget/g0/functions/mass_storage.disk0/lun.0/file" + imageDirectory = "/data" + imageNone = "/dev/mmcblk0p3" + mountDevice = "/sys/kernel/config/usb_gadget/g0/functions/mass_storage.disk0/lun.0/file" ) func (s *Service) GetImages(c *gin.Context) { var rsp proto.Response var images []string - err := filepath.Walk(ImageDirectory, func(path string, info os.FileInfo, err error) error { + err := filepath.Walk(imageDirectory, func(path string, info os.FileInfo, err error) error { if err != nil { return err } @@ -34,7 +36,6 @@ func (s *Service) GetImages(c *gin.Context) { return nil }) - if err != nil { rsp.ErrRsp(c, -2, "get images failed") return @@ -57,11 +58,11 @@ func (s *Service) MountImage(c *gin.Context) { image := req.File if image == "" { - image = ImageNone + image = imageNone } // mount - if err := os.WriteFile(MountDevice, []byte(image), 0666); err != nil { + if err := os.WriteFile(mountDevice, []byte(image), 0o666); err != nil { log.Errorf("mount file %s failed: %s", image, err) rsp.ErrRsp(c, -2, "mount image failed") return @@ -88,14 +89,14 @@ func (s *Service) MountImage(c *gin.Context) { func (s *Service) GetMountedImage(c *gin.Context) { var rsp proto.Response - content, err := os.ReadFile(MountDevice) + content, err := os.ReadFile(mountDevice) if err != nil { rsp.ErrRsp(c, -2, "read failed") return } image := strings.ReplaceAll(string(content), "\n", "") - if image == ImageNone { + if image == imageNone { image = "" } diff --git a/server/service/stream/frame-detect.go b/server/service/stream/frame-detect.go index 75cb741..2cf2110 100644 --- a/server/service/stream/frame-detect.go +++ b/server/service/stream/frame-detect.go @@ -1,17 +1,19 @@ package stream import ( - "NanoKVM-Server/proto" "errors" - "github.com/gin-gonic/gin" - log "github.com/sirupsen/logrus" "os" "time" + + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" + + "NanoKVM-Server/proto" ) const ( - FrameDetect = "/etc/kvm/frame_detact" - FrameDetectTmp = "/etc/kvm/frame_detact_tmp" + frameDetect = "/etc/kvm/frame_detact" + frameDetectTmp = "/etc/kvm/frame_detact_tmp" ) type UpdateFrameDetectRsp struct { @@ -43,15 +45,17 @@ func (s *Service) UpdateFrameDetect(c *gin.Context) { } if isEnabled { - _ = os.Remove(FrameDetect) - _ = os.Remove(FrameDetectTmp) + _ = os.Remove(frameDetect) + _ = os.Remove(frameDetectTmp) } else { - file, err2 := os.OpenFile(FrameDetect, os.O_CREATE|os.O_TRUNC, 0644) + file, err2 := os.OpenFile(frameDetect, os.O_CREATE|os.O_TRUNC, 0o644) if err2 != nil { rsp.ErrRsp(c, -3, "enable frame detect failed") return } - defer file.Close() + defer func() { + _ = file.Close() + }() } isEnabled, err = isFrameDetectEnabled() @@ -69,7 +73,7 @@ func (s *Service) UpdateFrameDetect(c *gin.Context) { func (s *Service) StopFrameDetect(c *gin.Context) { var rsp proto.Response - exist, err := isFileExist(FrameDetect) + exist, err := isFileExist(frameDetect) if err != nil { rsp.ErrRsp(c, -1, "unknown frame status") return @@ -80,7 +84,7 @@ func (s *Service) StopFrameDetect(c *gin.Context) { return } - err = os.Rename(FrameDetect, FrameDetectTmp) + err = os.Rename(frameDetect, frameDetectTmp) if err != nil { rsp.ErrRsp(c, -2, "stop operation failed") return @@ -88,7 +92,7 @@ func (s *Service) StopFrameDetect(c *gin.Context) { go func() { time.Sleep(20 * time.Second) - _ = os.Rename(FrameDetectTmp, FrameDetect) + _ = os.Rename(frameDetectTmp, frameDetect) log.Debugf("frame detect started") }() @@ -97,7 +101,7 @@ func (s *Service) StopFrameDetect(c *gin.Context) { } func isFrameDetectEnabled() (bool, error) { - exist, err := isFileExist(FrameDetect) + exist, err := isFileExist(frameDetect) if err != nil { return false, err } @@ -106,7 +110,7 @@ func isFrameDetectEnabled() (bool, error) { return true, nil } - exist, err = isFileExist(FrameDetectTmp) + exist, err = isFileExist(frameDetectTmp) if err != nil { return false, err } diff --git a/server/service/stream/mjpeg.go b/server/service/stream/mjpeg.go index 5890018..415ee66 100644 --- a/server/service/stream/mjpeg.go +++ b/server/service/stream/mjpeg.go @@ -1,9 +1,10 @@ package stream import ( - "github.com/gin-gonic/gin" "io" "net/http" + + "github.com/gin-gonic/gin" ) func (s *Service) Mjpeg(c *gin.Context) { @@ -14,7 +15,9 @@ func (s *Service) Mjpeg(c *gin.Context) { c.String(http.StatusInternalServerError, "Failed to connect to MJPEG server") return } - defer resp.Body.Close() + defer func() { + _ = resp.Body.Close() + }() if resp.StatusCode != http.StatusOK { c.String(resp.StatusCode, "MJPEG server returned an error") @@ -23,5 +26,9 @@ func (s *Service) Mjpeg(c *gin.Context) { c.Header("Content-Type", resp.Header.Get("Content-Type")) - _, _ = io.Copy(c.Writer, resp.Body) + _, err = io.Copy(c.Writer, resp.Body) + if err != nil { + c.String(http.StatusInternalServerError, "Failed to copy MJPEG stream") + return + } } diff --git a/server/service/vm/gpio.go b/server/service/vm/gpio.go index 4ce0840..b37ee58 100644 --- a/server/service/vm/gpio.go +++ b/server/service/vm/gpio.go @@ -1,25 +1,16 @@ package vm import ( - "NanoKVM-Server/proto" - "github.com/gin-gonic/gin" - log "github.com/sirupsen/logrus" + "fmt" "os" "strconv" - "strings" "time" -) -const ( - HW_VERSION_FILE = "/etc/kvm/hw" + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" - GPIO_PWR = "/sys/class/gpio/gpio503/value" - GPIO_PWR_LED = "/sys/class/gpio/gpio504/value" - - GPIO_RST_ALPHA = "/sys/class/gpio/gpio507/value" - GPIO_HDD_LED_ALPHA = "/sys/class/gpio/gpio505/value" - - GPIO_RST_BETA = "/sys/class/gpio/gpio505/value" + "NanoKVM-Server/config" + "NanoKVM-Server/proto" ) func (s *Service) SetGpio(c *gin.Context) { @@ -27,23 +18,19 @@ func (s *Service) SetGpio(c *gin.Context) { var rsp proto.Response if err := proto.ParseFormRequest(c, &req); err != nil { - rsp.ErrRsp(c, -1, "invalid arguments") + rsp.ErrRsp(c, -1, fmt.Sprintf("invalid arguments: %s", err)) return } - version := getHwVersion() device := "" - if req.Type == "power" { - device = GPIO_PWR - } else if req.Type == "reset" { - if version == "alpha" { - device = GPIO_RST_ALPHA - } else { - device = GPIO_RST_BETA - } - } else { - rsp.ErrRsp(c, -2, "invalid power event") + switch req.Type { + case "power": + device = s.config.HW.GPIOPower + case "reset": + device = s.config.HW.GPIOReset + default: + rsp.ErrRsp(c, -2, fmt.Sprintf("invalid power event: %s", req.Type)) return } @@ -55,27 +42,30 @@ func (s *Service) SetGpio(c *gin.Context) { } if err := writeGpio(device, duration); err != nil { - rsp.ErrRsp(c, -3, "operation failed") + rsp.ErrRsp(c, -3, fmt.Sprintf("operation failed: %s", err)) return } - log.Debugf("set gpio %s success", device) + log.Debugf("gpio %s set sucessfully", device) rsp.OkRsp(c) } func (s *Service) GetGpio(c *gin.Context) { var rsp proto.Response - pwr, err := readGpio(GPIO_PWR_LED) + pwr, err := readGpio(s.config.HW.GPIOPowerLED) if err != nil { - rsp.ErrRsp(c, -2, "read led failed") + rsp.ErrRsp(c, -2, fmt.Sprintf("failed to read power led: %s", err)) return } hdd := false - version := getHwVersion() - if version == "alpha" { - hdd, err = readGpio(GPIO_HDD_LED_ALPHA) + if s.config.HW.Version == config.HWVersionAlpha { + hdd, err = readGpio(s.config.HW.GPIOHDDLed) + if err != nil { + rsp.ErrRsp(c, -2, fmt.Sprintf("failed to read hdd led: %s", err)) + return + } } data := &proto.GetGpioRsp{ @@ -85,28 +75,15 @@ func (s *Service) GetGpio(c *gin.Context) { rsp.OkRspWithData(c, data) } -// get hardware version: alpha or beta -func getHwVersion() string { - content, err := os.ReadFile(HW_VERSION_FILE) - if err == nil { - version := strings.ReplaceAll(string(content), "\n", "") - if version == "beta" { - return "beta" - } - } - - return "alpha" -} - func writeGpio(device string, duration time.Duration) error { - if err := os.WriteFile(device, []byte("1"), 0666); err != nil { + if err := os.WriteFile(device, []byte("1"), 0o666); err != nil { log.Errorf("write gpio %s failed: %s", device, err) return err } time.Sleep(duration) - if err := os.WriteFile(device, []byte("0"), 0666); err != nil { + if err := os.WriteFile(device, []byte("0"), 0o666); err != nil { log.Errorf("write gpio %s failed: %s", device, err) return err } diff --git a/server/service/vm/info.go b/server/service/vm/info.go index b4c593f..f1299c1 100644 --- a/server/service/vm/info.go +++ b/server/service/vm/info.go @@ -1,13 +1,15 @@ package vm import ( - "NanoKVM-Server/proto" "fmt" - "github.com/gin-gonic/gin" - log "github.com/sirupsen/logrus" "net" "os" "strings" + + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" + + "NanoKVM-Server/proto" ) var imageVersionMap = map[string]string{ diff --git a/server/service/vm/screen.go b/server/service/vm/screen.go index 50665c7..68ef750 100644 --- a/server/service/vm/screen.go +++ b/server/service/vm/screen.go @@ -1,11 +1,13 @@ package vm import ( - "NanoKVM-Server/proto" "fmt" + "os" + "github.com/gin-gonic/gin" log "github.com/sirupsen/logrus" - "os" + + "NanoKVM-Server/proto" ) var screenFileMap = map[string]string{ @@ -30,7 +32,7 @@ func (s *Service) SetScreen(c *gin.Context) { } data := fmt.Sprintf("%d", req.Value) - err := os.WriteFile(file, []byte(data), 0666) + err := os.WriteFile(file, []byte(data), 0o666) if err != nil { log.Errorf("write kvm %s failed: %s", file, err) rsp.ErrRsp(c, -3, "update screen failed") diff --git a/server/service/vm/script.go b/server/service/vm/script.go index 9e0e161..9606e8a 100644 --- a/server/service/vm/script.go +++ b/server/service/vm/script.go @@ -1,15 +1,17 @@ package vm import ( - "NanoKVM-Server/proto" - "NanoKVM-Server/utils" "fmt" - "github.com/gin-gonic/gin" - log "github.com/sirupsen/logrus" "os" "os/exec" "path/filepath" "strings" + + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" + + "NanoKVM-Server/proto" + "NanoKVM-Server/utils" ) const ScriptDirectory = "/etc/kvm/scripts" @@ -29,7 +31,6 @@ func (s *Service) GetScripts(c *gin.Context) { return nil }) - if err != nil { log.Errorf("get scripts failed: %s", err) rsp.ErrRsp(c, -1, "get scripts failed") @@ -58,7 +59,7 @@ func (s *Service) UploadScript(c *gin.Context) { } if _, err = os.Stat(ScriptDirectory); err != nil { - _ = os.MkdirAll(ScriptDirectory, 0755) + _ = os.MkdirAll(ScriptDirectory, 0o755) } target := fmt.Sprintf("%s/%s", ScriptDirectory, header.Filename) @@ -68,7 +69,7 @@ func (s *Service) UploadScript(c *gin.Context) { return } - _ = utils.EnsurePermission(target, 0100) + _ = utils.EnsurePermission(target, 0o100) data := &proto.UploadScriptRsp{ File: header.Filename, @@ -103,7 +104,12 @@ func (s *Service) RunScript(c *gin.Context) { } else { cmd.Stdout = nil cmd.Stderr = nil - go cmd.Run() + go func() { + err := cmd.Run() + if err != nil { + log.Errorf("run script %s in background failed: %s", req.Name, err) + } + }() } if err != nil { diff --git a/server/service/vm/service.go b/server/service/vm/service.go index d47dc71..124f3e5 100644 --- a/server/service/vm/service.go +++ b/server/service/vm/service.go @@ -1,7 +1,15 @@ package vm -type Service struct{} +import ( + "NanoKVM-Server/config" +) + +type Service struct { + config *config.Config +} func NewService() *Service { - return &Service{} + return &Service{ + config: config.GetInstance(), + } } diff --git a/server/service/vm/terminal.go b/server/service/vm/terminal.go index 26e0a2d..fbf22cf 100644 --- a/server/service/vm/terminal.go +++ b/server/service/vm/terminal.go @@ -1,16 +1,18 @@ package vm import ( - "NanoKVM-Server/utils" "encoding/json" - "github.com/gin-gonic/gin" - "github.com/gorilla/websocket" - log "github.com/sirupsen/logrus" - "golang.org/x/crypto/ssh" "io" "net/http" "os" "time" + + "github.com/gin-gonic/gin" + "github.com/gorilla/websocket" + log "github.com/sirupsen/logrus" + "golang.org/x/crypto/ssh" + + "NanoKVM-Server/utils" ) const ( @@ -161,10 +163,12 @@ func (s *SshClient) bridgeWSAndSSH() { s.client, err = ssh.Dial("tcp", s.addr, config) if err != nil { - //log.Errorf("init ssh failed: %s", err) + // log.Errorf("init ssh failed: %s", err) return } - defer s.client.Close() + defer func() { + _ = s.client.Close() + }() s.session, err = s.client.NewSession() if err != nil { @@ -185,7 +189,9 @@ func (s *SshClient) bridgeWSAndSSH() { log.Errorf("open ssh session in failed: %s", err) return } - defer s.sessionIn.Close() + defer func() { + _ = s.sessionIn.Close() + }() if err = s.session.RequestPty("xterm", size.Height, size.Width, terminalModes); err != nil { log.Errorf("oen session request pty failed: %s", err) diff --git a/server/service/vm/virtual-device.go b/server/service/vm/virtual-device.go index 4a6af2a..2eb40e9 100644 --- a/server/service/vm/virtual-device.go +++ b/server/service/vm/virtual-device.go @@ -1,18 +1,20 @@ package vm import ( - "NanoKVM-Server/proto" - "NanoKVM-Server/service/hid" "errors" - "github.com/gin-gonic/gin" - log "github.com/sirupsen/logrus" "os" "os/exec" + + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" + + "NanoKVM-Server/proto" + "NanoKVM-Server/service/hid" ) const ( - VirtualNetwork = "/boot/usb.rndis0" - VirtualDisk = "/boot/usb.disk0" + virtualNetwork = "/boot/usb.rndis0" + virtualDisk = "/boot/usb.disk0" ) var ( @@ -46,8 +48,8 @@ var ( func (s *Service) GetVirtualDevice(c *gin.Context) { var rsp proto.Response - network, _ := isDeviceExist(VirtualNetwork) - disk, _ := isDeviceExist(VirtualDisk) + network, _ := isDeviceExist(virtualNetwork) + disk, _ := isDeviceExist(virtualDisk) rsp.OkRspWithData(c, &proto.GetVirtualDeviceRsp{ Network: network, @@ -68,8 +70,9 @@ func (s *Service) UpdateVirtualDevice(c *gin.Context) { var device string var commands []string - if req.Device == "network" { - device = VirtualNetwork + switch req.Device { + case "network": + device = virtualNetwork exist, _ := isDeviceExist(device) if !exist { @@ -77,8 +80,8 @@ func (s *Service) UpdateVirtualDevice(c *gin.Context) { } else { commands = unmountNetworkCommands } - } else if req.Device == "disk" { - device = VirtualDisk + case "disk": + device = virtualDisk exist, _ := isDeviceExist(device) if !exist { @@ -86,13 +89,18 @@ func (s *Service) UpdateVirtualDevice(c *gin.Context) { } else { commands = unmountDiskCommands } - } else { + default: rsp.ErrRsp(c, -2, "invalid arguments") return } - hid.Close() - defer hid.Open() + h := hid.GetHid() + h.Lock() + h.CloseNoLock() + defer func() { + h.OpenNoLock() + h.Unlock() + }() for _, command := range commands { err := exec.Command("sh", "-c", command).Run() diff --git a/server/service/ws/watch.go b/server/service/ws/watch.go index 3cd4347..0d19704 100644 --- a/server/service/ws/watch.go +++ b/server/service/ws/watch.go @@ -2,11 +2,12 @@ package ws import ( "encoding/json" - log "github.com/sirupsen/logrus" "os" "strconv" "strings" "time" + + log "github.com/sirupsen/logrus" ) const ( diff --git a/server/service/ws/ws.go b/server/service/ws/ws.go index 36e2bda..55acfc4 100644 --- a/server/service/ws/ws.go +++ b/server/service/ws/ws.go @@ -1,13 +1,15 @@ package ws import ( - "NanoKVM-Server/service/hid" "encoding/json" + "net/http" + "time" + "github.com/gin-gonic/gin" "github.com/gorilla/websocket" log "github.com/sirupsen/logrus" - "net/http" - "time" + + "NanoKVM-Server/service/hid" ) const ( @@ -17,6 +19,7 @@ const ( type WsClient struct { conn *websocket.Conn + hid *hid.Hid keyboard chan []int mouse chan []int watcher chan struct{} @@ -39,6 +42,7 @@ func (s *Service) Connect(c *gin.Context) { log.Debug("websocket connected") client := &WsClient{ + hid: hid.GetHid(), conn: conn, keyboard: make(chan []int, 200), mouse: make(chan []int, 200), @@ -51,10 +55,10 @@ func (s *Service) Connect(c *gin.Context) { func (c *WsClient) Start() { defer c.Clean() - hid.Open() + c.hid.Open() - go hid.Keyboard(c.keyboard) - go hid.Mouse(c.mouse) + go c.hid.Keyboard(c.keyboard) + go c.hid.Mouse(c.mouse) go c.Watch() @@ -97,7 +101,7 @@ func (c *WsClient) Watch() { ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() - var fileModMap = map[string]time.Time{ + fileModMap := map[string]time.Time{ StreamState: time.Unix(0, 0), } @@ -133,7 +137,7 @@ func (c *WsClient) Clean() { close(c.watcher) - hid.Close() + c.hid.Close() log.Debug("websocket disconnected") } diff --git a/server/utils/chmod.go b/server/utils/chmod.go new file mode 100644 index 0000000..8b9b11d --- /dev/null +++ b/server/utils/chmod.go @@ -0,0 +1,23 @@ +package utils + +import ( + "os" + "path/filepath" +) + +func ChmodRecursively(path string, mode uint32) error { + return filepath.Walk(path, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if !info.IsDir() { + err = os.Chmod(path, os.FileMode(mode)) + if err != nil { + return err + } + } + + return nil + }) +} diff --git a/server/utils/encrypt.go b/server/utils/encrypt.go index 1091dd1..9b6af58 100644 --- a/server/utils/encrypt.go +++ b/server/utils/encrypt.go @@ -1,9 +1,10 @@ package utils import ( + "net/url" + "github.com/mervick/aes-everywhere/go/aes256" log "github.com/sirupsen/logrus" - "net/url" ) const EncryptSecretKey = "nanokvm-sipeed-2024" diff --git a/server/utils/http.go b/server/utils/http.go index 077dbd6..cb45891 100644 --- a/server/utils/http.go +++ b/server/utils/http.go @@ -2,34 +2,48 @@ package utils import ( "errors" - log "github.com/sirupsen/logrus" "io" "net/http" "os" + "path/filepath" + + log "github.com/sirupsen/logrus" ) func Download(req *http.Request, target string) error { - out, err := os.OpenFile(target, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) + log.Debugf("downloading %s to %s", req.URL.String(), target) + err := os.MkdirAll(filepath.Dir(target), 0o755) if err != nil { - log.Errorf("create file %s err: %s", target, err) + log.Errorf("create dir %s err: %s", filepath.Dir(target), err) return err } - defer out.Close() + out, err := os.OpenFile(target, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o755) + if err != nil { + log.Errorf("cannot create file '%s', error: %s", target, err) + return err + } + defer func() { + _ = out.Close() + }() resp, err := (&http.Client{}).Do(req) if err != nil { - log.Errorf("download file err: %s", err) + log.Errorf("request error: %s", err) return err } - defer resp.Body.Close() + defer func() { + _ = resp.Body.Close() + }() if resp.StatusCode != http.StatusOK { - return errors.New("request error") + log.Errorf("request failed, status code: %d", resp.StatusCode) + return errors.New("update website is inaccessible right now") } contentType := resp.Header.Get("Content-Type") if contentType != "application/octet-stream" && contentType != "application/zip" { - return errors.New("download error") + log.Debugf("unexpected content-type, it should be either octet-stream or zip, but got: %s", contentType) + return errors.New("unsupported content type") } _, err = io.Copy(out, resp.Body) diff --git a/server/utils/move-file.go b/server/utils/move-file.go new file mode 100644 index 0000000..a887ba5 --- /dev/null +++ b/server/utils/move-file.go @@ -0,0 +1,78 @@ +package utils + +import ( + "io" + "os" + "path/filepath" + "strings" +) + +func MoveFile(src, dst string) error { + if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil { + return err + } + err := os.Rename(src, dst) + if err != nil { + if strings.Contains(err.Error(), "invalid cross-device link") { + return MoveFileCrossFS(src, dst) + } + return err + } + return nil +} + +func MoveFileCrossFS(src, dst string) error { + tmp := dst + ".tmp" + srcFile, err := os.Open(src) + if err != nil { + return err + } + + tmpFile, err := os.Create(tmp) + if err != nil { + _ = srcFile.Close() + return err + } + _, err = io.Copy(tmpFile, srcFile) + if err != nil { + _ = srcFile.Close() + _ = tmpFile.Close() + return err + } + _ = srcFile.Close() + _ = tmpFile.Close() + fi, err := os.Stat(src) + if err != nil { + return err + } + err = os.Chmod(tmp, fi.Mode()) + if err != nil { + return err + } + _ = os.Remove(src) + err = os.Rename(tmp, dst) + if err != nil { + return err + } + return nil +} + +func MoveFilesRecursively(src, dst string) error { + return filepath.Walk(src, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + fileName := strings.Replace(path, src, "", 1) + dstName := dst + fileName + fileInfo, err := os.Stat(path) + if err != nil { + return err + } + + if fileInfo.IsDir() { + return os.MkdirAll(dstName, fileInfo.Mode()) + } + return MoveFile(path, dstName) + }) +} diff --git a/server/utils/unzip.go b/server/utils/unzip.go new file mode 100644 index 0000000..922993c --- /dev/null +++ b/server/utils/unzip.go @@ -0,0 +1,61 @@ +package utils + +import ( + "archive/zip" + "io" + "os" + "path/filepath" +) + +func Unzip(filename string, dest string) error { + r, err := zip.OpenReader(filename) + if err != nil { + return err + } + defer func() { + _ = r.Close() + }() + + for _, f := range r.File { + dstPath := filepath.Join(dest, filepath.Clean("/"+f.Name)) + if f.FileInfo().IsDir() { + err = os.MkdirAll(dstPath, 0o755) + if err != nil { + return err + } + } else { + err = unzipFile(dstPath, f) + if err == nil { + return err + } + } + } + return nil +} + +func unzipFile(dstPath string, f *zip.File) error { + err := os.MkdirAll(filepath.Dir(dstPath), 0o755) + if err != nil { + return err + } + out, err := os.OpenFile(dstPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, f.Mode()) + if err != nil { + return err + } + defer func() { + _ = out.Close() + }() + + archivedFile, err := f.Open() + if err != nil { + return err + } + + if _, err = io.Copy(out, archivedFile); err != nil { + return err + } + if err = os.Chmod(dstPath, f.Mode()); err != nil { + return err + } + return nil +} diff --git a/web/src/api/application.ts b/web/src/api/application.ts index f79bbab..d121719 100644 --- a/web/src/api/application.ts +++ b/web/src/api/application.ts @@ -10,7 +10,7 @@ export function update() { return http.request({ method: 'post', url: '/api/application/update', - timeout: 5 * 60 * 1000 + timeout: 15 * 60 * 1000 }); }