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

100
server/utils/cert.go Normal file
View File

@@ -0,0 +1,100 @@
package utils
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"net"
"os"
"time"
log "github.com/sirupsen/logrus"
)
func GenerateCert() error {
var (
host = "localhost"
ipAddress = []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")}
validFor = time.Hour * 24 * 365 * 10
certFile = "/etc/kvm/server.crt"
keyFile = "/etc/kvm/server.key"
)
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
log.Errorf("failed to generate RSA private key: %v", err)
return err
}
publicKey := &privateKey.PublicKey
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
log.Errorf("failed to generate serial number: %v", err)
return err
}
template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
CommonName: host,
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(validFor),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
IsCA: false,
DNSNames: []string{host},
IPAddresses: ipAddress,
}
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey, privateKey)
if err != nil {
log.Errorf("failed to create certificate: %v", err)
return err
}
// generate certificate
certOut, err := os.Create(certFile)
if err != nil {
log.Errorf("failed to create %s: %v", certFile, err)
return err
}
if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
log.Errorf("failed to encode %s: %v", certFile, err)
return err
}
_ = certOut.Sync()
_ = certOut.Close()
log.Debugf("%s generated", certFile)
// generate private key
keyOut, err := os.OpenFile(keyFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) // 权限 0600
if err != nil {
log.Errorf("failed to create %s: %v", keyFile, err)
return err
}
privateBytes, err := x509.MarshalPKCS8PrivateKey(privateKey)
if err != nil {
log.Errorf("failed to marshal private key: %v", err)
return err
}
if err := pem.Encode(keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: privateBytes}); err != nil {
log.Errorf("failed to encode %s: %v", keyFile, err)
return err
}
_ = keyOut.Sync()
_ = keyOut.Close()
log.Debugf("%s generated", keyFile)
return nil
}

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

30
server/utils/encrypt.go Normal file
View File

@@ -0,0 +1,30 @@
package utils
import (
"net/url"
"github.com/mervick/aes-everywhere/go/aes256"
log "github.com/sirupsen/logrus"
)
// SecretKey is only used to prevent the data from being transmitted in plaintext.
const SecretKey = "NanoKVM-KOREA-TestKey-2512092155"
func Decrypt(ciphertext string) (string, error) {
if ciphertext == "" {
return "", nil
}
decrypt := aes256.Decrypt(ciphertext, SecretKey)
return decrypt, nil
}
func DecodeDecrypt(data string) (string, error) {
ciphertext, err := url.QueryUnescape(data)
if err != nil {
log.Errorf("decode ciphertext failed: %s", err)
return "", err
}
return Decrypt(ciphertext)
}

38
server/utils/hdmi.go Normal file
View File

@@ -0,0 +1,38 @@
package utils
import (
"os"
log "github.com/sirupsen/logrus"
)
const (
HDMIDisableFile = "/etc/kvm/hdmi_disable"
)
func PersistHDMIDisabled() {
f, err := os.OpenFile(HDMIDisableFile, os.O_CREATE|os.O_RDONLY, 0644)
if err != nil {
log.Error("failed to create hdmi disable file:", err)
return
}
f.Close()
}
func PersistHDMIEnabled() {
if err := os.Remove(HDMIDisableFile); err != nil {
log.Error("failed to remove hdmi disable file:", err)
return
}
}
func IsHdmiDisabled() bool {
if _, err := os.Stat(HDMIDisableFile); err != nil {
if os.IsNotExist(err) {
return false // HDMI is enabled
}
log.Error("failed to check hdmi disable file:", err)
return false // Assume HDMI is enabled on error
}
return true // HDMI is disabled
}

56
server/utils/http.go Normal file
View File

@@ -0,0 +1,56 @@
package utils
import (
"errors"
"io"
"net/http"
"os"
"path/filepath"
log "github.com/sirupsen/logrus"
)
func Download(req *http.Request, target string) error {
log.Debugf("downloading %s to %s", req.URL.String(), target)
err := os.MkdirAll(filepath.Dir(target), 0o755)
if err != nil {
log.Errorf("create dir %s err: %s", filepath.Dir(target), err)
return err
}
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("request error: %s", err)
return err
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != http.StatusOK {
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" && contentType != "application/gzip" {
log.Debugf("unexpected content-type, it should be either octet-stream or (g)zip, but got: %s", contentType)
return errors.New("unsupported content type")
}
_, err = io.Copy(out, resp.Body)
if err != nil {
log.Errorf("download file to %s err: %s", target, err)
return err
}
return nil
}

77
server/utils/memory.go Normal file
View File

@@ -0,0 +1,77 @@
package utils
import (
"fmt"
"os"
"runtime/debug"
"strconv"
"strings"
log "github.com/sirupsen/logrus"
)
const GoMemLimitFile = "/etc/kvm/GOMEMLIMIT"
func InitGoMemLimit() {
if !IsGoMemLimitExist() {
return
}
limit, err := GetGoMemLimit()
if err != nil {
return
}
debug.SetMemoryLimit(limit * 1024 * 1024)
log.Debugf("set GOMEMLIMIT to %d MB", limit)
}
func SetGoMemLimit(limit int64) error {
memoryLimit := max(limit, 50)
debug.SetMemoryLimit(memoryLimit * 1024 * 1024)
log.Debugf("set GOMEMLIMIT to %d MB", limit)
data := []byte(fmt.Sprintf("%d", limit))
err := os.WriteFile(GoMemLimitFile, data, 0o644)
if err != nil {
log.Errorf("failed to write GOMEMLIMIT: %s", err)
return err
}
return nil
}
func GetGoMemLimit() (int64, error) {
data, err := os.ReadFile(GoMemLimitFile)
if err != nil {
log.Errorf("failed to read GOMEMLIMIT: %s", err)
return 0, err
}
content := strings.TrimSpace(string(data))
limit, err := strconv.ParseInt(content, 10, 64)
if err != nil {
log.Errorf("failed to parse GOMEMLIMIT: %s", err)
return 0, err
}
return limit, nil
}
func DelGoMemLimit() error {
debug.SetMemoryLimit(1024 * 1024 * 1024)
err := os.Remove(GoMemLimitFile)
if err != nil {
log.Errorf("failed to delete GOMEMLIMIT: %s", err)
return err
}
return nil
}
func IsGoMemLimitExist() bool {
_, err := os.Stat(GoMemLimitFile)
return err == nil
}

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

View File

@@ -0,0 +1,48 @@
package utils
import "os"
func HasPermission(filePath string, perm os.FileMode) (bool, error) {
fileInfo, err := os.Stat(filePath)
if err != nil {
return false, err
}
mode := fileInfo.Mode().Perm()
if mode&perm == perm {
return true, nil
}
return false, nil
}
func AddPermission(filePath string, perm os.FileMode) error {
fileInfo, err := os.Stat(filePath)
if err != nil {
return err
}
mode := fileInfo.Mode() | perm
err = os.Chmod(filePath, mode)
if err != nil {
return err
}
return nil
}
func EnsurePermission(filePath string, perm os.FileMode) error {
hasPerm, err := HasPermission(filePath, perm)
if err != nil {
return err
}
if !hasPerm {
err = AddPermission(filePath, perm)
if err != nil {
return err
}
}
return nil
}

82
server/utils/untar.go Normal file
View File

@@ -0,0 +1,82 @@
package utils
import (
"archive/tar"
"compress/gzip"
"io"
"os"
"path/filepath"
"strings"
)
func UnTarGz(srcFile string, destDir string) (string, error) {
if err := os.MkdirAll(destDir, 0755); err != nil {
return "", err
}
fr, err := os.Open(srcFile)
if err != nil {
return "", err
}
defer func() {
_ = fr.Close()
}()
gr, err := gzip.NewReader(fr)
if err != nil {
return "", err
}
defer func() {
_ = gr.Close()
}()
tr := tar.NewReader(gr)
targetFile := ""
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return "", err
}
if targetFile == "" {
parts := strings.Split(header.Name, "/")
if len(parts) > 0 {
targetFile = filepath.Join(destDir, parts[0])
}
}
filename := filepath.Join(destDir, header.Name)
switch header.Typeflag {
case tar.TypeDir:
if err := os.MkdirAll(filename, os.FileMode(header.Mode)); err != nil {
return "", err
}
case tar.TypeReg:
file, err := os.OpenFile(filename, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
if err != nil {
return "", err
}
if _, err := io.Copy(file, tr); err != nil {
_ = file.Close()
return "", err
}
_ = file.Close()
case tar.TypeSymlink:
if err := os.Symlink(header.Linkname, filename); err != nil {
return "", err
}
}
}
return targetFile, nil
}

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
}