Refactor: Rename NanoKVM to BatchuKVM and update server URL

This commit is contained in:
2025-12-09 20:35:38 +09:00
commit 8cf674c9e5
396 changed files with 54380 additions and 0 deletions

116
server/service/vm/gpio.go Normal file
View File

@@ -0,0 +1,116 @@
package vm
import (
"fmt"
"os"
"strconv"
"time"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"NanoKVM-Server/config"
"NanoKVM-Server/proto"
)
func (s *Service) SetGpio(c *gin.Context) {
var req proto.SetGpioReq
var rsp proto.Response
if err := proto.ParseFormRequest(c, &req); err != nil {
rsp.ErrRsp(c, -1, fmt.Sprintf("invalid arguments: %s", err))
return
}
device := ""
conf := config.GetInstance().Hardware
switch req.Type {
case "power":
device = conf.GPIOPower
case "reset":
device = conf.GPIOReset
default:
rsp.ErrRsp(c, -2, fmt.Sprintf("invalid power event: %s", req.Type))
return
}
var duration time.Duration
if req.Duration > 0 {
duration = time.Duration(req.Duration) * time.Millisecond
} else {
duration = 800 * time.Millisecond
}
if err := writeGpio(device, duration); err != nil {
rsp.ErrRsp(c, -3, fmt.Sprintf("operation failed: %s", err))
return
}
log.Debugf("gpio %s set successfully", device)
rsp.OkRsp(c)
}
func (s *Service) GetGpio(c *gin.Context) {
var rsp proto.Response
conf := config.GetInstance().Hardware
pwr, err := readGpio(conf.GPIOPowerLED)
if err != nil {
rsp.ErrRsp(c, -2, fmt.Sprintf("failed to read power led: %s", err))
return
}
hdd := false
if conf.Version == config.HWVersionAlpha {
hdd, err = readGpio(conf.GPIOHDDLed)
if err != nil {
rsp.ErrRsp(c, -2, fmt.Sprintf("failed to read hdd led: %s", err))
return
}
}
data := &proto.GetGpioRsp{
PWR: pwr,
HDD: hdd,
}
rsp.OkRspWithData(c, data)
}
func writeGpio(device string, duration time.Duration) error {
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"), 0o666); err != nil {
log.Errorf("write gpio %s failed: %s", device, err)
return err
}
return nil
}
func readGpio(device string) (bool, error) {
content, err := os.ReadFile(device)
if err != nil {
log.Errorf("read gpio %s failed: %s", device, err)
return false, err
}
contentStr := string(content)
if len(contentStr) > 1 {
contentStr = contentStr[:len(contentStr)-1]
}
value, err := strconv.Atoi(contentStr)
if err != nil {
log.Errorf("invalid gpio content: %s", content)
return false, nil
}
return value == 0, nil
}

60
server/service/vm/hdmi.go Normal file
View File

@@ -0,0 +1,60 @@
package vm
import (
"time"
"NanoKVM-Server/common"
"NanoKVM-Server/proto"
"NanoKVM-Server/utils"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
)
func (s *Service) ResetHdmi(c *gin.Context) {
var rsp proto.Response
vision := common.GetKvmVision()
vision.SetHDMI(false)
time.Sleep(1 * time.Second)
vision.SetHDMI(true)
utils.PersistHDMIEnabled()
rsp.OkRsp(c)
log.Debug("reset hdmi")
}
func (s *Service) EnableHdmi(c *gin.Context) {
var rsp proto.Response
vision := common.GetKvmVision()
vision.SetHDMI(true)
utils.PersistHDMIEnabled()
rsp.OkRsp(c)
log.Debug("enable hdmi")
}
func (s *Service) DisableHdmi(c *gin.Context) {
var rsp proto.Response
vision := common.GetKvmVision()
vision.SetHDMI(false)
utils.PersistHDMIDisabled()
rsp.OkRsp(c)
log.Debug("disable hdmi")
}
func (s *Service) GetHdmiState(c *gin.Context) {
var rsp proto.Response
rsp.OkRspWithData(c, &proto.GetGetHdmiStateRsp{
Enabled: !utils.IsHdmiDisabled(),
})
log.Debug("get hdmi state")
}

View File

@@ -0,0 +1,84 @@
package vm
import (
"fmt"
"os"
"os/exec"
"strings"
"NanoKVM-Server/proto"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
)
const (
BootHostnameFile = "/boot/hostname"
EtcHostname = "/etc/hostname"
EtcHosts = "/etc/hosts"
)
func (s *Service) SetHostname(c *gin.Context) {
var req proto.SetHostnameReq
var rsp proto.Response
if err := proto.ParseFormRequest(c, &req); err != nil {
rsp.ErrRsp(c, -1, "invalid arguments")
return
}
dataRead, err := os.ReadFile(EtcHostname)
if err != nil {
rsp.ErrRsp(c, -1, "read Hostname failed")
return
}
oldHostname := strings.Replace(string(dataRead), "\n", "", -1)
if (oldHostname != req.Hostname) {
dataRead, err = os.ReadFile(EtcHosts)
if err != nil {
rsp.ErrRsp(c, -1, "read Hosts failed")
return
}
data := []byte(strings.Replace(string(dataRead), oldHostname, req.Hostname, -1))
if err := os.WriteFile(EtcHosts, data, 0o644); err != nil {
rsp.ErrRsp(c, -2, "failed to write data")
return
}
}
data := []byte(fmt.Sprintf("%s", req.Hostname))
if err := os.WriteFile(BootHostnameFile, data, 0o644); err != nil {
rsp.ErrRsp(c, -2, "failed to write data")
return
}
if err := os.WriteFile(EtcHostname, data, 0o644); err != nil {
rsp.ErrRsp(c, -3, "failed to write data")
return
}
rsp.OkRsp(c)
log.Debugf("set Hostname: %s", req.Hostname)
_ = exec.Command("hostname", "-F", EtcHostname).Run()
}
func (s *Service) GetHostname(c *gin.Context) {
var rsp proto.Response
data, err := os.ReadFile(EtcHostname)
if err != nil {
rsp.ErrRsp(c, -1, "read Hostname failed")
return
}
rsp.OkRspWithData(c, &proto.GetHostnameRsp{
Hostname: strings.Replace(string(data), "\n", "", -1),
})
log.Debugf("get Hostname successful")
}

115
server/service/vm/info.go Normal file
View File

@@ -0,0 +1,115 @@
package vm
import (
"NanoKVM-Server/config"
"fmt"
"os"
"strings"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"NanoKVM-Server/proto"
)
var imageVersionMap = map[string]string{
"2024-06-23-20-59-2d2bfb.img": "v1.0.0",
"2024-07-23-20-18-587710.img": "v1.1.0",
"2024-08-08-19-44-bef2ca.img": "v1.2.0",
"2024-11-13-09-59-9c961a.img": "v1.3.0",
"2025-02-17-19-08-3649fe.img": "v1.4.0",
"2025-04-17-14-21-98d17d.img": "v1.4.1",
}
func (s *Service) GetInfo(c *gin.Context) {
var rsp proto.Response
data := &proto.GetInfoRsp{
IPs: getIPs(),
Mdns: getMdns(),
Image: getImageVersion(),
Application: getApplicationVersion(),
DeviceKey: getDeviceKey(),
}
rsp.OkRspWithData(c, data)
log.Debug("get vm information success")
}
func getIPs() (ips []proto.IP) {
interfaces, err := GetInterfaceInfos()
if err != nil {
return
}
for _, iface := range interfaces {
if iface.IP.To4() != nil {
ips = append(ips, proto.IP{
Name: iface.Name,
Addr: iface.IP.String(),
Version: "IPv4",
Type: iface.Type,
})
}
}
return
}
func getMdns() string {
if pid := getAvahiDaemonPid(); pid == "" {
return ""
}
content, err := os.ReadFile("/etc/hostname")
if err != nil {
return ""
}
mdns := strings.ReplaceAll(string(content), "\n", "")
return fmt.Sprintf("%s.local", mdns)
}
func getImageVersion() string {
content, err := os.ReadFile("/boot/ver")
if err != nil {
return ""
}
image := strings.ReplaceAll(string(content), "\n", "")
if version, ok := imageVersionMap[image]; ok {
return version
}
return image
}
func getApplicationVersion() string {
content, err := os.ReadFile("/kvmapp/version")
if err != nil {
return "1.0.0"
}
return strings.ReplaceAll(string(content), "\n", "")
}
func getDeviceKey() string {
content, err := os.ReadFile("/device_key")
if err != nil {
return ""
}
return strings.ReplaceAll(string(content), "\n", "")
}
func (s *Service) GetHardware(c *gin.Context) {
var rsp proto.Response
conf := config.GetInstance()
version := conf.Hardware.Version.String()
rsp.OkRspWithData(c, &proto.GetHardwareRsp{
Version: version,
})
}

107
server/service/vm/ip.go Normal file
View File

@@ -0,0 +1,107 @@
package vm
import (
"fmt"
"net"
"strings"
log "github.com/sirupsen/logrus"
)
const (
Wired = "Wired"
Wireless = "Wireless"
Other = "Other"
)
type InterfaceInfo struct {
Name string
Type string
IP net.IP
}
func GetInterfaceInfos() ([]*InterfaceInfo, error) {
var interfaceInfos []*InterfaceInfo
interfaces, err := net.Interfaces()
if err != nil {
log.Errorf("failed to get net interfaces: %s", err)
return nil, err
}
for _, iface := range interfaces {
info := getInterfaceInfo(iface)
if info != nil {
interfaceInfos = append(interfaceInfos, info)
}
}
if len(interfaceInfos) == 0 {
return nil, fmt.Errorf("no valid IP address")
}
return interfaceInfos, nil
}
func getInterfaceInfo(iface net.Interface) *InterfaceInfo {
if iface.Flags&net.FlagUp == 0 {
return nil
}
interfaceType := getInterfaceType(iface)
if interfaceType == Other {
return nil
}
interfaceIP := getInterfaceIP(iface)
if interfaceIP == nil {
return nil
}
return &InterfaceInfo{
Name: iface.Name,
Type: interfaceType,
IP: interfaceIP,
}
}
func getInterfaceType(iface net.Interface) string {
if strings.HasPrefix(iface.Name, "eth") || strings.HasPrefix(iface.Name, "en") {
return Wired
}
if strings.HasPrefix(iface.Name, "wlan") || strings.HasPrefix(iface.Name, "wl") {
return Wireless
}
return Other
}
func getInterfaceIP(iface net.Interface) net.IP {
addrs, err := iface.Addrs()
if err != nil {
log.Errorf("failed to get interface addresses: %s", err)
return nil
}
for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
default:
continue
}
if ip == nil {
continue
}
return ip
}
return nil
}

View File

@@ -0,0 +1,134 @@
package jiggler
import (
"NanoKVM-Server/service/hid"
"os"
"strings"
"sync"
"time"
)
const (
ConfigFile = "/etc/kvm/mouse-jiggler"
Interval = 15 * time.Second
)
var (
jiggler Jiggler
once sync.Once
)
type Jiggler struct {
mutex sync.Mutex
enabled bool
running bool
mode string
lastUpdated time.Time
}
func GetJiggler() *Jiggler {
once.Do(func() {
jiggler = Jiggler{
mutex: sync.Mutex{},
enabled: false,
running: false,
mode: "relative",
lastUpdated: time.Now(),
}
content, err := os.ReadFile(ConfigFile)
if err != nil {
return
}
mode := strings.ReplaceAll(string(content), "\n", "")
if mode != "" {
jiggler.mode = mode
}
jiggler.enabled = true
})
return &jiggler
}
func (j *Jiggler) Enable(mode string) error {
err := os.WriteFile(ConfigFile, []byte(mode), 0644)
if err != nil {
return err
}
j.enabled = true
j.mode = mode
j.Run()
return nil
}
func (j *Jiggler) Disable() error {
if err := os.Remove(ConfigFile); err != nil {
return err
}
j.enabled = false
j.mode = "relative"
return nil
}
func (j *Jiggler) Run() {
if !j.enabled || j.running {
return
}
j.mutex.Lock()
j.running = true
j.mutex.Unlock()
j.Update()
go func() {
ticker := time.NewTicker(Interval)
defer ticker.Stop()
for range ticker.C {
if !j.enabled {
j.running = false
return
}
if time.Since(j.lastUpdated) > Interval {
move(j.mode)
j.Update()
}
}
}()
}
func (j *Jiggler) Update() {
if j.running {
j.lastUpdated = time.Now()
}
}
func (j *Jiggler) IsEnabled() bool {
return j.enabled
}
func (j *Jiggler) GetMode() string {
return j.mode
}
func move(mode string) {
h := hid.GetHid()
if mode == "absolute" {
h.WriteHid2([]byte{0x00, 0x00, 0x3f, 0x00, 0x3f, 0x00})
time.Sleep(100 * time.Millisecond)
h.WriteHid2([]byte{0x00, 0xff, 0x3f, 0xff, 0x3f, 0x00})
} else {
h.WriteHid1([]byte{0x00, 0xa, 0xa, 0x00})
time.Sleep(100 * time.Millisecond)
h.WriteHid1([]byte{0x00, 0xf6, 0xf6, 0x00})
}
}

92
server/service/vm/mdns.go Normal file
View File

@@ -0,0 +1,92 @@
package vm
import (
"NanoKVM-Server/proto"
"fmt"
"os"
"os/exec"
"strings"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
)
const (
AvahiDaemonPid = "/run/avahi-daemon/pid"
AvahiDaemonScript = "/etc/init.d/S50avahi-daemon"
AvahiDaemonBackupScript = "/kvmapp/system/init.d/S50avahi-daemon"
)
func (s *Service) GetMdnsState(c *gin.Context) {
var rsp proto.Response
pid := getAvahiDaemonPid()
rsp.OkRspWithData(c, &proto.GetMdnsStateRsp{
Enabled: pid != "",
})
}
func (s *Service) EnableMdns(c *gin.Context) {
var rsp proto.Response
pid := getAvahiDaemonPid()
if pid != "" {
rsp.OkRsp(c)
return
}
commands := []string{
fmt.Sprintf("cp -f %s %s", AvahiDaemonBackupScript, AvahiDaemonScript),
fmt.Sprintf("%s start", AvahiDaemonScript),
}
command := strings.Join(commands, " && ")
err := exec.Command("sh", "-c", command).Run()
if err != nil {
log.Errorf("failed to start avahi-daemon: %s", err)
rsp.ErrRsp(c, -1, "failed to enable mdns")
return
}
rsp.OkRsp(c)
log.Debugf("avahi-daemon started")
}
func (s *Service) DisableMdns(c *gin.Context) {
var rsp proto.Response
pid := getAvahiDaemonPid()
if pid == "" {
rsp.OkRsp(c)
return
}
command := fmt.Sprintf("kill -9 %s", pid)
err := exec.Command("sh", "-c", command).Run()
if err != nil {
log.Errorf("failed to stop avahi-daemon: %s", err)
rsp.ErrRsp(c, -1, "failed to disable mdns")
return
}
_ = os.Remove(AvahiDaemonPid)
_ = os.Remove(AvahiDaemonScript)
rsp.OkRsp(c)
log.Debugf("avahi-daemon stopped")
}
func getAvahiDaemonPid() string {
if _, err := os.Stat(AvahiDaemonPid); err != nil {
return ""
}
content, err := os.ReadFile(AvahiDaemonPid)
if err != nil {
log.Errorf("failed to read mdns pid: %s", err)
return ""
}
return strings.ReplaceAll(string(content), "\n", "")
}

View File

@@ -0,0 +1,58 @@
package vm
import (
"NanoKVM-Server/proto"
"NanoKVM-Server/utils"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
)
func (s *Service) SetMemoryLimit(c *gin.Context) {
var req proto.SetMemoryLimitReq
var rsp proto.Response
err := proto.ParseFormRequest(c, &req)
if err != nil {
rsp.ErrRsp(c, -1, "invalid arguments")
return
}
if req.Enabled {
err = utils.SetGoMemLimit(req.Limit)
} else {
err = utils.DelGoMemLimit()
}
if err != nil {
rsp.ErrRsp(c, -2, "failed to set memory limit")
return
}
rsp.OkRsp(c)
log.Debugf("set memory limit successful, enabled: %t, limit: %d", req.Enabled, req.Limit)
}
func (s *Service) GetMemoryLimit(c *gin.Context) {
var rsp proto.Response
exist := utils.IsGoMemLimitExist()
if !exist {
rsp.OkRspWithData(c, &proto.GetMemoryLimitRsp{
Enabled: false,
Limit: 0,
})
return
}
limit, err := utils.GetGoMemLimit()
if err != nil {
rsp.ErrRsp(c, -1, "failed to get memory limit")
return
}
rsp.OkRspWithData(c, &proto.GetMemoryLimitRsp{
Enabled: true,
Limit: limit,
})
}

View File

@@ -0,0 +1,49 @@
package vm
import (
"NanoKVM-Server/proto"
"NanoKVM-Server/service/vm/jiggler"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
)
func (s *Service) GetMouseJiggler(c *gin.Context) {
var rsp proto.Response
mouseJiggler := jiggler.GetJiggler()
data := &proto.GetMouseJigglerRsp{
Enabled: mouseJiggler.IsEnabled(),
Mode: mouseJiggler.GetMode(),
}
rsp.OkRspWithData(c, data)
}
func (s *Service) SetMouseJiggler(c *gin.Context) {
var req proto.SetMouseJigglerReq
var rsp proto.Response
err := proto.ParseFormRequest(c, &req)
if err != nil {
rsp.ErrRsp(c, -1, "invalid arguments")
return
}
mouseJiggler := jiggler.GetJiggler()
if req.Enabled {
err = mouseJiggler.Enable(req.Mode)
} else {
err = mouseJiggler.Disable()
}
if err != nil {
rsp.ErrRsp(c, -2, "operation failed")
return
}
rsp.OkRsp(c)
log.Debugf("set mouse jiggler: %t", req.Enabled)
}

72
server/service/vm/oled.go Normal file
View File

@@ -0,0 +1,72 @@
package vm
import (
"NanoKVM-Server/proto"
"fmt"
"os"
"strconv"
"strings"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
)
const (
OLEDExistFile = "/etc/kvm/oled_exist"
OLEDSleepFile = "/etc/kvm/oled_sleep"
)
func (s *Service) SetOLED(c *gin.Context) {
var req proto.SetOledReq
var rsp proto.Response
if err := proto.ParseFormRequest(c, &req); err != nil {
rsp.ErrRsp(c, -1, "invalid arguments")
return
}
data := []byte(fmt.Sprintf("%d", req.Sleep))
err := os.WriteFile(OLEDSleepFile, data, 0o644)
if err != nil {
rsp.ErrRsp(c, -2, "failed to write data")
return
}
rsp.OkRsp(c)
log.Debugf("set OLED sleep: %d", req.Sleep)
}
func (s *Service) GetOLED(c *gin.Context) {
var rsp proto.Response
if _, err := os.Stat(OLEDExistFile); err != nil {
rsp.OkRspWithData(c, &proto.GetOLEDRsp{
Exist: false,
Sleep: 0,
})
return
}
data, err := os.ReadFile(OLEDSleepFile)
if err != nil {
rsp.OkRspWithData(c, &proto.GetOLEDRsp{
Exist: true,
Sleep: 0,
})
return
}
content := strings.TrimSpace(string(data))
sleep, err := strconv.Atoi(content)
if err != nil {
log.Errorf("failed to parse OLED: %s", err)
rsp.ErrRsp(c, -1, "failed to parse OLED config")
return
}
rsp.OkRspWithData(c, &proto.GetOLEDRsp{
Exist: true,
Sleep: sleep,
})
log.Debugf("get OLED config successful, sleep %d", sleep)
}

View File

@@ -0,0 +1,76 @@
package vm
import (
"NanoKVM-Server/common"
"fmt"
"os"
"strconv"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"NanoKVM-Server/proto"
)
var screenFileMap = map[string]string{
"type": "/kvmapp/kvm/type",
"fps": "/kvmapp/kvm/fps",
"quality": "/kvmapp/kvm/qlty",
"resolution": "/kvmapp/kvm/res",
}
func (s *Service) SetScreen(c *gin.Context) {
var req proto.SetScreenReq
var rsp proto.Response
err := proto.ParseFormRequest(c, &req)
if err != nil {
rsp.ErrRsp(c, -1, "invalid arguments")
return
}
switch req.Type {
case "type":
data := "h264"
if req.Value == 0 {
data = "mjpeg"
}
err = writeScreen("type", data)
case "gop":
gop := 30
if req.Value >= 1 && req.Value <= 100 {
gop = req.Value
}
common.GetKvmVision().SetGop(uint8(gop))
default:
data := strconv.Itoa(req.Value)
err = writeScreen(req.Type, data)
}
if err != nil {
rsp.ErrRsp(c, -2, "update screen failed")
return
}
common.SetScreen(req.Type, req.Value)
log.Debugf("update screen: %+v", req)
rsp.OkRsp(c)
}
func writeScreen(key string, value string) error {
file, ok := screenFileMap[key]
if !ok {
return fmt.Errorf("invalid argument %s", key)
}
err := os.WriteFile(file, []byte(value), 0o666)
if err != nil {
log.Errorf("write kvm %s failed: %s", file, err)
return err
}
return nil
}

155
server/service/vm/script.go Normal file
View File

@@ -0,0 +1,155 @@
package vm
import (
"fmt"
"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"
func (s *Service) GetScripts(c *gin.Context) {
var rsp proto.Response
var files []string
err := filepath.Walk(ScriptDirectory, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && isScript(info.Name()) {
files = append(files, info.Name())
}
return nil
})
if err != nil {
rsp.ErrRsp(c, -1, "get scripts failed")
return
}
rsp.OkRspWithData(c, &proto.GetScriptsRsp{
Files: files,
})
log.Debugf("get scripts total %d", len(files))
}
func (s *Service) UploadScript(c *gin.Context) {
var rsp proto.Response
_, header, err := c.Request.FormFile("file")
if err != nil {
rsp.ErrRsp(c, -1, "bad request")
return
}
if !isScript(header.Filename) {
rsp.ErrRsp(c, -2, "invalid arguments")
return
}
if _, err = os.Stat(ScriptDirectory); err != nil {
_ = os.MkdirAll(ScriptDirectory, 0o755)
}
target := fmt.Sprintf("%s/%s", ScriptDirectory, header.Filename)
err = c.SaveUploadedFile(header, target)
if err != nil {
rsp.ErrRsp(c, -2, "save failed")
return
}
_ = utils.EnsurePermission(target, 0o100)
data := &proto.UploadScriptRsp{
File: header.Filename,
}
rsp.OkRspWithData(c, data)
log.Debugf("upload script %s success", header.Filename)
}
func (s *Service) RunScript(c *gin.Context) {
var req proto.RunScriptReq
var rsp proto.Response
if err := proto.ParseFormRequest(c, &req); err != nil {
rsp.ErrRsp(c, -1, "invalid arguments")
return
}
command := fmt.Sprintf("%s/%s", ScriptDirectory, req.Name)
name := strings.ToLower(req.Name)
if strings.HasSuffix(name, ".py") {
command = fmt.Sprintf("python %s", command)
}
var output []byte
var err error
cmd := exec.Command("sh", "-c", command)
if req.Type == "foreground" {
output, err = cmd.CombinedOutput()
} else {
cmd.Stdout = nil
cmd.Stderr = nil
go func() {
err := cmd.Run()
if err != nil {
log.Errorf("run script %s in background failed: %s", req.Name, err)
}
}()
}
if err != nil {
log.Errorf("run script %s failed: %s", req.Name, err.Error())
rsp.ErrRsp(c, -2, "run script failed")
return
}
rsp.OkRspWithData(c, &proto.RunScriptRsp{
Log: string(output),
})
log.Debugf("run script %s success", req.Name)
}
func (s *Service) DeleteScript(c *gin.Context) {
var req proto.DeleteScriptReq
var rsp proto.Response
if err := proto.ParseFormRequest(c, &req); err != nil {
rsp.ErrRsp(c, -1, "invalid arguments")
return
}
file := fmt.Sprintf("%s/%s", ScriptDirectory, req.Name)
if err := os.Remove(file); err != nil {
log.Errorf("delete script %s failed: %s", file, err)
rsp.ErrRsp(c, -3, "delete failed")
return
}
rsp.OkRsp(c)
log.Debugf("delete script %s success", file)
}
func isScript(name string) bool {
nameLower := strings.ToLower(name)
if strings.HasSuffix(nameLower, ".sh") || strings.HasSuffix(nameLower, ".py") {
return true
}
return false
}

View File

@@ -0,0 +1,8 @@
package vm
type Service struct {
}
func NewService() *Service {
return &Service{}
}

67
server/service/vm/ssh.go Normal file
View File

@@ -0,0 +1,67 @@
package vm
import (
"NanoKVM-Server/proto"
"errors"
"fmt"
"os"
"os/exec"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
)
const (
SSHScript = "/etc/init.d/S50sshd"
SSHStopFlag = "/etc/kvm/ssh_stop"
)
func (s *Service) GetSSHState(c *gin.Context) {
var rsp proto.Response
enabled := isSSHEnabled()
rsp.OkRspWithData(c, &proto.GetSSHStateRsp{
Enabled: enabled,
})
}
func (s *Service) EnableSSH(c *gin.Context) {
var rsp proto.Response
command := fmt.Sprintf("%s permanent_on", SSHScript)
err := exec.Command("sh", "-c", command).Run()
if err != nil {
log.Errorf("failed to run SSH script: %s", err)
rsp.ErrRsp(c, -1, "operation failed")
return
}
rsp.OkRsp(c)
log.Debugf("SSH enabled")
}
func (s *Service) DisableSSH(c *gin.Context) {
var rsp proto.Response
command := fmt.Sprintf("%s permanent_off", SSHScript)
err := exec.Command("sh", "-c", command).Run()
if err != nil {
log.Errorf("failed to run SSH script: %s", err)
rsp.ErrRsp(c, -1, "operation failed")
return
}
rsp.OkRsp(c)
log.Debugf("SSH disabled")
}
func isSSHEnabled() bool {
_, err := os.Stat(SSHStopFlag)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return true
}
}
return false
}

176
server/service/vm/swap.go Normal file
View File

@@ -0,0 +1,176 @@
package vm
import (
"fmt"
"os"
"os/exec"
"strings"
"time"
"NanoKVM-Server/proto"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
)
const (
SwapFile = "/swapfile"
InittabPath = "/etc/inittab"
TempInittab = "/etc/.inittab.tmp"
)
func (s *Service) GetSwap(c *gin.Context) {
var rsp proto.Response
rsp.OkRspWithData(c, &proto.GetSwapRsp{
Size: getSwapSize(),
})
}
func (s *Service) SetSwap(c *gin.Context) {
var rsp proto.Response
var req proto.SetSwapReq
if err := proto.ParseFormRequest(c, &req); err != nil {
rsp.ErrRsp(c, -1, "invalid arguments")
return
}
size := getSwapSize()
if req.Size == size {
rsp.OkRsp(c)
return
}
if req.Size == 0 {
if err := disableSwap(); err != nil {
rsp.ErrRsp(c, -2, "disable swap failed")
return
}
if err := disableInittab(); err != nil {
rsp.ErrRsp(c, -3, "disable inittab failed")
return
}
} else {
if err := enableSwap(req.Size); err != nil {
rsp.ErrRsp(c, -4, "enable swap failed")
return
}
if err := enableInittab(); err != nil {
rsp.ErrRsp(c, -5, "enable inittab failed")
return
}
}
rsp.OkRsp(c)
}
func getSwapSize() int64 {
fileInfo, err := os.Stat(SwapFile)
if err != nil {
return 0
}
return fileInfo.Size() / 1024 / 1024
}
func enableSwap(size int64) error {
if getSwapSize() > 0 {
if err := disableSwap(); err != nil {
return err
}
}
commands := []string{
fmt.Sprintf("fallocate -l %dM %s", size, SwapFile),
fmt.Sprintf("chmod 600 %s", SwapFile),
fmt.Sprintf("mkswap %s", SwapFile),
fmt.Sprintf("swapon %s", SwapFile),
}
for _, command := range commands {
err := exec.Command("sh", "-c", command).Run()
if err != nil {
log.Errorf("failed to execute %s: %s", command, err)
return err
}
time.Sleep(300 * time.Millisecond)
}
log.Debugf("set swap file size: %d", size)
return nil
}
func disableSwap() error {
command := "swapoff -a"
if err := exec.Command("sh", "-c", command).Run(); err != nil {
log.Errorf("failed to execute swapoff: %s", err)
return err
}
if err := os.Remove(SwapFile); err != nil {
log.Errorf("failed to delete %s: %s", SwapFile, err)
return err
}
return nil
}
func enableInittab() error {
f, err := os.OpenFile(InittabPath, os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
log.Errorf("read inittab failed: %s", err)
return err
}
defer func() {
_ = f.Close()
}()
content := fmt.Sprintf("\nsi11::sysinit:/sbin/swapon %s", SwapFile)
_, err = f.WriteString(content)
if err != nil {
log.Errorf("write inittab failed: %s", err)
return err
}
log.Debugf("write to %s: %s", InittabPath, content)
return nil
}
func disableInittab() error {
defer func() {
_ = os.Remove(TempInittab)
}()
input, err := os.ReadFile(InittabPath)
if err != nil {
log.Errorf("read fstab failed: %s", err)
return err
}
lines := strings.Split(string(input), "\n")
output := make([]string, 0)
for _, line := range lines {
if strings.HasSuffix(line, SwapFile) {
log.Debugf("%s delete line: %s", InittabPath, line)
} else {
output = append(output, line)
}
}
content := strings.Join(output, "\n")
content = strings.TrimSuffix(content, "\n")
if err := os.WriteFile(TempInittab, []byte(content), 0644); err != nil {
log.Errorf("write temp fstab failed: %s", err)
return err
}
if err := os.Rename(TempInittab, InittabPath); err != nil {
log.Errorf("replace fstab failed: %s", err)
return err
}
return nil
}

View File

@@ -0,0 +1,25 @@
package vm
import (
"NanoKVM-Server/proto"
"os/exec"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
)
func (s *Service) Reboot(c *gin.Context) {
var rsp proto.Response
log.Println("reboot system...")
err := exec.Command("reboot").Run()
if err != nil {
rsp.ErrRsp(c, -1, "operation failed")
log.Errorf("failed to reboot: %s", err)
return
}
rsp.OkRsp(c)
log.Debug("system rebooted")
}

View File

@@ -0,0 +1,110 @@
package vm
import (
"encoding/json"
"net/http"
"os"
"os/exec"
"time"
"github.com/creack/pty"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
log "github.com/sirupsen/logrus"
)
const (
messageWait = 10 * time.Second
maxMessageSize = 1024
)
type WinSize struct {
Rows uint16 `json:"rows"`
Cols uint16 `json:"cols"`
}
var upgrader = websocket.Upgrader{
ReadBufferSize: maxMessageSize,
WriteBufferSize: maxMessageSize,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
func (s *Service) Terminal(c *gin.Context) {
ws, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
log.Errorf("failed to init websocket: %s", err)
return
}
defer func() {
_ = ws.Close()
}()
cmd := exec.Command("/bin/sh")
ptmx, err := pty.Start(cmd)
if err != nil {
log.Errorf("failed to start pty: %s", err)
return
}
defer func() {
_ = ptmx.Close()
_ = cmd.Process.Kill()
}()
go wsWrite(ws, ptmx)
wsRead(ws, ptmx)
}
// pty to ws
func wsWrite(ws *websocket.Conn, ptmx *os.File) {
data := make([]byte, maxMessageSize)
for {
n, err := ptmx.Read(data)
if err != nil {
return
}
if n > 0 {
_ = ws.SetWriteDeadline(time.Now().Add(messageWait))
err = ws.WriteMessage(websocket.BinaryMessage, data[:n])
if err != nil {
log.Errorf("write ws message failed: %s", err)
return
}
}
}
}
// ws to pty
func wsRead(ws *websocket.Conn, ptmx *os.File) {
var zeroTime time.Time
_ = ws.SetReadDeadline(zeroTime)
for {
msgType, p, err := ws.ReadMessage()
if err != nil {
return
}
// resize message
if msgType == websocket.BinaryMessage {
var winSize WinSize
if err := json.Unmarshal(p, &winSize); err == nil {
_ = pty.Setsize(ptmx, &pty.Winsize{
Rows: winSize.Rows,
Cols: winSize.Cols,
})
}
continue
}
_, err = ptmx.Write(p)
if err != nil {
log.Errorf("failed to write to pty: %s", err)
return
}
}
}

76
server/service/vm/tls.go Normal file
View File

@@ -0,0 +1,76 @@
package vm
import (
"fmt"
"os/exec"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"NanoKVM-Server/config"
"NanoKVM-Server/proto"
"NanoKVM-Server/utils"
)
func (s *Service) SetTls(c *gin.Context) {
var req proto.SetTlsReq
var rsp proto.Response
err := proto.ParseFormRequest(c, &req)
if err != nil {
rsp.ErrRsp(c, -1, fmt.Sprintf("invalid arguments: %s", err))
return
}
if req.Enabled {
err = enableTls()
} else {
err = disableTls()
}
if err != nil {
log.Errorf("failed to set TLS: %s", err)
rsp.ErrRsp(c, -2, "operation failed")
return
}
rsp.OkRsp(c)
_ = exec.Command("sh", "-c", "/etc/init.d/S95nanokvm restart").Run()
}
func enableTls() error {
if err := utils.GenerateCert(); err != nil {
return err
}
conf, err := config.Read()
if err != nil {
return err
}
conf.Proto = "https"
conf.Cert.Crt = "/etc/kvm/server.crt"
conf.Cert.Key = "/etc/kvm/server.key"
if err := config.Write(conf); err != nil {
return err
}
return nil
}
func disableTls() error {
conf, err := config.Read()
if err != nil {
return err
}
conf.Proto = "http"
if err := config.Write(conf); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,134 @@
package vm
import (
"errors"
"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"
)
var (
mountNetworkCommands = []string{
"touch /boot/usb.rndis0",
"/etc/init.d/S03usbdev stop",
"/etc/init.d/S03usbdev start",
}
unmountNetworkCommands = []string{
"/etc/init.d/S03usbdev stop",
"rm -rf /sys/kernel/config/usb_gadget/g0/configs/c.1/rndis.usb0",
"rm /boot/usb.rndis0",
"/etc/init.d/S03usbdev start",
}
mountDiskCommands = []string{
"touch /boot/usb.disk0",
"/etc/init.d/S03usbdev stop",
"/etc/init.d/S03usbdev start",
}
unmountDiskCommands = []string{
"/etc/init.d/S03usbdev stop",
"rm -rf /sys/kernel/config/usb_gadget/g0/configs/c.1/mass_storage.disk0",
"rm /boot/usb.disk0",
"/etc/init.d/S03usbdev start",
}
)
func (s *Service) GetVirtualDevice(c *gin.Context) {
var rsp proto.Response
network, _ := isDeviceExist(virtualNetwork)
disk, _ := isDeviceExist(virtualDisk)
rsp.OkRspWithData(c, &proto.GetVirtualDeviceRsp{
Network: network,
Disk: disk,
})
log.Debugf("get virtual device success")
}
func (s *Service) UpdateVirtualDevice(c *gin.Context) {
var req proto.UpdateVirtualDeviceReq
var rsp proto.Response
if err := proto.ParseFormRequest(c, &req); err != nil {
rsp.ErrRsp(c, -1, "invalid argument")
return
}
var device string
var commands []string
switch req.Device {
case "network":
device = virtualNetwork
exist, _ := isDeviceExist(device)
if !exist {
commands = mountNetworkCommands
} else {
commands = unmountNetworkCommands
}
case "disk":
device = virtualDisk
exist, _ := isDeviceExist(device)
if !exist {
commands = mountDiskCommands
} else {
commands = unmountDiskCommands
}
default:
rsp.ErrRsp(c, -2, "invalid arguments")
return
}
h := hid.GetHid()
h.Lock()
h.CloseNoLock()
defer func() {
h.OpenNoLock()
h.Unlock()
}()
for _, command := range commands {
err := exec.Command("sh", "-c", command).Run()
if err != nil {
rsp.ErrRsp(c, -3, "operation failed")
return
}
}
on, _ := isDeviceExist(device)
rsp.OkRspWithData(c, &proto.UpdateVirtualDeviceRsp{
On: on,
})
log.Debugf("update virtual device %s success", req.Device)
}
func isDeviceExist(device string) (bool, error) {
_, err := os.Stat(device)
if err == nil {
return true, nil
}
if errors.Is(err, os.ErrNotExist) {
return false, nil
}
log.Errorf("check file %s err: %s", device, err)
return false, err
}

View File

@@ -0,0 +1,58 @@
package vm
import (
"os"
"strings"
"NanoKVM-Server/proto"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
)
const (
WebTitleFile = "/etc/kvm/web-title"
)
func (s *Service) SetWebTitle(c *gin.Context) {
var req proto.SetWebTitleReq
var rsp proto.Response
if err := proto.ParseFormRequest(c, &req); err != nil {
rsp.ErrRsp(c, -1, "invalid arguments")
return
}
if req.Title == "" || req.Title == "BatchuKVM" {
err := os.Remove(WebTitleFile)
if err != nil {
rsp.ErrRsp(c, -2, "reset failed")
return
}
} else {
err := os.WriteFile(WebTitleFile, []byte(req.Title), 0o644)
if err != nil {
rsp.ErrRsp(c, -3, "write failed")
return
}
}
rsp.OkRsp(c)
log.Debugf("set web title: %s", req.Title)
}
func (s *Service) GetWebTitle(c *gin.Context) {
var rsp proto.Response
data, err := os.ReadFile(WebTitleFile)
if err != nil {
rsp.ErrRsp(c, -1, "read web title failed")
return
}
rsp.OkRspWithData(c, &proto.GetWebTitleRsp{
Title: strings.Replace(string(data), "\n", "", -1),
})
log.Debugf("get web title successful")
}