gssh-agent 1.0.3 → 1.0.5
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/.github/workflows/ci.yml +27 -0
- package/.github/workflows/publish.yml +104 -0
- package/README.md +31 -61
- package/bin/gssh +0 -0
- package/cmd/gssh/main.go +144 -113
- package/fix_manager.patch +79 -0
- package/internal/client/ssh.go +186 -3
- package/internal/client/ssh_test.go +43 -0
- package/internal/portforward/forwarder.go +94 -40
- package/internal/protocol/types.go +3 -1
- package/internal/session/manager.go +164 -39
- package/internal/session/manager_test.go +324 -0
- package/package.json +3 -4
- package/pkg/rpc/handler.go +1 -1
- package/pkg/rpc/handler_test.go +36 -0
- package/plan.md +4 -0
- package/skill.md +34 -8
- package/bin/daemon +0 -0
- package/bin/gssh-daemon +0 -0
package/cmd/gssh/main.go
CHANGED
|
@@ -21,58 +21,62 @@ const (
|
|
|
21
21
|
version = "0.1.0"
|
|
22
22
|
)
|
|
23
23
|
|
|
24
|
-
var (
|
|
25
|
-
socketPath = flag.String("socket", defaultSocketPath, "Unix socket path")
|
|
26
|
-
)
|
|
27
|
-
|
|
28
24
|
func main() {
|
|
29
|
-
// Check for version flag before parsing
|
|
25
|
+
// Check for version flag before any parsing
|
|
30
26
|
if len(os.Args) > 1 && (os.Args[1] == "-v" || os.Args[1] == "--version") {
|
|
31
27
|
fmt.Printf("gssh version %s\n", version)
|
|
32
28
|
os.Exit(0)
|
|
33
29
|
}
|
|
34
30
|
|
|
35
|
-
// Check for help flag
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
os.Exit(0)
|
|
40
|
-
}
|
|
31
|
+
// Check for help flag
|
|
32
|
+
if len(os.Args) > 1 && (os.Args[1] == "-h" || os.Args[1] == "--help") {
|
|
33
|
+
printUsage()
|
|
34
|
+
os.Exit(0)
|
|
41
35
|
}
|
|
42
36
|
|
|
43
|
-
flag
|
|
37
|
+
// Parse global -socket flag manually
|
|
38
|
+
socketPath := defaultSocketPath
|
|
39
|
+
args := os.Args[1:]
|
|
40
|
+
for i := 0; i < len(args); i++ {
|
|
41
|
+
if args[i] == "-socket" && i+1 < len(args) {
|
|
42
|
+
socketPath = args[i+1]
|
|
43
|
+
args = append(args[:i], args[i+2:]...)
|
|
44
|
+
i--
|
|
45
|
+
}
|
|
46
|
+
}
|
|
44
47
|
|
|
45
|
-
if
|
|
48
|
+
if len(args) < 1 {
|
|
46
49
|
printUsage()
|
|
47
50
|
os.Exit(1)
|
|
48
51
|
}
|
|
49
52
|
|
|
50
|
-
cmd :=
|
|
53
|
+
cmd := args[0]
|
|
54
|
+
subArgs := args[1:]
|
|
51
55
|
|
|
52
56
|
var err error
|
|
53
57
|
switch cmd {
|
|
54
58
|
case "connect":
|
|
55
|
-
err = handleConnect()
|
|
59
|
+
err = handleConnect(subArgs, socketPath)
|
|
56
60
|
case "disconnect":
|
|
57
|
-
err = handleDisconnect()
|
|
61
|
+
err = handleDisconnect(subArgs, socketPath)
|
|
58
62
|
case "reconnect":
|
|
59
|
-
err = handleReconnect()
|
|
63
|
+
err = handleReconnect(subArgs, socketPath)
|
|
60
64
|
case "exec":
|
|
61
|
-
err = handleExec()
|
|
65
|
+
err = handleExec(subArgs, socketPath)
|
|
62
66
|
case "list", "ls":
|
|
63
|
-
err = handleList()
|
|
67
|
+
err = handleList(socketPath)
|
|
64
68
|
case "use":
|
|
65
|
-
err = handleUse()
|
|
69
|
+
err = handleUse(subArgs, socketPath)
|
|
66
70
|
case "forward":
|
|
67
|
-
err = handleForward()
|
|
71
|
+
err = handleForward(subArgs, socketPath)
|
|
68
72
|
case "forwards":
|
|
69
|
-
err = handleForwards()
|
|
73
|
+
err = handleForwards(socketPath)
|
|
70
74
|
case "forward-close":
|
|
71
|
-
err = handleForwardClose()
|
|
72
|
-
case "scp":
|
|
73
|
-
err = handleSCP()
|
|
75
|
+
err = handleForwardClose(subArgs, socketPath)
|
|
76
|
+
case "scp", "sync":
|
|
77
|
+
err = handleSCP(subArgs, socketPath)
|
|
74
78
|
case "sftp":
|
|
75
|
-
err = handleSFTP()
|
|
79
|
+
err = handleSFTP(subArgs, socketPath)
|
|
76
80
|
case "help":
|
|
77
81
|
printUsage()
|
|
78
82
|
default:
|
|
@@ -89,9 +93,7 @@ func main() {
|
|
|
89
93
|
|
|
90
94
|
// readPassword reads password from terminal without echo
|
|
91
95
|
func readPassword() (string, error) {
|
|
92
|
-
// Check if stdin is a terminal
|
|
93
96
|
if !term.IsTerminal(int(os.Stdin.Fd())) {
|
|
94
|
-
// Try to read from stdin
|
|
95
97
|
reader := bufio.NewReader(os.Stdin)
|
|
96
98
|
fmt.Print("Password: ")
|
|
97
99
|
password, err := reader.ReadString('\n')
|
|
@@ -131,8 +133,8 @@ func readPassphrase() (string, error) {
|
|
|
131
133
|
return string(bytePassphrase), nil
|
|
132
134
|
}
|
|
133
135
|
|
|
134
|
-
func sendRequest(method string, params interface{}) ([]byte, error) {
|
|
135
|
-
conn, err := net.Dial("unix",
|
|
136
|
+
func sendRequest(socketPath, method string, params interface{}) ([]byte, error) {
|
|
137
|
+
conn, err := net.Dial("unix", socketPath)
|
|
136
138
|
if err != nil {
|
|
137
139
|
return nil, fmt.Errorf("failed to connect to daemon: %w", err)
|
|
138
140
|
}
|
|
@@ -155,7 +157,6 @@ func sendRequest(method string, params interface{}) ([]byte, error) {
|
|
|
155
157
|
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
|
156
158
|
}
|
|
157
159
|
|
|
158
|
-
// Add newline to signal end of request
|
|
159
160
|
reqData = append(reqData, '\n')
|
|
160
161
|
|
|
161
162
|
_, err = conn.Write(reqData)
|
|
@@ -163,14 +164,12 @@ func sendRequest(method string, params interface{}) ([]byte, error) {
|
|
|
163
164
|
return nil, fmt.Errorf("failed to send request: %w", err)
|
|
164
165
|
}
|
|
165
166
|
|
|
166
|
-
// Read line by line
|
|
167
167
|
reader := bufio.NewReader(conn)
|
|
168
168
|
line, err := reader.ReadBytes('\n')
|
|
169
169
|
if err != nil {
|
|
170
170
|
return nil, fmt.Errorf("failed to read response: %w", err)
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
// Remove trailing newline
|
|
174
173
|
line = line[:len(line)-1]
|
|
175
174
|
|
|
176
175
|
var resp protocol.Response
|
|
@@ -190,22 +189,24 @@ func sendRequest(method string, params interface{}) ([]byte, error) {
|
|
|
190
189
|
return result, nil
|
|
191
190
|
}
|
|
192
191
|
|
|
193
|
-
func handleConnect() error {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
192
|
+
func handleConnect(args []string, socketPath string) error {
|
|
193
|
+
fs := flag.NewFlagSet("connect", flag.ContinueOnError)
|
|
194
|
+
fs.SetOutput(os.Stderr)
|
|
195
|
+
|
|
196
|
+
user := fs.String("u", "", "Username")
|
|
197
|
+
host := fs.String("h", "", "Host")
|
|
198
|
+
port := fs.Int("p", 22, "Port")
|
|
199
|
+
password := fs.String("P", "", "Password")
|
|
200
|
+
keyPath := fs.String("i", "", "SSH key path")
|
|
201
|
+
askPassword := fs.Bool("ask-pass", false, "Ask for password interactively")
|
|
202
|
+
askPassphrase := fs.Bool("ask-passphrase", false, "Ask for key passphrase interactively")
|
|
201
203
|
|
|
202
|
-
|
|
204
|
+
fs.Parse(args)
|
|
203
205
|
|
|
204
206
|
if *user == "" || *host == "" {
|
|
205
207
|
return fmt.Errorf("user and host are required")
|
|
206
208
|
}
|
|
207
209
|
|
|
208
|
-
// If no password provided via flag and -ask-pass is set, read interactively
|
|
209
210
|
if *password == "" && *askPassword {
|
|
210
211
|
p, err := readPassword()
|
|
211
212
|
if err != nil {
|
|
@@ -214,14 +215,11 @@ func handleConnect() error {
|
|
|
214
215
|
*password = p
|
|
215
216
|
}
|
|
216
217
|
|
|
217
|
-
// If key path provided and -ask-passphrase is set, read interactively
|
|
218
218
|
if *keyPath != "" && *askPassphrase {
|
|
219
219
|
passphrase, err := readPassphrase()
|
|
220
220
|
if err != nil {
|
|
221
221
|
return fmt.Errorf("failed to read passphrase: %w", err)
|
|
222
222
|
}
|
|
223
|
-
// Note: Passphrase support would require modifying the SSH client
|
|
224
|
-
// For now, we'll just warn that it's not supported
|
|
225
223
|
if passphrase != "" {
|
|
226
224
|
fmt.Println("Note: Passphrase for keys is not yet supported, ignoring")
|
|
227
225
|
}
|
|
@@ -235,7 +233,7 @@ func handleConnect() error {
|
|
|
235
233
|
KeyPath: *keyPath,
|
|
236
234
|
}
|
|
237
235
|
|
|
238
|
-
result, err := sendRequest("connect", params)
|
|
236
|
+
result, err := sendRequest(socketPath, "connect", params)
|
|
239
237
|
if err != nil {
|
|
240
238
|
return err
|
|
241
239
|
}
|
|
@@ -249,15 +247,24 @@ func handleConnect() error {
|
|
|
249
247
|
return nil
|
|
250
248
|
}
|
|
251
249
|
|
|
252
|
-
func handleDisconnect() error {
|
|
253
|
-
|
|
254
|
-
|
|
250
|
+
func handleDisconnect(args []string, socketPath string) error {
|
|
251
|
+
fs := flag.NewFlagSet("disconnect", flag.ContinueOnError)
|
|
252
|
+
fs.SetOutput(os.Stderr)
|
|
253
|
+
|
|
254
|
+
sessionID := fs.String("s", "", "Session ID")
|
|
255
|
+
fs.Parse(args)
|
|
256
|
+
|
|
257
|
+
// Support positional argument
|
|
258
|
+
sessionIDStr := *sessionID
|
|
259
|
+
if sessionIDStr == "" && fs.NArg() > 0 {
|
|
260
|
+
sessionIDStr = fs.Arg(0)
|
|
261
|
+
}
|
|
255
262
|
|
|
256
263
|
params := protocol.DisconnectParams{
|
|
257
|
-
SessionID:
|
|
264
|
+
SessionID: sessionIDStr,
|
|
258
265
|
}
|
|
259
266
|
|
|
260
|
-
_, err := sendRequest("disconnect", params)
|
|
267
|
+
_, err := sendRequest(socketPath, "disconnect", params)
|
|
261
268
|
if err != nil {
|
|
262
269
|
return err
|
|
263
270
|
}
|
|
@@ -266,15 +273,24 @@ func handleDisconnect() error {
|
|
|
266
273
|
return nil
|
|
267
274
|
}
|
|
268
275
|
|
|
269
|
-
func handleReconnect() error {
|
|
270
|
-
|
|
271
|
-
|
|
276
|
+
func handleReconnect(args []string, socketPath string) error {
|
|
277
|
+
fs := flag.NewFlagSet("reconnect", flag.ContinueOnError)
|
|
278
|
+
fs.SetOutput(os.Stderr)
|
|
279
|
+
|
|
280
|
+
sessionID := fs.String("s", "", "Session ID")
|
|
281
|
+
fs.Parse(args)
|
|
282
|
+
|
|
283
|
+
// Support positional argument
|
|
284
|
+
sessionIDStr := *sessionID
|
|
285
|
+
if sessionIDStr == "" && fs.NArg() > 0 {
|
|
286
|
+
sessionIDStr = fs.Arg(0)
|
|
287
|
+
}
|
|
272
288
|
|
|
273
289
|
params := protocol.ReconnectParams{
|
|
274
|
-
SessionID:
|
|
290
|
+
SessionID: sessionIDStr,
|
|
275
291
|
}
|
|
276
292
|
|
|
277
|
-
result, err := sendRequest("reconnect", params)
|
|
293
|
+
result, err := sendRequest(socketPath, "reconnect", params)
|
|
278
294
|
if err != nil {
|
|
279
295
|
return err
|
|
280
296
|
}
|
|
@@ -288,22 +304,22 @@ func handleReconnect() error {
|
|
|
288
304
|
return nil
|
|
289
305
|
}
|
|
290
306
|
|
|
291
|
-
func handleExec() error {
|
|
292
|
-
|
|
293
|
-
|
|
307
|
+
func handleExec(args []string, socketPath string) error {
|
|
308
|
+
fs := flag.NewFlagSet("exec", flag.ContinueOnError)
|
|
309
|
+
fs.SetOutput(os.Stderr)
|
|
294
310
|
|
|
295
|
-
sessionID :=
|
|
296
|
-
|
|
297
|
-
|
|
311
|
+
sessionID := fs.String("s", "", "Session ID")
|
|
312
|
+
timeout := fs.Int("t", 0, "Timeout in seconds (0 = no timeout)")
|
|
313
|
+
sudoPassword := fs.String("S", "", "sudo password")
|
|
314
|
+
askSudoPassword := fs.Bool("ask-sudo-pass", false, "Interactively ask for sudo password")
|
|
298
315
|
|
|
299
|
-
|
|
316
|
+
fs.Parse(args)
|
|
300
317
|
|
|
301
|
-
if
|
|
318
|
+
if fs.NArg() < 1 {
|
|
302
319
|
return fmt.Errorf("command required")
|
|
303
320
|
}
|
|
304
|
-
command := strings.Join(
|
|
321
|
+
command := strings.Join(fs.Args(), " ")
|
|
305
322
|
|
|
306
|
-
// If sudo password needed but not provided, prompt for it
|
|
307
323
|
if *askSudoPassword && *sudoPassword == "" {
|
|
308
324
|
p, err := readPassword()
|
|
309
325
|
if err != nil {
|
|
@@ -312,18 +328,17 @@ func handleExec() error {
|
|
|
312
328
|
*sudoPassword = p
|
|
313
329
|
}
|
|
314
330
|
|
|
315
|
-
// If command contains sudo and password provided, wrap the command
|
|
316
331
|
if *sudoPassword != "" && strings.Contains(command, "sudo") {
|
|
317
|
-
// Use printf to pipe password to sudo -S
|
|
318
332
|
command = fmt.Sprintf("printf '%%s\\n' '%s' | %s -S", *sudoPassword, command)
|
|
319
333
|
}
|
|
320
334
|
|
|
321
335
|
params := protocol.ExecParams{
|
|
322
336
|
SessionID: *sessionID,
|
|
323
337
|
Command: command,
|
|
338
|
+
Timeout: *timeout,
|
|
324
339
|
}
|
|
325
340
|
|
|
326
|
-
result, err := sendRequest("exec", params)
|
|
341
|
+
result, err := sendRequest(socketPath, "exec", params)
|
|
327
342
|
if err != nil {
|
|
328
343
|
return err
|
|
329
344
|
}
|
|
@@ -345,8 +360,8 @@ func handleExec() error {
|
|
|
345
360
|
return nil
|
|
346
361
|
}
|
|
347
362
|
|
|
348
|
-
func handleList() error {
|
|
349
|
-
result, err := sendRequest("list", nil)
|
|
363
|
+
func handleList(socketPath string) error {
|
|
364
|
+
result, err := sendRequest(socketPath, "list", nil)
|
|
350
365
|
if err != nil {
|
|
351
366
|
return err
|
|
352
367
|
}
|
|
@@ -369,19 +384,22 @@ func handleList() error {
|
|
|
369
384
|
return nil
|
|
370
385
|
}
|
|
371
386
|
|
|
372
|
-
func handleUse() error {
|
|
373
|
-
flag.
|
|
387
|
+
func handleUse(args []string, socketPath string) error {
|
|
388
|
+
fs := flag.NewFlagSet("use", flag.ContinueOnError)
|
|
389
|
+
fs.SetOutput(os.Stderr)
|
|
374
390
|
|
|
375
|
-
|
|
391
|
+
fs.Parse(args)
|
|
392
|
+
|
|
393
|
+
if fs.NArg() < 1 {
|
|
376
394
|
return fmt.Errorf("session ID required")
|
|
377
395
|
}
|
|
378
|
-
sessionIDStr :=
|
|
396
|
+
sessionIDStr := fs.Arg(0)
|
|
379
397
|
|
|
380
398
|
params := protocol.UseParams{
|
|
381
399
|
SessionID: sessionIDStr,
|
|
382
400
|
}
|
|
383
401
|
|
|
384
|
-
_, err := sendRequest("use", params)
|
|
402
|
+
_, err := sendRequest(socketPath, "use", params)
|
|
385
403
|
if err != nil {
|
|
386
404
|
return err
|
|
387
405
|
}
|
|
@@ -390,13 +408,16 @@ func handleUse() error {
|
|
|
390
408
|
return nil
|
|
391
409
|
}
|
|
392
410
|
|
|
393
|
-
func handleForward() error {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
411
|
+
func handleForward(args []string, socketPath string) error {
|
|
412
|
+
fs := flag.NewFlagSet("forward", flag.ContinueOnError)
|
|
413
|
+
fs.SetOutput(os.Stderr)
|
|
414
|
+
|
|
415
|
+
sessionID := fs.String("s", "", "Session ID")
|
|
416
|
+
local := fs.Int("l", 0, "Local port")
|
|
417
|
+
remote := fs.Int("r", 0, "Remote port")
|
|
418
|
+
isRemote := fs.Bool("R", false, "Remote port forward")
|
|
398
419
|
|
|
399
|
-
|
|
420
|
+
fs.Parse(args)
|
|
400
421
|
|
|
401
422
|
if *local == 0 || *remote == 0 {
|
|
402
423
|
return fmt.Errorf("local and remote ports are required")
|
|
@@ -414,7 +435,7 @@ func handleForward() error {
|
|
|
414
435
|
Remote: *remote,
|
|
415
436
|
}
|
|
416
437
|
|
|
417
|
-
result, err := sendRequest("forward", params)
|
|
438
|
+
result, err := sendRequest(socketPath, "forward", params)
|
|
418
439
|
if err != nil {
|
|
419
440
|
return err
|
|
420
441
|
}
|
|
@@ -428,8 +449,8 @@ func handleForward() error {
|
|
|
428
449
|
return nil
|
|
429
450
|
}
|
|
430
451
|
|
|
431
|
-
func handleForwards() error {
|
|
432
|
-
result, err := sendRequest("forwards", nil)
|
|
452
|
+
func handleForwards(socketPath string) error {
|
|
453
|
+
result, err := sendRequest(socketPath, "forwards", nil)
|
|
433
454
|
if err != nil {
|
|
434
455
|
return err
|
|
435
456
|
}
|
|
@@ -452,19 +473,22 @@ func handleForwards() error {
|
|
|
452
473
|
return nil
|
|
453
474
|
}
|
|
454
475
|
|
|
455
|
-
func handleForwardClose() error {
|
|
456
|
-
flag.
|
|
476
|
+
func handleForwardClose(args []string, socketPath string) error {
|
|
477
|
+
fs := flag.NewFlagSet("forward-close", flag.ContinueOnError)
|
|
478
|
+
fs.SetOutput(os.Stderr)
|
|
479
|
+
|
|
480
|
+
fs.Parse(args)
|
|
457
481
|
|
|
458
|
-
if
|
|
482
|
+
if fs.NArg() < 1 {
|
|
459
483
|
return fmt.Errorf("forward ID required")
|
|
460
484
|
}
|
|
461
|
-
forwardID :=
|
|
485
|
+
forwardID := fs.Arg(0)
|
|
462
486
|
|
|
463
487
|
params := protocol.ForwardCloseParams{
|
|
464
488
|
ForwardID: forwardID,
|
|
465
489
|
}
|
|
466
490
|
|
|
467
|
-
_, err := sendRequest("forward_close", params)
|
|
491
|
+
_, err := sendRequest(socketPath, "forward_close", params)
|
|
468
492
|
if err != nil {
|
|
469
493
|
return err
|
|
470
494
|
}
|
|
@@ -473,19 +497,22 @@ func handleForwardClose() error {
|
|
|
473
497
|
return nil
|
|
474
498
|
}
|
|
475
499
|
|
|
476
|
-
func handleSCP() error {
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
isDownload := flag.Bool("get", false, "Download mode (remote->local)")
|
|
500
|
+
func handleSCP(args []string, socketPath string) error {
|
|
501
|
+
fs := flag.NewFlagSet("scp", flag.ContinueOnError)
|
|
502
|
+
fs.SetOutput(os.Stderr)
|
|
480
503
|
|
|
481
|
-
|
|
504
|
+
sessionID := fs.String("s", "", "Session ID")
|
|
505
|
+
isUpload := fs.Bool("put", false, "Upload mode (local->remote)")
|
|
506
|
+
isDownload := fs.Bool("get", false, "Download mode (remote->local)")
|
|
482
507
|
|
|
483
|
-
|
|
508
|
+
fs.Parse(args)
|
|
509
|
+
|
|
510
|
+
if fs.NArg() < 2 {
|
|
484
511
|
return fmt.Errorf("source and destination paths required")
|
|
485
512
|
}
|
|
486
513
|
|
|
487
|
-
source :=
|
|
488
|
-
dest :=
|
|
514
|
+
source := fs.Arg(0)
|
|
515
|
+
dest := fs.Arg(1)
|
|
489
516
|
|
|
490
517
|
if !*isUpload && !*isDownload {
|
|
491
518
|
return fmt.Errorf("must specify -put or -get")
|
|
@@ -498,7 +525,7 @@ func handleSCP() error {
|
|
|
498
525
|
IsUpload: *isUpload,
|
|
499
526
|
}
|
|
500
527
|
|
|
501
|
-
result, err := sendRequest("scp", params)
|
|
528
|
+
result, err := sendRequest(socketPath, "scp", params)
|
|
502
529
|
if err != nil {
|
|
503
530
|
return err
|
|
504
531
|
}
|
|
@@ -517,12 +544,15 @@ func handleSCP() error {
|
|
|
517
544
|
return nil
|
|
518
545
|
}
|
|
519
546
|
|
|
520
|
-
func handleSFTP() error {
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
547
|
+
func handleSFTP(args []string, socketPath string) error {
|
|
548
|
+
fs := flag.NewFlagSet("sftp", flag.ContinueOnError)
|
|
549
|
+
fs.SetOutput(os.Stderr)
|
|
550
|
+
|
|
551
|
+
sessionID := fs.String("s", "", "Session ID")
|
|
552
|
+
command := fs.String("c", "", "SFTP command (ls, mkdir, rm)")
|
|
553
|
+
path := fs.String("p", ".", "Path")
|
|
524
554
|
|
|
525
|
-
|
|
555
|
+
fs.Parse(args)
|
|
526
556
|
|
|
527
557
|
if *command == "" {
|
|
528
558
|
return fmt.Errorf("SFTP command required (-c ls|mkdir|rm)")
|
|
@@ -535,7 +565,7 @@ func handleSFTP() error {
|
|
|
535
565
|
Command: "ls",
|
|
536
566
|
Path: *path,
|
|
537
567
|
}
|
|
538
|
-
result, err := sendRequest("sftp_list", params)
|
|
568
|
+
result, err := sendRequest(socketPath, "sftp_list", params)
|
|
539
569
|
if err != nil {
|
|
540
570
|
return err
|
|
541
571
|
}
|
|
@@ -555,7 +585,7 @@ func handleSFTP() error {
|
|
|
555
585
|
Command: "mkdir",
|
|
556
586
|
Path: *path,
|
|
557
587
|
}
|
|
558
|
-
_, err := sendRequest("sftp_mkdir", params)
|
|
588
|
+
_, err := sendRequest(socketPath, "sftp_mkdir", params)
|
|
559
589
|
if err != nil {
|
|
560
590
|
return err
|
|
561
591
|
}
|
|
@@ -567,7 +597,7 @@ func handleSFTP() error {
|
|
|
567
597
|
Command: "rm",
|
|
568
598
|
Path: *path,
|
|
569
599
|
}
|
|
570
|
-
_, err := sendRequest("sftp_remove", params)
|
|
600
|
+
_, err := sendRequest(socketPath, "sftp_remove", params)
|
|
571
601
|
if err != nil {
|
|
572
602
|
return err
|
|
573
603
|
}
|
|
@@ -585,8 +615,8 @@ func printUsage() {
|
|
|
585
615
|
|
|
586
616
|
Usage:
|
|
587
617
|
gssh connect -u user -h host [-p port] [-i key_path] [-P password] [--ask-pass]
|
|
588
|
-
gssh disconnect [
|
|
589
|
-
gssh reconnect [
|
|
618
|
+
gssh disconnect [session_id]
|
|
619
|
+
gssh reconnect [session_id]
|
|
590
620
|
gssh exec [-s session_id] [-S password | --ask-sudo-pass] "command"
|
|
591
621
|
gssh list
|
|
592
622
|
gssh use <session_id>
|
|
@@ -595,6 +625,8 @@ Usage:
|
|
|
595
625
|
gssh forward-close <forward_id>
|
|
596
626
|
gssh scp [-s session_id] -put <local> <remote>
|
|
597
627
|
gssh scp [-s session_id] -get <remote> <local>
|
|
628
|
+
gssh sync [-s session_id] -put <local> <remote>
|
|
629
|
+
gssh sync [-s session_id] -get <remote> <local>
|
|
598
630
|
gssh sftp [-s session_id] -c <ls|mkdir|rm> -p <path>
|
|
599
631
|
gssh -v, --version
|
|
600
632
|
|
|
@@ -621,16 +653,15 @@ Options:
|
|
|
621
653
|
Examples:
|
|
622
654
|
gssh connect -u admin -h 192.168.1.1 -P password
|
|
623
655
|
gssh exec "ls -la"
|
|
656
|
+
gssh reconnect 549b6eff-f62c-4dae-a7e9-298815233cf4
|
|
624
657
|
gssh forward -l 8080 -r 80
|
|
625
|
-
gssh forward -l 9000 -r 3000 -R
|
|
626
658
|
gssh scp -put local.txt /home/user/remote.txt
|
|
627
|
-
gssh
|
|
659
|
+
gssh sync -put local_dir /home/user/remote_dir
|
|
628
660
|
`, version)
|
|
629
661
|
}
|
|
630
662
|
|
|
631
663
|
// Restore terminal on exit
|
|
632
664
|
func init() {
|
|
633
|
-
// Setup cleanup to restore terminal state on unexpected exit
|
|
634
665
|
c := make(chan os.Signal, 1)
|
|
635
666
|
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
|
|
636
667
|
go func() {
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
--- internal/session/manager.go
|
|
2
|
+
+++ internal/session/manager.go
|
|
3
|
+
@@ -100,16 +100,24 @@
|
|
4
|
+
// Check if session already exists
|
|
5
|
+
for _, s := range m.sessions {
|
|
6
|
+
if s.Host == host && s.User == user && s.Port == port {
|
|
7
|
+
- if s.Status == "connected" {
|
|
8
|
+
+ s.mu.RLock()
|
|
9
|
+
+ status := s.Status
|
|
10
|
+
+ s.mu.RUnlock()
|
|
11
|
+
+
|
|
12
|
+
+ if status == "connected" {
|
|
13
|
+
return toProtocolSession(s), fmt.Errorf("session already exists")
|
|
14
|
+
}
|
|
15
|
+
// Try to reconnect
|
|
16
|
+
sshClient, err := client.Connect(user, host, port, password, keyPath)
|
|
17
|
+
if err != nil {
|
|
18
|
+
return nil, err
|
|
19
|
+
}
|
|
20
|
+
+
|
|
21
|
+
+ s.mu.Lock()
|
|
22
|
+
s.SSHClient = sshClient
|
|
23
|
+
s.Status = "connected"
|
|
24
|
+
+ s.mu.Unlock()
|
|
25
|
+
+
|
|
26
|
+
return toProtocolSession(s), nil
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
@@ -159,10 +167,12 @@
|
|
30
|
+
return fmt.Errorf("session not found")
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
+ ms.mu.Lock()
|
|
34
|
+
if ms.SSHClient != nil {
|
|
35
|
+
ms.SSHClient.Close()
|
|
36
|
+
}
|
|
37
|
+
-
|
|
38
|
+
ms.Status = "disconnected"
|
|
39
|
+
+ ms.mu.Unlock()
|
|
40
|
+
|
|
41
|
+
// Clear default ID when disconnecting
|
|
42
|
+
if m.defaultID == sessionID {
|
|
43
|
+
@@ -188,10 +198,14 @@
|
|
44
|
+
return nil, fmt.Errorf("session not found")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
+ ms.mu.RLock()
|
|
48
|
+
+ existingClient := ms.SSHClient
|
|
49
|
+
+ ms.mu.RUnlock()
|
|
50
|
+
+
|
|
51
|
+
// Close existing connection
|
|
52
|
+
- if ms.SSHClient != nil {
|
|
53
|
+
- ms.SSHClient.Close()
|
|
54
|
+
+ if existingClient != nil {
|
|
55
|
+
+ existingClient.Close()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Create new connection
|
|
59
|
+
sshClient, err := client.Connect(ms.User, ms.Host, ms.Port, ms.Password, ms.KeyPath)
|
|
60
|
+
@@ -239,15 +253,16 @@
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
ms.mu.RLock()
|
|
64
|
+
- if ms.SSHClient == nil {
|
|
65
|
+
+ sshClient := ms.SSHClient
|
|
66
|
+
+ ms.mu.RUnlock()
|
|
67
|
+
+
|
|
68
|
+
+ if sshClient == nil {
|
|
69
|
+
- ms.mu.RUnlock()
|
|
70
|
+
return nil, fmt.Errorf("session not connected")
|
|
71
|
+
}
|
|
72
|
+
- ms.mu.RUnlock()
|
|
73
|
+
|
|
74
|
+
// 复用 SSH 连接,创建新的 session 执行命令
|
|
75
|
+
- session, err := ms.SSHClient.Client.NewSession()
|
|
76
|
+
+ session, err := sshClient.Client.NewSession()
|
|
77
|
+
if err != nil {
|
|
78
|
+
return nil, fmt.Errorf("failed to create session: %w", err)
|
|
79
|
+
}
|