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
This commit is contained in:
Vladimir Smirnov
2024-10-09 20:54:47 +02:00
parent 726cd915cc
commit a58227910b
50 changed files with 962 additions and 416 deletions

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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
)

View File

@@ -3,8 +3,9 @@ package logger
import (
"bytes"
"fmt"
"github.com/sirupsen/logrus"
"path/filepath"
"github.com/sirupsen/logrus"
)
type formatter struct{}

View File

@@ -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{})

View File

@@ -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"
)

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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")
}

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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

View File

@@ -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) {

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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{}

View File

@@ -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

View File

@@ -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) {

36
server/service/hid/hid.go Normal file
View File

@@ -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
}

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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))

View File

@@ -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")
}

View File

@@ -1,7 +1,11 @@
package hid
type Service struct{}
type Service struct {
hid *Hid
}
func NewService() *Service {
return &Service{}
return &Service{
hid: GetHid(),
}
}

View File

@@ -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, "")
}
}

View File

@@ -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)

View File

@@ -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 = ""
}

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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{

View File

@@ -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")

View File

@@ -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 {

View File

@@ -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(),
}
}

View File

@@ -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)

View File

@@ -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()

View File

@@ -2,11 +2,12 @@ package ws
import (
"encoding/json"
log "github.com/sirupsen/logrus"
"os"
"strconv"
"strings"
"time"
log "github.com/sirupsen/logrus"
)
const (

View File

@@ -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")
}

23
server/utils/chmod.go Normal file
View File

@@ -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
})
}

View File

@@ -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"

View File

@@ -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)

78
server/utils/move-file.go Normal file
View File

@@ -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)
})
}

61
server/utils/unzip.go Normal file
View File

@@ -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
}

View File

@@ -10,7 +10,7 @@ export function update() {
return http.request({
method: 'post',
url: '/api/application/update',
timeout: 5 * 60 * 1000
timeout: 15 * 60 * 1000
});
}