forked from jetkvm/kvm
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathusb_mass_storage.go
558 lines (481 loc) · 13.9 KB
/
usb_mass_storage.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
package kvm
import (
"encoding/json"
"errors"
"fmt"
"io"
"kvm/resource"
"log"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"sync"
"syscall"
"time"
"github.com/gin-gonic/gin"
"github.com/psanford/httpreadat"
"github.com/google/uuid"
"github.com/pion/webrtc/v4"
)
const massStorageName = "mass_storage.usb0"
var massStorageFunctionPath = path.Join(gadgetPath, "jetkvm", "functions", massStorageName)
func writeFile(path string, data string) error {
return os.WriteFile(path, []byte(data), 0644)
}
func setMassStorageImage(imagePath string) error {
err := writeFile(path.Join(massStorageFunctionPath, "lun.0", "file"), imagePath)
if err != nil {
return fmt.Errorf("failed to set image path: %w", err)
}
return nil
}
func setMassStorageMode(cdrom bool) error {
mode := "0"
if cdrom {
mode = "1"
}
err := writeFile(path.Join(massStorageFunctionPath, "lun.0", "cdrom"), mode)
if err != nil {
return fmt.Errorf("failed to set cdrom mode: %w", err)
}
return nil
}
func onDiskMessage(msg webrtc.DataChannelMessage) {
fmt.Println("Disk Message, len:", len(msg.Data))
diskReadChan <- msg.Data
}
func mountImage(imagePath string) error {
err := setMassStorageImage("")
if err != nil {
return fmt.Errorf("Remove Mass Storage Image Error", err)
}
err = setMassStorageImage(imagePath)
if err != nil {
return fmt.Errorf("Set Mass Storage Image Error", err)
}
return nil
}
var nbdDevice *NBDDevice
const imagesFolder = "/userdata/jetkvm/images"
func rpcMountBuiltInImage(filename string) error {
log.Println("Mount Built-In Image", 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) {
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 {
fmt.Println("Remove Mass Storage Image Error", 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"})
}