heyvm 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +621 -0
- package/bin/file-browser +0 -0
- package/bin/heyvm +0 -0
- package/bin/heyvm-core +0 -0
- package/bin/heyvm.js +61 -0
- package/core/README.md +51 -0
- package/core/cmd/heyvm-core/main.go +73 -0
- package/core/go.mod +25 -0
- package/core/go.sum +47 -0
- package/core/internal/auth/errors.go +29 -0
- package/core/internal/auth/password.go +187 -0
- package/core/internal/auth/provider.go +33 -0
- package/core/internal/auth/ssh_key.go +142 -0
- package/core/internal/config/config.go +111 -0
- package/core/internal/ipc/actions.go +738 -0
- package/core/internal/ipc/handler.go +281 -0
- package/core/internal/ipc/protocol.go +126 -0
- package/core/internal/sftp/errors.go +29 -0
- package/core/internal/sftp/manager.go +303 -0
- package/core/internal/sftp/types.go +30 -0
- package/core/internal/ssh/errors.go +23 -0
- package/core/internal/ssh/manager.go +226 -0
- package/core/internal/ssh/session.go +105 -0
- package/core/internal/vm/errors.go +35 -0
- package/core/internal/vm/models.go +84 -0
- package/core/internal/vm/registry.go +240 -0
- package/package.json +59 -0
- package/scripts/install.js +100 -0
- package/ui/README.md +43 -0
- package/ui/dist/App.d.ts +3 -0
- package/ui/dist/App.d.ts.map +1 -0
- package/ui/dist/App.js +142 -0
- package/ui/dist/App.js.map +1 -0
- package/ui/dist/components/ConfirmDialog.d.ts +9 -0
- package/ui/dist/components/ConfirmDialog.d.ts.map +1 -0
- package/ui/dist/components/ConfirmDialog.js +22 -0
- package/ui/dist/components/ConfirmDialog.js.map +1 -0
- package/ui/dist/components/ErrorMessage.d.ts +8 -0
- package/ui/dist/components/ErrorMessage.d.ts.map +1 -0
- package/ui/dist/components/ErrorMessage.js +10 -0
- package/ui/dist/components/ErrorMessage.js.map +1 -0
- package/ui/dist/components/Header.d.ts +8 -0
- package/ui/dist/components/Header.d.ts.map +1 -0
- package/ui/dist/components/Header.js +10 -0
- package/ui/dist/components/Header.js.map +1 -0
- package/ui/dist/components/LoadingSpinner.d.ts +7 -0
- package/ui/dist/components/LoadingSpinner.d.ts.map +1 -0
- package/ui/dist/components/LoadingSpinner.js +7 -0
- package/ui/dist/components/LoadingSpinner.js.map +1 -0
- package/ui/dist/components/StatusBar.d.ts +11 -0
- package/ui/dist/components/StatusBar.d.ts.map +1 -0
- package/ui/dist/components/StatusBar.js +11 -0
- package/ui/dist/components/StatusBar.js.map +1 -0
- package/ui/dist/core/ipc.d.ts +96 -0
- package/ui/dist/core/ipc.d.ts.map +1 -0
- package/ui/dist/core/ipc.js +310 -0
- package/ui/dist/core/ipc.js.map +1 -0
- package/ui/dist/core/types.d.ts +45 -0
- package/ui/dist/core/types.d.ts.map +1 -0
- package/ui/dist/core/types.js +3 -0
- package/ui/dist/core/types.js.map +1 -0
- package/ui/dist/hooks/useFiles.d.ts +14 -0
- package/ui/dist/hooks/useFiles.d.ts.map +1 -0
- package/ui/dist/hooks/useFiles.js +102 -0
- package/ui/dist/hooks/useFiles.js.map +1 -0
- package/ui/dist/hooks/useVM.d.ts +10 -0
- package/ui/dist/hooks/useVM.d.ts.map +1 -0
- package/ui/dist/hooks/useVM.js +54 -0
- package/ui/dist/hooks/useVM.js.map +1 -0
- package/ui/dist/hooks/useVMList.d.ts +10 -0
- package/ui/dist/hooks/useVMList.d.ts.map +1 -0
- package/ui/dist/hooks/useVMList.js +56 -0
- package/ui/dist/hooks/useVMList.js.map +1 -0
- package/ui/dist/index.d.ts +3 -0
- package/ui/dist/index.d.ts.map +1 -0
- package/ui/dist/index.js +7488 -0
- package/ui/dist/index.js.map +1 -0
- package/ui/dist/keybindings.d.ts +146 -0
- package/ui/dist/keybindings.d.ts.map +1 -0
- package/ui/dist/keybindings.js +96 -0
- package/ui/dist/keybindings.js.map +1 -0
- package/ui/dist/screens/AddVMScreen.d.ts +9 -0
- package/ui/dist/screens/AddVMScreen.d.ts.map +1 -0
- package/ui/dist/screens/AddVMScreen.js +163 -0
- package/ui/dist/screens/AddVMScreen.js.map +1 -0
- package/ui/dist/screens/VMDetailScreen.d.ts +12 -0
- package/ui/dist/screens/VMDetailScreen.d.ts.map +1 -0
- package/ui/dist/screens/VMDetailScreen.js +96 -0
- package/ui/dist/screens/VMDetailScreen.js.map +1 -0
- package/ui/dist/screens/VMListScreen.d.ts +12 -0
- package/ui/dist/screens/VMListScreen.d.ts.map +1 -0
- package/ui/dist/screens/VMListScreen.js +158 -0
- package/ui/dist/screens/VMListScreen.js.map +1 -0
- package/ui/dist/screens/tabs/FilesTab.d.ts +9 -0
- package/ui/dist/screens/tabs/FilesTab.d.ts.map +1 -0
- package/ui/dist/screens/tabs/FilesTab.js +374 -0
- package/ui/dist/screens/tabs/FilesTab.js.map +1 -0
- package/ui/dist/screens/tabs/OverviewTab.d.ts +10 -0
- package/ui/dist/screens/tabs/OverviewTab.d.ts.map +1 -0
- package/ui/dist/screens/tabs/OverviewTab.js +110 -0
- package/ui/dist/screens/tabs/OverviewTab.js.map +1 -0
- package/ui/dist/screens/tabs/TerminalTab.d.ts +9 -0
- package/ui/dist/screens/tabs/TerminalTab.d.ts.map +1 -0
- package/ui/dist/screens/tabs/TerminalTab.js +270 -0
- package/ui/dist/screens/tabs/TerminalTab.js.map +1 -0
- package/ui/dist/utils/terminalEmulator.d.ts +49 -0
- package/ui/dist/utils/terminalEmulator.d.ts.map +1 -0
- package/ui/dist/utils/terminalEmulator.js +88 -0
- package/ui/dist/utils/terminalEmulator.js.map +1 -0
- package/ui/package.json +34 -0
- package/ui/scripts/start-with-core.js +81 -0
|
@@ -0,0 +1,738 @@
|
|
|
1
|
+
package ipc
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/base64"
|
|
5
|
+
"fmt"
|
|
6
|
+
"io"
|
|
7
|
+
"log"
|
|
8
|
+
|
|
9
|
+
"github.com/adishm/heyvm/internal/auth"
|
|
10
|
+
"github.com/adishm/heyvm/internal/sftp"
|
|
11
|
+
"github.com/adishm/heyvm/internal/vm"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
// handleListVMs returns all VMs in the registry
|
|
15
|
+
func (h *Handler) handleListVMs(params map[string]interface{}) Response {
|
|
16
|
+
vms := h.registry.List()
|
|
17
|
+
return NewSuccessResponse(vms)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// handleAddVM adds a new VM to the registry
|
|
21
|
+
func (h *Handler) handleAddVM(params map[string]interface{}) Response {
|
|
22
|
+
// Parse VM from params
|
|
23
|
+
vmData, ok := params["vm"].(map[string]interface{})
|
|
24
|
+
if !ok {
|
|
25
|
+
return NewErrorResponseWithMessage("invalid VM data")
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Extract fields
|
|
29
|
+
name, _ := vmData["name"].(string)
|
|
30
|
+
host, _ := vmData["host"].(string)
|
|
31
|
+
port, _ := vmData["port"].(float64) // JSON numbers are float64
|
|
32
|
+
username, _ := vmData["username"].(string)
|
|
33
|
+
|
|
34
|
+
if port == 0 {
|
|
35
|
+
port = 22 // Default SSH port
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Extract auth config
|
|
39
|
+
authData, ok := vmData["auth"].(map[string]interface{})
|
|
40
|
+
if !ok {
|
|
41
|
+
return NewErrorResponseWithMessage("invalid auth configuration")
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
authTypeStr, _ := authData["type"].(string)
|
|
45
|
+
keyPath, _ := authData["keyPath"].(string)
|
|
46
|
+
rememberPassword, _ := authData["rememberPassword"].(bool)
|
|
47
|
+
|
|
48
|
+
var authType vm.AuthType
|
|
49
|
+
switch authTypeStr {
|
|
50
|
+
case "key":
|
|
51
|
+
authType = vm.AuthTypeKey
|
|
52
|
+
case "password":
|
|
53
|
+
authType = vm.AuthTypePassword
|
|
54
|
+
default:
|
|
55
|
+
return NewErrorResponseWithMessage("invalid auth type")
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Create VM
|
|
59
|
+
newVM := &vm.VM{
|
|
60
|
+
Name: name,
|
|
61
|
+
Host: host,
|
|
62
|
+
Port: int(port),
|
|
63
|
+
Username: username,
|
|
64
|
+
Auth: vm.AuthConfig{
|
|
65
|
+
Type: authType,
|
|
66
|
+
KeyPath: keyPath,
|
|
67
|
+
RememberPassword: rememberPassword,
|
|
68
|
+
},
|
|
69
|
+
Status: vm.VMStatusDisconnected,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Add to registry
|
|
73
|
+
if err := h.registry.Add(newVM); err != nil {
|
|
74
|
+
return NewErrorResponse(err)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Save registry
|
|
78
|
+
if err := h.registry.Save(); err != nil {
|
|
79
|
+
log.Printf("Warning: failed to save registry: %v", err)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return NewSuccessResponseWithMessage("VM added successfully", newVM)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// handleRemoveVM removes a VM from the registry
|
|
86
|
+
func (h *Handler) handleRemoveVM(params map[string]interface{}) Response {
|
|
87
|
+
vmID, ok := params["vm_id"].(string)
|
|
88
|
+
if !ok {
|
|
89
|
+
return NewErrorResponseWithMessage("vm_id parameter required")
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Disconnect if connected
|
|
93
|
+
h.mu.Lock()
|
|
94
|
+
if sshMgr, exists := h.sshManagers[vmID]; exists {
|
|
95
|
+
sshMgr.Disconnect()
|
|
96
|
+
delete(h.sshManagers, vmID)
|
|
97
|
+
}
|
|
98
|
+
if sftpMgr, exists := h.sftpManagers[vmID]; exists {
|
|
99
|
+
sftpMgr.Close()
|
|
100
|
+
delete(h.sftpManagers, vmID)
|
|
101
|
+
}
|
|
102
|
+
if provider, exists := h.authProviders[vmID]; exists {
|
|
103
|
+
provider.Disconnect()
|
|
104
|
+
delete(h.authProviders, vmID)
|
|
105
|
+
}
|
|
106
|
+
h.mu.Unlock()
|
|
107
|
+
|
|
108
|
+
// Remove from registry
|
|
109
|
+
if err := h.registry.Remove(vmID); err != nil {
|
|
110
|
+
return NewErrorResponse(err)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Save registry
|
|
114
|
+
if err := h.registry.Save(); err != nil {
|
|
115
|
+
log.Printf("Warning: failed to save registry: %v", err)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return NewSuccessResponseWithMessage("VM removed successfully", nil)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// handleConnectVM establishes SSH connection to a VM
|
|
122
|
+
func (h *Handler) handleConnectVM(params map[string]interface{}) Response {
|
|
123
|
+
vmID, ok := params["vm_id"].(string)
|
|
124
|
+
if !ok {
|
|
125
|
+
return NewErrorResponseWithMessage("vm_id parameter required")
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Get SSH manager
|
|
129
|
+
sshMgr, err := h.getSSHManager(vmID)
|
|
130
|
+
if err != nil {
|
|
131
|
+
return NewErrorResponse(err)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Check if already connected (idempotent)
|
|
135
|
+
if sshMgr.IsConnected() {
|
|
136
|
+
// Already connected - just ensure status is correct
|
|
137
|
+
if err := h.registry.UpdateStatus(vmID, vm.VMStatusConnected); err != nil {
|
|
138
|
+
log.Printf("Warning: failed to update VM status: %v", err)
|
|
139
|
+
}
|
|
140
|
+
if err := h.registry.Save(); err != nil {
|
|
141
|
+
log.Printf("Warning: failed to save registry: %v", err)
|
|
142
|
+
}
|
|
143
|
+
return NewSuccessResponseWithMessage("Already connected", nil)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Set status to connecting
|
|
147
|
+
if err := h.registry.UpdateStatus(vmID, vm.VMStatusConnecting); err != nil {
|
|
148
|
+
log.Printf("Warning: failed to update VM status: %v", err)
|
|
149
|
+
}
|
|
150
|
+
if err := h.registry.Save(); err != nil {
|
|
151
|
+
log.Printf("Warning: failed to save registry: %v", err)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Connect (this is now idempotent)
|
|
155
|
+
if err := sshMgr.Connect(); err != nil {
|
|
156
|
+
h.registry.UpdateStatus(vmID, vm.VMStatusError)
|
|
157
|
+
h.registry.Save()
|
|
158
|
+
return NewErrorResponse(err)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Update status to connected
|
|
162
|
+
if err := h.registry.UpdateStatus(vmID, vm.VMStatusConnected); err != nil {
|
|
163
|
+
log.Printf("Warning: failed to update VM status: %v", err)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Save registry
|
|
167
|
+
if err := h.registry.Save(); err != nil {
|
|
168
|
+
log.Printf("Warning: failed to save registry: %v", err)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return NewSuccessResponseWithMessage("Connected successfully", nil)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// handleDisconnectVM closes SSH connection to a VM
|
|
175
|
+
func (h *Handler) handleDisconnectVM(params map[string]interface{}) Response {
|
|
176
|
+
vmID, ok := params["vm_id"].(string)
|
|
177
|
+
if !ok {
|
|
178
|
+
return NewErrorResponseWithMessage("vm_id parameter required")
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
h.mu.Lock()
|
|
182
|
+
|
|
183
|
+
// Close SFTP if exists
|
|
184
|
+
if sftpMgr, exists := h.sftpManagers[vmID]; exists {
|
|
185
|
+
sftpMgr.Close()
|
|
186
|
+
delete(h.sftpManagers, vmID)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Disconnect SSH if exists
|
|
190
|
+
if sshMgr, exists := h.sshManagers[vmID]; exists {
|
|
191
|
+
if err := sshMgr.Disconnect(); err != nil {
|
|
192
|
+
h.mu.Unlock()
|
|
193
|
+
return NewErrorResponse(err)
|
|
194
|
+
}
|
|
195
|
+
delete(h.sshManagers, vmID)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
h.mu.Unlock()
|
|
199
|
+
|
|
200
|
+
// Update status
|
|
201
|
+
if err := h.registry.UpdateStatus(vmID, vm.VMStatusDisconnected); err != nil {
|
|
202
|
+
log.Printf("Warning: failed to update VM status: %v", err)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Save registry
|
|
206
|
+
if err := h.registry.Save(); err != nil {
|
|
207
|
+
log.Printf("Warning: failed to save registry: %v", err)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return NewSuccessResponseWithMessage("Disconnected successfully", nil)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// handleExecuteCommand executes a command on a VM
|
|
214
|
+
func (h *Handler) handleExecuteCommand(params map[string]interface{}) Response {
|
|
215
|
+
vmID, ok := params["vm_id"].(string)
|
|
216
|
+
if !ok {
|
|
217
|
+
return NewErrorResponseWithMessage("vm_id parameter required")
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
command, ok := params["command"].(string)
|
|
221
|
+
if !ok {
|
|
222
|
+
return NewErrorResponseWithMessage("command parameter required")
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Get SSH manager
|
|
226
|
+
sshMgr, err := h.getSSHManager(vmID)
|
|
227
|
+
if err != nil {
|
|
228
|
+
return NewErrorResponse(err)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Ensure connected
|
|
232
|
+
if !sshMgr.IsConnected() {
|
|
233
|
+
if err := sshMgr.Connect(); err != nil {
|
|
234
|
+
return NewErrorResponse(err)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Execute command
|
|
239
|
+
output, err := sshMgr.ExecuteCommand(command)
|
|
240
|
+
if err != nil {
|
|
241
|
+
return NewErrorResponse(err)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return NewSuccessResponse(map[string]interface{}{
|
|
245
|
+
"output": output,
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// handleListFiles lists files in a directory on a VM
|
|
250
|
+
func (h *Handler) handleListFiles(params map[string]interface{}) Response {
|
|
251
|
+
vmID, ok := params["vm_id"].(string)
|
|
252
|
+
if !ok {
|
|
253
|
+
return NewErrorResponseWithMessage("vm_id parameter required")
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
path, ok := params["path"].(string)
|
|
257
|
+
if !ok {
|
|
258
|
+
path = "/" // Default to root
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Get SFTP manager
|
|
262
|
+
sftpMgr, err := h.getSFTPManager(vmID)
|
|
263
|
+
if err != nil {
|
|
264
|
+
return NewErrorResponse(err)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// List files
|
|
268
|
+
files, err := sftpMgr.List(path)
|
|
269
|
+
if err != nil {
|
|
270
|
+
return NewErrorResponse(err)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return NewSuccessResponse(files)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// handleUploadFile uploads a file to a VM
|
|
277
|
+
func (h *Handler) handleUploadFile(params map[string]interface{}) Response {
|
|
278
|
+
vmID, ok := params["vm_id"].(string)
|
|
279
|
+
if !ok {
|
|
280
|
+
return NewErrorResponseWithMessage("vm_id parameter required")
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
localPath, ok := params["local_path"].(string)
|
|
284
|
+
if !ok {
|
|
285
|
+
return NewErrorResponseWithMessage("local_path parameter required")
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
remotePath, ok := params["remote_path"].(string)
|
|
289
|
+
if !ok {
|
|
290
|
+
return NewErrorResponseWithMessage("remote_path parameter required")
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Get SFTP manager
|
|
294
|
+
sftpMgr, err := h.getSFTPManager(vmID)
|
|
295
|
+
if err != nil {
|
|
296
|
+
return NewErrorResponse(err)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Upload file with progress reporting
|
|
300
|
+
opts := &sftp.TransferOptions{
|
|
301
|
+
Overwrite: true,
|
|
302
|
+
PreservePermissions: true,
|
|
303
|
+
OnProgress: func(bytesTransferred, totalBytes int64) {
|
|
304
|
+
// Emit progress event to UI
|
|
305
|
+
h.Emit(EventTransferProgress, map[string]interface{}{
|
|
306
|
+
"vm_id": vmID,
|
|
307
|
+
"file": remotePath,
|
|
308
|
+
"direction": "upload",
|
|
309
|
+
"bytes_transferred": bytesTransferred,
|
|
310
|
+
"total_bytes": totalBytes,
|
|
311
|
+
"percent": float64(bytesTransferred) / float64(totalBytes) * 100,
|
|
312
|
+
})
|
|
313
|
+
},
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if err := sftpMgr.Upload(localPath, remotePath, opts); err != nil {
|
|
317
|
+
return NewErrorResponse(err)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return NewSuccessResponseWithMessage("File uploaded successfully", nil)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// handleDownloadFile downloads a file from a VM
|
|
324
|
+
func (h *Handler) handleDownloadFile(params map[string]interface{}) Response {
|
|
325
|
+
vmID, ok := params["vm_id"].(string)
|
|
326
|
+
if !ok {
|
|
327
|
+
return NewErrorResponseWithMessage("vm_id parameter required")
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
remotePath, ok := params["remote_path"].(string)
|
|
331
|
+
if !ok {
|
|
332
|
+
return NewErrorResponseWithMessage("remote_path parameter required")
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
localPath, ok := params["local_path"].(string)
|
|
336
|
+
if !ok {
|
|
337
|
+
return NewErrorResponseWithMessage("local_path parameter required")
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Get SFTP manager
|
|
341
|
+
sftpMgr, err := h.getSFTPManager(vmID)
|
|
342
|
+
if err != nil {
|
|
343
|
+
return NewErrorResponse(err)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Download file with progress reporting
|
|
347
|
+
opts := &sftp.TransferOptions{
|
|
348
|
+
Overwrite: true,
|
|
349
|
+
PreservePermissions: true,
|
|
350
|
+
OnProgress: func(bytesTransferred, totalBytes int64) {
|
|
351
|
+
// Emit progress event to UI
|
|
352
|
+
h.Emit(EventTransferProgress, map[string]interface{}{
|
|
353
|
+
"vm_id": vmID,
|
|
354
|
+
"file": remotePath,
|
|
355
|
+
"direction": "download",
|
|
356
|
+
"bytes_transferred": bytesTransferred,
|
|
357
|
+
"total_bytes": totalBytes,
|
|
358
|
+
"percent": float64(bytesTransferred) / float64(totalBytes) * 100,
|
|
359
|
+
})
|
|
360
|
+
},
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if err := sftpMgr.Download(remotePath, localPath, opts); err != nil {
|
|
364
|
+
return NewErrorResponse(err)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return NewSuccessResponseWithMessage("File downloaded successfully", nil)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// handleDeleteFile deletes a file on a VM
|
|
371
|
+
func (h *Handler) handleDeleteFile(params map[string]interface{}) Response {
|
|
372
|
+
vmID, ok := params["vm_id"].(string)
|
|
373
|
+
if !ok {
|
|
374
|
+
return NewErrorResponseWithMessage("vm_id parameter required")
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
path, ok := params["path"].(string)
|
|
378
|
+
if !ok {
|
|
379
|
+
return NewErrorResponseWithMessage("path parameter required")
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Get SFTP manager
|
|
383
|
+
sftpMgr, err := h.getSFTPManager(vmID)
|
|
384
|
+
if err != nil {
|
|
385
|
+
return NewErrorResponse(err)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Delete file
|
|
389
|
+
if err := sftpMgr.Delete(path); err != nil {
|
|
390
|
+
return NewErrorResponse(err)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return NewSuccessResponseWithMessage("File deleted successfully", nil)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// handleRenameFile renames/moves a file on a VM
|
|
397
|
+
func (h *Handler) handleRenameFile(params map[string]interface{}) Response {
|
|
398
|
+
vmID, ok := params["vm_id"].(string)
|
|
399
|
+
if !ok {
|
|
400
|
+
return NewErrorResponseWithMessage("vm_id parameter required")
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
oldPath, ok := params["old_path"].(string)
|
|
404
|
+
if !ok {
|
|
405
|
+
return NewErrorResponseWithMessage("old_path parameter required")
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
newPath, ok := params["new_path"].(string)
|
|
409
|
+
if !ok {
|
|
410
|
+
return NewErrorResponseWithMessage("new_path parameter required")
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Get SFTP manager
|
|
414
|
+
sftpMgr, err := h.getSFTPManager(vmID)
|
|
415
|
+
if err != nil {
|
|
416
|
+
return NewErrorResponse(err)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Rename file
|
|
420
|
+
if err := sftpMgr.Rename(oldPath, newPath); err != nil {
|
|
421
|
+
return NewErrorResponse(err)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return NewSuccessResponseWithMessage("File renamed successfully", nil)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// handleStorePassword stores a password in the keychain
|
|
428
|
+
func (h *Handler) handleStorePassword(params map[string]interface{}) Response {
|
|
429
|
+
vmID, ok := params["vm_id"].(string)
|
|
430
|
+
if !ok {
|
|
431
|
+
return NewErrorResponseWithMessage("vm_id parameter required")
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
password, ok := params["password"].(string)
|
|
435
|
+
if !ok {
|
|
436
|
+
return NewErrorResponseWithMessage("password parameter required")
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Create password auth provider
|
|
440
|
+
passwordAuth, err := auth.NewPasswordAuth()
|
|
441
|
+
if err != nil {
|
|
442
|
+
return NewErrorResponse(err)
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Store password
|
|
446
|
+
if err := passwordAuth.StorePassword(vmID, password); err != nil {
|
|
447
|
+
return NewErrorResponse(err)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return NewSuccessResponseWithMessage("Password stored successfully", nil)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// handleTestConnection tests SSH connection to a VM without saving it
|
|
454
|
+
func (h *Handler) handleTestConnection(params map[string]interface{}) Response {
|
|
455
|
+
// Parse VM from params (similar to handleAddVM)
|
|
456
|
+
vmData, ok := params["vm"].(map[string]interface{})
|
|
457
|
+
if !ok {
|
|
458
|
+
return NewErrorResponseWithMessage("invalid VM data")
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
name, _ := vmData["name"].(string)
|
|
462
|
+
host, _ := vmData["host"].(string)
|
|
463
|
+
port, _ := vmData["port"].(float64)
|
|
464
|
+
username, _ := vmData["username"].(string)
|
|
465
|
+
|
|
466
|
+
if port == 0 {
|
|
467
|
+
port = 22
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
authData, ok := vmData["auth"].(map[string]interface{})
|
|
471
|
+
if !ok {
|
|
472
|
+
return NewErrorResponseWithMessage("invalid auth configuration")
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
authTypeStr, _ := authData["type"].(string)
|
|
476
|
+
keyPath, _ := authData["keyPath"].(string)
|
|
477
|
+
|
|
478
|
+
// Get temporary password from params (for testing only)
|
|
479
|
+
tempPassword, _ := params["password"].(string)
|
|
480
|
+
|
|
481
|
+
var authType vm.AuthType
|
|
482
|
+
switch authTypeStr {
|
|
483
|
+
case "key":
|
|
484
|
+
authType = vm.AuthTypeKey
|
|
485
|
+
case "password":
|
|
486
|
+
authType = vm.AuthTypePassword
|
|
487
|
+
default:
|
|
488
|
+
return NewErrorResponseWithMessage("invalid auth type")
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Create temporary VM
|
|
492
|
+
testVM := &vm.VM{
|
|
493
|
+
ID: "test",
|
|
494
|
+
Name: name,
|
|
495
|
+
Host: host,
|
|
496
|
+
Port: int(port),
|
|
497
|
+
Username: username,
|
|
498
|
+
Auth: vm.AuthConfig{
|
|
499
|
+
Type: authType,
|
|
500
|
+
KeyPath: keyPath,
|
|
501
|
+
},
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Create auth provider with temporary password if provided
|
|
505
|
+
var provider auth.Provider
|
|
506
|
+
var err error
|
|
507
|
+
|
|
508
|
+
if authType == vm.AuthTypePassword && tempPassword != "" {
|
|
509
|
+
// Use temporary password for testing
|
|
510
|
+
provider, err = auth.NewPasswordAuthWithTemp(tempPassword)
|
|
511
|
+
} else {
|
|
512
|
+
provider, err = auth.NewProvider(testVM)
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if err != nil {
|
|
516
|
+
return NewErrorResponse(err)
|
|
517
|
+
}
|
|
518
|
+
defer provider.Disconnect()
|
|
519
|
+
|
|
520
|
+
// Test connection
|
|
521
|
+
client, err := provider.Connect(testVM)
|
|
522
|
+
if err != nil {
|
|
523
|
+
return NewErrorResponse(fmt.Errorf("connection test failed: %w", err))
|
|
524
|
+
}
|
|
525
|
+
defer client.Close()
|
|
526
|
+
|
|
527
|
+
return NewSuccessResponseWithMessage("Connection test successful", nil)
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// handleStartPTY starts an interactive PTY session
|
|
531
|
+
func (h *Handler) handleStartPTY(params map[string]interface{}) Response {
|
|
532
|
+
vmID, ok := params["vm_id"].(string)
|
|
533
|
+
if !ok {
|
|
534
|
+
return NewErrorResponseWithMessage("vm_id parameter required")
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
rows, ok := params["rows"].(float64)
|
|
538
|
+
if !ok {
|
|
539
|
+
rows = 24 // Default rows
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
cols, ok := params["cols"].(float64)
|
|
543
|
+
if !ok {
|
|
544
|
+
cols = 80 // Default cols
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Get SSH manager
|
|
548
|
+
sshMgr, err := h.getSSHManager(vmID)
|
|
549
|
+
if err != nil {
|
|
550
|
+
h.Emit(EventPTYError, map[string]interface{}{
|
|
551
|
+
"vm_id": vmID,
|
|
552
|
+
"error": err.Error(),
|
|
553
|
+
})
|
|
554
|
+
return NewErrorResponse(err)
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Ensure connected
|
|
558
|
+
if !sshMgr.IsConnected() {
|
|
559
|
+
if err := sshMgr.Connect(); err != nil {
|
|
560
|
+
h.Emit(EventPTYError, map[string]interface{}{
|
|
561
|
+
"vm_id": vmID,
|
|
562
|
+
"error": err.Error(),
|
|
563
|
+
})
|
|
564
|
+
return NewErrorResponse(err)
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Start PTY session
|
|
569
|
+
session, err := sshMgr.StartPTY(int(rows), int(cols))
|
|
570
|
+
if err != nil {
|
|
571
|
+
h.Emit(EventPTYError, map[string]interface{}{
|
|
572
|
+
"vm_id": vmID,
|
|
573
|
+
"error": err.Error(),
|
|
574
|
+
})
|
|
575
|
+
return NewErrorResponse(err)
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
sessionID := session.ID()
|
|
579
|
+
|
|
580
|
+
// Emit PTY_READY event
|
|
581
|
+
h.Emit(EventPTYReady, map[string]interface{}{
|
|
582
|
+
"vm_id": vmID,
|
|
583
|
+
"session_id": sessionID,
|
|
584
|
+
})
|
|
585
|
+
|
|
586
|
+
// Start THE ONLY goroutine that reads from combined stdout+stderr
|
|
587
|
+
// This keeps stdin alive and ensures all output is captured
|
|
588
|
+
go func() {
|
|
589
|
+
combined := session.CombinedOutput()
|
|
590
|
+
buf := make([]byte, 4096)
|
|
591
|
+
for {
|
|
592
|
+
// Blocking read from combined output - this is the only reader
|
|
593
|
+
n, err := combined.Read(buf)
|
|
594
|
+
if n > 0 {
|
|
595
|
+
// Base64 encode PTY output to keep JSON safe
|
|
596
|
+
encoded := base64.StdEncoding.EncodeToString(buf[:n])
|
|
597
|
+
// Emit PTY_OUTPUT event with base64-encoded data
|
|
598
|
+
h.Emit(EventPTYOutput, map[string]interface{}{
|
|
599
|
+
"session_id": sessionID,
|
|
600
|
+
"data": encoded,
|
|
601
|
+
})
|
|
602
|
+
}
|
|
603
|
+
if err != nil {
|
|
604
|
+
if err == io.EOF {
|
|
605
|
+
log.Printf("PTY session %s: combined output EOF (shell exited)", sessionID)
|
|
606
|
+
} else {
|
|
607
|
+
log.Printf("PTY session %s: read error: %v", sessionID, err)
|
|
608
|
+
}
|
|
609
|
+
// Emit PTY_EXIT event
|
|
610
|
+
h.Emit(EventPTYExit, map[string]interface{}{
|
|
611
|
+
"session_id": sessionID,
|
|
612
|
+
})
|
|
613
|
+
break
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}()
|
|
617
|
+
|
|
618
|
+
// No response needed - events are emitted instead
|
|
619
|
+
return Response{}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// handleWriteToPTY writes data to a PTY session (fire-and-forget)
|
|
623
|
+
func (h *Handler) handleWriteToPTY(params map[string]interface{}) Response {
|
|
624
|
+
vmID, ok := params["vm_id"].(string)
|
|
625
|
+
if !ok {
|
|
626
|
+
return NewErrorResponseWithMessage("vm_id parameter required")
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
sessionID, ok := params["session_id"].(string)
|
|
630
|
+
if !ok {
|
|
631
|
+
return NewErrorResponseWithMessage("session_id parameter required")
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
data, ok := params["data"].(string)
|
|
635
|
+
if !ok {
|
|
636
|
+
return NewErrorResponseWithMessage("data parameter required")
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Get SSH manager
|
|
640
|
+
sshMgr, err := h.getSSHManager(vmID)
|
|
641
|
+
if err != nil {
|
|
642
|
+
return NewErrorResponse(err)
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Get session
|
|
646
|
+
session, err := sshMgr.GetSession(sessionID)
|
|
647
|
+
if err != nil {
|
|
648
|
+
return NewErrorResponse(err)
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Write data (non-blocking from IPC perspective - fire and forget)
|
|
652
|
+
go func() {
|
|
653
|
+
_, err := session.Write([]byte(data))
|
|
654
|
+
if err != nil {
|
|
655
|
+
log.Printf("PTY session %s: write error: %v", sessionID, err)
|
|
656
|
+
}
|
|
657
|
+
}()
|
|
658
|
+
|
|
659
|
+
// No response needed
|
|
660
|
+
return Response{}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// handleResizePTY resizes a PTY session
|
|
664
|
+
func (h *Handler) handleResizePTY(params map[string]interface{}) Response {
|
|
665
|
+
vmID, ok := params["vm_id"].(string)
|
|
666
|
+
if !ok {
|
|
667
|
+
log.Printf("PTY resize: vm_id parameter required")
|
|
668
|
+
return Response{}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
sessionID, ok := params["session_id"].(string)
|
|
672
|
+
if !ok {
|
|
673
|
+
log.Printf("PTY resize: session_id parameter required")
|
|
674
|
+
return Response{}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
rows, ok := params["rows"].(float64)
|
|
678
|
+
if !ok {
|
|
679
|
+
log.Printf("PTY resize: rows parameter required")
|
|
680
|
+
return Response{}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
cols, ok := params["cols"].(float64)
|
|
684
|
+
if !ok {
|
|
685
|
+
log.Printf("PTY resize: cols parameter required")
|
|
686
|
+
return Response{}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Get SSH manager
|
|
690
|
+
sshMgr, err := h.getSSHManager(vmID)
|
|
691
|
+
if err != nil {
|
|
692
|
+
log.Printf("PTY resize error for VM %s: %v", vmID, err)
|
|
693
|
+
return Response{}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Get session
|
|
697
|
+
session, err := sshMgr.GetSession(sessionID)
|
|
698
|
+
if err != nil {
|
|
699
|
+
log.Printf("PTY resize error getting session %s: %v", sessionID, err)
|
|
700
|
+
return Response{}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Resize
|
|
704
|
+
if err := session.Resize(int(rows), int(cols)); err != nil {
|
|
705
|
+
log.Printf("PTY resize error for session %s: %v", sessionID, err)
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// No response needed
|
|
709
|
+
return Response{}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// handleClosePTY closes a PTY session
|
|
713
|
+
func (h *Handler) handleClosePTY(params map[string]interface{}) Response {
|
|
714
|
+
vmID, ok := params["vm_id"].(string)
|
|
715
|
+
if !ok {
|
|
716
|
+
return NewErrorResponseWithMessage("vm_id parameter required")
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
sessionID, ok := params["session_id"].(string)
|
|
720
|
+
if !ok {
|
|
721
|
+
return NewErrorResponseWithMessage("session_id parameter required")
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Get SSH manager
|
|
725
|
+
sshMgr, err := h.getSSHManager(vmID)
|
|
726
|
+
if err != nil {
|
|
727
|
+
log.Printf("PTY close error for VM %s: %v", vmID, err)
|
|
728
|
+
return Response{}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// Close session
|
|
732
|
+
if err := sshMgr.CloseSession(sessionID); err != nil {
|
|
733
|
+
log.Printf("PTY close error for session %s: %v", sessionID, err)
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// No response needed
|
|
737
|
+
return Response{}
|
|
738
|
+
}
|