mirror of https://github.com/jetkvm/kvm.git
566 lines
14 KiB
Go
566 lines
14 KiB
Go
package kvm
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
"github.com/pion/webrtc/v4"
|
|
"github.com/psanford/httpreadat"
|
|
|
|
"github.com/jetkvm/kvm/resource"
|
|
)
|
|
|
|
func writeFile(path string, data string) error {
|
|
return os.WriteFile(path, []byte(data), 0644)
|
|
}
|
|
|
|
func setMassStorageImage(imagePath string) error {
|
|
massStorageFunctionPath, err := gadget.GetPath("mass_storage_lun0")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get mass storage path: %w", err)
|
|
}
|
|
|
|
if err := writeFile(path.Join(massStorageFunctionPath, "file"), imagePath); err != nil {
|
|
return fmt.Errorf("failed to set image path: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func setMassStorageMode(cdrom bool) error {
|
|
massStorageFunctionPath, err := gadget.GetPath("mass_storage_lun0")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get mass storage path: %w", err)
|
|
}
|
|
|
|
mode := "0"
|
|
if cdrom {
|
|
mode = "1"
|
|
}
|
|
if err := writeFile(path.Join(massStorageFunctionPath, "lun.0", "cdrom"), mode); err != nil {
|
|
return fmt.Errorf("failed to set cdrom mode: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func onDiskMessage(msg webrtc.DataChannelMessage) {
|
|
logger.Infof("Disk Message, len: %d", len(msg.Data))
|
|
diskReadChan <- msg.Data
|
|
}
|
|
|
|
func mountImage(imagePath string) error {
|
|
err := setMassStorageImage("")
|
|
if err != nil {
|
|
return fmt.Errorf("Remove Mass Storage Image Error: %w", err)
|
|
}
|
|
err = setMassStorageImage(imagePath)
|
|
if err != nil {
|
|
return fmt.Errorf("Set Mass Storage Image Error: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var nbdDevice *NBDDevice
|
|
|
|
const imagesFolder = "/userdata/jetkvm/images"
|
|
|
|
func rpcMountBuiltInImage(filename string) error {
|
|
logger.Infof("Mount Built-In Image: %s", filename)
|
|
_ = os.MkdirAll(imagesFolder, 0755)
|
|
imagePath := filepath.Join(imagesFolder, filename)
|
|
|
|
// Check if the file exists in the imagesFolder
|
|
if _, err := os.Stat(imagePath); err == nil {
|
|
return mountImage(imagePath)
|
|
}
|
|
|
|
// If not, try to find it in ResourceFS
|
|
file, err := resource.ResourceFS.Open(filename)
|
|
if err != nil {
|
|
return fmt.Errorf("image %s not found in built-in resources: %w", filename, err)
|
|
}
|
|
defer file.Close()
|
|
|
|
// Create the file in imagesFolder
|
|
outFile, err := os.Create(imagePath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create image file: %w", err)
|
|
}
|
|
defer outFile.Close()
|
|
|
|
// Copy the content
|
|
_, err = io.Copy(outFile, file)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to write image file: %w", err)
|
|
}
|
|
|
|
// Mount the newly created image
|
|
return mountImage(imagePath)
|
|
}
|
|
|
|
func getMassStorageMode() (bool, error) {
|
|
massStorageFunctionPath, err := gadget.GetPath("mass_storage_lun0")
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to get mass storage path: %w", err)
|
|
}
|
|
|
|
data, err := os.ReadFile(path.Join(massStorageFunctionPath, "lun.0", "cdrom"))
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to read cdrom mode: %w", err)
|
|
}
|
|
|
|
// Trim any whitespace characters. It has a newline at the end
|
|
trimmedData := strings.TrimSpace(string(data))
|
|
|
|
return trimmedData == "1", nil
|
|
}
|
|
|
|
type VirtualMediaUrlInfo struct {
|
|
Usable bool
|
|
Reason string //only populated if Usable is false
|
|
Size int64
|
|
}
|
|
|
|
func rpcCheckMountUrl(url string) (*VirtualMediaUrlInfo, error) {
|
|
return nil, errors.New("not implemented")
|
|
}
|
|
|
|
type VirtualMediaSource string
|
|
|
|
const (
|
|
WebRTC VirtualMediaSource = "WebRTC"
|
|
HTTP VirtualMediaSource = "HTTP"
|
|
Storage VirtualMediaSource = "Storage"
|
|
)
|
|
|
|
type VirtualMediaMode string
|
|
|
|
const (
|
|
CDROM VirtualMediaMode = "CDROM"
|
|
Disk VirtualMediaMode = "Disk"
|
|
)
|
|
|
|
type VirtualMediaState struct {
|
|
Source VirtualMediaSource `json:"source"`
|
|
Mode VirtualMediaMode `json:"mode"`
|
|
Filename string `json:"filename,omitempty"`
|
|
URL string `json:"url,omitempty"`
|
|
Size int64 `json:"size"`
|
|
}
|
|
|
|
var currentVirtualMediaState *VirtualMediaState
|
|
var virtualMediaStateMutex sync.RWMutex
|
|
|
|
func rpcGetVirtualMediaState() (*VirtualMediaState, error) {
|
|
virtualMediaStateMutex.RLock()
|
|
defer virtualMediaStateMutex.RUnlock()
|
|
return currentVirtualMediaState, nil
|
|
}
|
|
|
|
func rpcUnmountImage() error {
|
|
virtualMediaStateMutex.Lock()
|
|
defer virtualMediaStateMutex.Unlock()
|
|
err := setMassStorageImage("\n")
|
|
if err != nil {
|
|
logger.Warnf("Remove Mass Storage Image Error: %v", err)
|
|
}
|
|
//TODO: check if we still need it
|
|
time.Sleep(500 * time.Millisecond)
|
|
if nbdDevice != nil {
|
|
nbdDevice.Close()
|
|
nbdDevice = nil
|
|
}
|
|
currentVirtualMediaState = nil
|
|
return nil
|
|
}
|
|
|
|
var httpRangeReader *httpreadat.RangeReader
|
|
|
|
func rpcMountWithHTTP(url string, mode VirtualMediaMode) error {
|
|
virtualMediaStateMutex.Lock()
|
|
if currentVirtualMediaState != nil {
|
|
virtualMediaStateMutex.Unlock()
|
|
return fmt.Errorf("another virtual media is already mounted")
|
|
}
|
|
httpRangeReader = httpreadat.New(url)
|
|
n, err := httpRangeReader.Size()
|
|
if err != nil {
|
|
virtualMediaStateMutex.Unlock()
|
|
return fmt.Errorf("failed to use http url: %w", err)
|
|
}
|
|
logger.Infof("using remote url %s with size %d", url, n)
|
|
currentVirtualMediaState = &VirtualMediaState{
|
|
Source: HTTP,
|
|
Mode: mode,
|
|
URL: url,
|
|
Size: n,
|
|
}
|
|
virtualMediaStateMutex.Unlock()
|
|
|
|
logger.Debug("Starting nbd device")
|
|
nbdDevice = NewNBDDevice()
|
|
err = nbdDevice.Start()
|
|
if err != nil {
|
|
logger.Errorf("failed to start nbd device: %v", err)
|
|
return err
|
|
}
|
|
logger.Debug("nbd device started")
|
|
//TODO: replace by polling on block device having right size
|
|
time.Sleep(1 * time.Second)
|
|
err = setMassStorageImage("/dev/nbd0")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
logger.Info("usb mass storage mounted")
|
|
return nil
|
|
}
|
|
|
|
func rpcMountWithWebRTC(filename string, size int64, mode VirtualMediaMode) error {
|
|
virtualMediaStateMutex.Lock()
|
|
if currentVirtualMediaState != nil {
|
|
virtualMediaStateMutex.Unlock()
|
|
return fmt.Errorf("another virtual media is already mounted")
|
|
}
|
|
currentVirtualMediaState = &VirtualMediaState{
|
|
Source: WebRTC,
|
|
Mode: mode,
|
|
Filename: filename,
|
|
Size: size,
|
|
}
|
|
virtualMediaStateMutex.Unlock()
|
|
logger.Debugf("currentVirtualMediaState is %v", currentVirtualMediaState)
|
|
logger.Debug("Starting nbd device")
|
|
nbdDevice = NewNBDDevice()
|
|
err := nbdDevice.Start()
|
|
if err != nil {
|
|
logger.Errorf("failed to start nbd device: %v", err)
|
|
return err
|
|
}
|
|
logger.Debug("nbd device started")
|
|
//TODO: replace by polling on block device having right size
|
|
time.Sleep(1 * time.Second)
|
|
err = setMassStorageImage("/dev/nbd0")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
logger.Info("usb mass storage mounted")
|
|
return nil
|
|
}
|
|
|
|
func rpcMountWithStorage(filename string, mode VirtualMediaMode) error {
|
|
filename, err := sanitizeFilename(filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
virtualMediaStateMutex.Lock()
|
|
defer virtualMediaStateMutex.Unlock()
|
|
if currentVirtualMediaState != nil {
|
|
return fmt.Errorf("another virtual media is already mounted")
|
|
}
|
|
|
|
fullPath := filepath.Join(imagesFolder, filename)
|
|
fileInfo, err := os.Stat(fullPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get file info: %w", err)
|
|
}
|
|
|
|
err = setMassStorageImage(fullPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to set mass storage image: %w", err)
|
|
}
|
|
currentVirtualMediaState = &VirtualMediaState{
|
|
Source: Storage,
|
|
Mode: mode,
|
|
Filename: filename,
|
|
Size: fileInfo.Size(),
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type StorageSpace struct {
|
|
BytesUsed int64 `json:"bytesUsed"`
|
|
BytesFree int64 `json:"bytesFree"`
|
|
}
|
|
|
|
func rpcGetStorageSpace() (*StorageSpace, error) {
|
|
var stat syscall.Statfs_t
|
|
err := syscall.Statfs(imagesFolder, &stat)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get storage stats: %v", err)
|
|
}
|
|
|
|
totalSpace := stat.Blocks * uint64(stat.Bsize)
|
|
freeSpace := stat.Bfree * uint64(stat.Bsize)
|
|
usedSpace := totalSpace - freeSpace
|
|
|
|
return &StorageSpace{
|
|
BytesUsed: int64(usedSpace),
|
|
BytesFree: int64(freeSpace),
|
|
}, nil
|
|
}
|
|
|
|
type StorageFile struct {
|
|
Filename string `json:"filename"`
|
|
Size int64 `json:"size"`
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
}
|
|
|
|
type StorageFiles struct {
|
|
Files []StorageFile `json:"files"`
|
|
}
|
|
|
|
func rpcListStorageFiles() (*StorageFiles, error) {
|
|
files, err := os.ReadDir(imagesFolder)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read directory: %v", err)
|
|
}
|
|
|
|
storageFiles := make([]StorageFile, 0)
|
|
for _, file := range files {
|
|
if file.IsDir() {
|
|
continue
|
|
}
|
|
|
|
info, err := file.Info()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get file info: %v", err)
|
|
}
|
|
|
|
storageFiles = append(storageFiles, StorageFile{
|
|
Filename: file.Name(),
|
|
Size: info.Size(),
|
|
CreatedAt: info.ModTime(),
|
|
})
|
|
}
|
|
|
|
return &StorageFiles{Files: storageFiles}, nil
|
|
}
|
|
|
|
func sanitizeFilename(filename string) (string, error) {
|
|
cleanPath := filepath.Clean(filename)
|
|
if filepath.IsAbs(cleanPath) || strings.Contains(cleanPath, "..") {
|
|
return "", errors.New("invalid filename")
|
|
}
|
|
sanitized := filepath.Base(cleanPath)
|
|
if sanitized == "." || sanitized == string(filepath.Separator) {
|
|
return "", errors.New("invalid filename")
|
|
}
|
|
return sanitized, nil
|
|
}
|
|
|
|
func rpcDeleteStorageFile(filename string) error {
|
|
sanitizedFilename, err := sanitizeFilename(filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fullPath := filepath.Join(imagesFolder, sanitizedFilename)
|
|
|
|
if _, err := os.Stat(fullPath); os.IsNotExist(err) {
|
|
return fmt.Errorf("file does not exist: %s", filename)
|
|
}
|
|
|
|
err = os.Remove(fullPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete file: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type StorageFileUpload struct {
|
|
AlreadyUploadedBytes int64 `json:"alreadyUploadedBytes"`
|
|
DataChannel string `json:"dataChannel"`
|
|
}
|
|
|
|
const uploadIdPrefix = "upload_"
|
|
|
|
func rpcStartStorageFileUpload(filename string, size int64) (*StorageFileUpload, error) {
|
|
sanitizedFilename, err := sanitizeFilename(filename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
filePath := path.Join(imagesFolder, sanitizedFilename)
|
|
uploadPath := filePath + ".incomplete"
|
|
|
|
if _, err := os.Stat(filePath); err == nil {
|
|
return nil, fmt.Errorf("file already exists: %s", sanitizedFilename)
|
|
}
|
|
|
|
var alreadyUploadedBytes int64 = 0
|
|
if stat, err := os.Stat(uploadPath); err == nil {
|
|
alreadyUploadedBytes = stat.Size()
|
|
}
|
|
|
|
uploadId := uploadIdPrefix + uuid.New().String()
|
|
file, err := os.OpenFile(uploadPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open file for upload: %v", err)
|
|
}
|
|
pendingUploadsMutex.Lock()
|
|
pendingUploads[uploadId] = pendingUpload{
|
|
File: file,
|
|
Size: size,
|
|
AlreadyUploadedBytes: alreadyUploadedBytes,
|
|
}
|
|
pendingUploadsMutex.Unlock()
|
|
return &StorageFileUpload{
|
|
AlreadyUploadedBytes: alreadyUploadedBytes,
|
|
DataChannel: uploadId,
|
|
}, nil
|
|
}
|
|
|
|
type pendingUpload struct {
|
|
File *os.File
|
|
Size int64
|
|
AlreadyUploadedBytes int64
|
|
}
|
|
|
|
var pendingUploads = make(map[string]pendingUpload)
|
|
var pendingUploadsMutex sync.Mutex
|
|
|
|
type UploadProgress struct {
|
|
Size int64
|
|
AlreadyUploadedBytes int64
|
|
}
|
|
|
|
func handleUploadChannel(d *webrtc.DataChannel) {
|
|
defer d.Close()
|
|
uploadId := d.Label()
|
|
pendingUploadsMutex.Lock()
|
|
pendingUpload, ok := pendingUploads[uploadId]
|
|
pendingUploadsMutex.Unlock()
|
|
if !ok {
|
|
logger.Warnf("upload channel opened for unknown upload: %s", uploadId)
|
|
return
|
|
}
|
|
totalBytesWritten := pendingUpload.AlreadyUploadedBytes
|
|
defer func() {
|
|
pendingUpload.File.Close()
|
|
if totalBytesWritten == pendingUpload.Size {
|
|
newName := strings.TrimSuffix(pendingUpload.File.Name(), ".incomplete")
|
|
err := os.Rename(pendingUpload.File.Name(), newName)
|
|
if err != nil {
|
|
logger.Errorf("failed to rename uploaded file: %v", err)
|
|
} else {
|
|
logger.Debugf("successfully renamed uploaded file to: %s", newName)
|
|
}
|
|
} else {
|
|
logger.Warnf("uploaded ended before the complete file received")
|
|
}
|
|
pendingUploadsMutex.Lock()
|
|
delete(pendingUploads, uploadId)
|
|
pendingUploadsMutex.Unlock()
|
|
}()
|
|
uploadComplete := make(chan struct{})
|
|
lastProgressTime := time.Now()
|
|
d.OnMessage(func(msg webrtc.DataChannelMessage) {
|
|
bytesWritten, err := pendingUpload.File.Write(msg.Data)
|
|
if err != nil {
|
|
logger.Errorf("failed to write to file: %v", err)
|
|
close(uploadComplete)
|
|
return
|
|
}
|
|
totalBytesWritten += int64(bytesWritten)
|
|
|
|
sendProgress := false
|
|
if time.Since(lastProgressTime) >= 200*time.Millisecond {
|
|
sendProgress = true
|
|
}
|
|
if totalBytesWritten >= pendingUpload.Size {
|
|
sendProgress = true
|
|
close(uploadComplete)
|
|
}
|
|
|
|
if sendProgress {
|
|
progress := UploadProgress{
|
|
Size: pendingUpload.Size,
|
|
AlreadyUploadedBytes: totalBytesWritten,
|
|
}
|
|
progressJSON, err := json.Marshal(progress)
|
|
if err != nil {
|
|
logger.Errorf("failed to marshal upload progress: %v", err)
|
|
} else {
|
|
err = d.SendText(string(progressJSON))
|
|
if err != nil {
|
|
logger.Errorf("failed to send upload progress: %v", err)
|
|
}
|
|
}
|
|
lastProgressTime = time.Now()
|
|
}
|
|
})
|
|
|
|
// Block until upload is complete
|
|
<-uploadComplete
|
|
}
|
|
|
|
func handleUploadHttp(c *gin.Context) {
|
|
uploadId := c.Query("uploadId")
|
|
pendingUploadsMutex.Lock()
|
|
pendingUpload, ok := pendingUploads[uploadId]
|
|
pendingUploadsMutex.Unlock()
|
|
if !ok {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Upload not found"})
|
|
return
|
|
}
|
|
|
|
totalBytesWritten := pendingUpload.AlreadyUploadedBytes
|
|
defer func() {
|
|
pendingUpload.File.Close()
|
|
if totalBytesWritten == pendingUpload.Size {
|
|
newName := strings.TrimSuffix(pendingUpload.File.Name(), ".incomplete")
|
|
err := os.Rename(pendingUpload.File.Name(), newName)
|
|
if err != nil {
|
|
logger.Errorf("failed to rename uploaded file: %v", err)
|
|
} else {
|
|
logger.Debugf("successfully renamed uploaded file to: %s", newName)
|
|
}
|
|
} else {
|
|
logger.Warnf("uploaded ended before the complete file received")
|
|
}
|
|
pendingUploadsMutex.Lock()
|
|
delete(pendingUploads, uploadId)
|
|
pendingUploadsMutex.Unlock()
|
|
}()
|
|
|
|
reader := c.Request.Body
|
|
buffer := make([]byte, 32*1024)
|
|
for {
|
|
n, err := reader.Read(buffer)
|
|
if err != nil && err != io.EOF {
|
|
logger.Errorf("failed to read from request body: %v", err)
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read upload data"})
|
|
return
|
|
}
|
|
|
|
if n > 0 {
|
|
bytesWritten, err := pendingUpload.File.Write(buffer[:n])
|
|
if err != nil {
|
|
logger.Errorf("failed to write to file: %v", err)
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to write upload data"})
|
|
return
|
|
}
|
|
totalBytesWritten += int64(bytesWritten)
|
|
}
|
|
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "Upload completed"})
|
|
}
|