gssh-agent 1.0.2 → 1.0.4
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/bin/daemon +0 -0
- package/bin/gssh +0 -0
- package/cmd/gssh/main.go +171 -121
- package/internal/session/manager.go +63 -2
- package/package.json +1 -1
package/bin/daemon
CHANGED
|
Binary file
|
package/bin/gssh
CHANGED
|
Binary file
|
package/cmd/gssh/main.go
CHANGED
|
@@ -18,47 +18,66 @@ import (
|
|
|
18
18
|
|
|
19
19
|
const (
|
|
20
20
|
defaultSocketPath = "/tmp/gssh.sock"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
var (
|
|
24
|
-
socketPath = flag.String("socket", defaultSocketPath, "Unix socket path")
|
|
21
|
+
version = "0.1.0"
|
|
25
22
|
)
|
|
26
23
|
|
|
27
24
|
func main() {
|
|
28
|
-
flag
|
|
25
|
+
// Check for version flag before any parsing
|
|
26
|
+
if len(os.Args) > 1 && (os.Args[1] == "-v" || os.Args[1] == "--version") {
|
|
27
|
+
fmt.Printf("gssh version %s\n", version)
|
|
28
|
+
os.Exit(0)
|
|
29
|
+
}
|
|
29
30
|
|
|
30
|
-
|
|
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)
|
|
35
|
+
}
|
|
36
|
+
|
|
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
|
+
}
|
|
47
|
+
|
|
48
|
+
if len(args) < 1 {
|
|
31
49
|
printUsage()
|
|
32
50
|
os.Exit(1)
|
|
33
51
|
}
|
|
34
52
|
|
|
35
|
-
cmd :=
|
|
53
|
+
cmd := args[0]
|
|
54
|
+
subArgs := args[1:]
|
|
36
55
|
|
|
37
56
|
var err error
|
|
38
57
|
switch cmd {
|
|
39
58
|
case "connect":
|
|
40
|
-
err = handleConnect()
|
|
59
|
+
err = handleConnect(subArgs, socketPath)
|
|
41
60
|
case "disconnect":
|
|
42
|
-
err = handleDisconnect()
|
|
61
|
+
err = handleDisconnect(subArgs, socketPath)
|
|
43
62
|
case "reconnect":
|
|
44
|
-
err = handleReconnect()
|
|
63
|
+
err = handleReconnect(subArgs, socketPath)
|
|
45
64
|
case "exec":
|
|
46
|
-
err = handleExec()
|
|
65
|
+
err = handleExec(subArgs, socketPath)
|
|
47
66
|
case "list", "ls":
|
|
48
|
-
err = handleList()
|
|
67
|
+
err = handleList(socketPath)
|
|
49
68
|
case "use":
|
|
50
|
-
err = handleUse()
|
|
69
|
+
err = handleUse(subArgs, socketPath)
|
|
51
70
|
case "forward":
|
|
52
|
-
err = handleForward()
|
|
71
|
+
err = handleForward(subArgs, socketPath)
|
|
53
72
|
case "forwards":
|
|
54
|
-
err = handleForwards()
|
|
73
|
+
err = handleForwards(socketPath)
|
|
55
74
|
case "forward-close":
|
|
56
|
-
err = handleForwardClose()
|
|
75
|
+
err = handleForwardClose(subArgs, socketPath)
|
|
57
76
|
case "scp":
|
|
58
|
-
err = handleSCP()
|
|
77
|
+
err = handleSCP(subArgs, socketPath)
|
|
59
78
|
case "sftp":
|
|
60
|
-
err = handleSFTP()
|
|
61
|
-
case "help"
|
|
79
|
+
err = handleSFTP(subArgs, socketPath)
|
|
80
|
+
case "help":
|
|
62
81
|
printUsage()
|
|
63
82
|
default:
|
|
64
83
|
fmt.Fprintf(os.Stderr, "Unknown command: %s\n", cmd)
|
|
@@ -74,9 +93,7 @@ func main() {
|
|
|
74
93
|
|
|
75
94
|
// readPassword reads password from terminal without echo
|
|
76
95
|
func readPassword() (string, error) {
|
|
77
|
-
// Check if stdin is a terminal
|
|
78
96
|
if !term.IsTerminal(int(os.Stdin.Fd())) {
|
|
79
|
-
// Try to read from stdin
|
|
80
97
|
reader := bufio.NewReader(os.Stdin)
|
|
81
98
|
fmt.Print("Password: ")
|
|
82
99
|
password, err := reader.ReadString('\n')
|
|
@@ -116,8 +133,8 @@ func readPassphrase() (string, error) {
|
|
|
116
133
|
return string(bytePassphrase), nil
|
|
117
134
|
}
|
|
118
135
|
|
|
119
|
-
func sendRequest(method string, params interface{}) ([]byte, error) {
|
|
120
|
-
conn, err := net.Dial("unix",
|
|
136
|
+
func sendRequest(socketPath, method string, params interface{}) ([]byte, error) {
|
|
137
|
+
conn, err := net.Dial("unix", socketPath)
|
|
121
138
|
if err != nil {
|
|
122
139
|
return nil, fmt.Errorf("failed to connect to daemon: %w", err)
|
|
123
140
|
}
|
|
@@ -140,7 +157,6 @@ func sendRequest(method string, params interface{}) ([]byte, error) {
|
|
|
140
157
|
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
|
141
158
|
}
|
|
142
159
|
|
|
143
|
-
// Add newline to signal end of request
|
|
144
160
|
reqData = append(reqData, '\n')
|
|
145
161
|
|
|
146
162
|
_, err = conn.Write(reqData)
|
|
@@ -148,14 +164,12 @@ func sendRequest(method string, params interface{}) ([]byte, error) {
|
|
|
148
164
|
return nil, fmt.Errorf("failed to send request: %w", err)
|
|
149
165
|
}
|
|
150
166
|
|
|
151
|
-
// Read line by line
|
|
152
167
|
reader := bufio.NewReader(conn)
|
|
153
168
|
line, err := reader.ReadBytes('\n')
|
|
154
169
|
if err != nil {
|
|
155
170
|
return nil, fmt.Errorf("failed to read response: %w", err)
|
|
156
171
|
}
|
|
157
172
|
|
|
158
|
-
// Remove trailing newline
|
|
159
173
|
line = line[:len(line)-1]
|
|
160
174
|
|
|
161
175
|
var resp protocol.Response
|
|
@@ -175,22 +189,24 @@ func sendRequest(method string, params interface{}) ([]byte, error) {
|
|
|
175
189
|
return result, nil
|
|
176
190
|
}
|
|
177
191
|
|
|
178
|
-
func handleConnect() error {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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")
|
|
186
203
|
|
|
187
|
-
|
|
204
|
+
fs.Parse(args)
|
|
188
205
|
|
|
189
206
|
if *user == "" || *host == "" {
|
|
190
207
|
return fmt.Errorf("user and host are required")
|
|
191
208
|
}
|
|
192
209
|
|
|
193
|
-
// If no password provided via flag and -ask-pass is set, read interactively
|
|
194
210
|
if *password == "" && *askPassword {
|
|
195
211
|
p, err := readPassword()
|
|
196
212
|
if err != nil {
|
|
@@ -199,14 +215,11 @@ func handleConnect() error {
|
|
|
199
215
|
*password = p
|
|
200
216
|
}
|
|
201
217
|
|
|
202
|
-
// If key path provided and -ask-passphrase is set, read interactively
|
|
203
218
|
if *keyPath != "" && *askPassphrase {
|
|
204
219
|
passphrase, err := readPassphrase()
|
|
205
220
|
if err != nil {
|
|
206
221
|
return fmt.Errorf("failed to read passphrase: %w", err)
|
|
207
222
|
}
|
|
208
|
-
// Note: Passphrase support would require modifying the SSH client
|
|
209
|
-
// For now, we'll just warn that it's not supported
|
|
210
223
|
if passphrase != "" {
|
|
211
224
|
fmt.Println("Note: Passphrase for keys is not yet supported, ignoring")
|
|
212
225
|
}
|
|
@@ -220,7 +233,7 @@ func handleConnect() error {
|
|
|
220
233
|
KeyPath: *keyPath,
|
|
221
234
|
}
|
|
222
235
|
|
|
223
|
-
result, err := sendRequest("connect", params)
|
|
236
|
+
result, err := sendRequest(socketPath, "connect", params)
|
|
224
237
|
if err != nil {
|
|
225
238
|
return err
|
|
226
239
|
}
|
|
@@ -234,15 +247,24 @@ func handleConnect() error {
|
|
|
234
247
|
return nil
|
|
235
248
|
}
|
|
236
249
|
|
|
237
|
-
func handleDisconnect() error {
|
|
238
|
-
|
|
239
|
-
|
|
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
|
+
}
|
|
240
262
|
|
|
241
263
|
params := protocol.DisconnectParams{
|
|
242
|
-
SessionID:
|
|
264
|
+
SessionID: sessionIDStr,
|
|
243
265
|
}
|
|
244
266
|
|
|
245
|
-
_, err := sendRequest("disconnect", params)
|
|
267
|
+
_, err := sendRequest(socketPath, "disconnect", params)
|
|
246
268
|
if err != nil {
|
|
247
269
|
return err
|
|
248
270
|
}
|
|
@@ -251,15 +273,24 @@ func handleDisconnect() error {
|
|
|
251
273
|
return nil
|
|
252
274
|
}
|
|
253
275
|
|
|
254
|
-
func handleReconnect() error {
|
|
255
|
-
|
|
256
|
-
|
|
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
|
+
}
|
|
257
288
|
|
|
258
289
|
params := protocol.ReconnectParams{
|
|
259
|
-
SessionID:
|
|
290
|
+
SessionID: sessionIDStr,
|
|
260
291
|
}
|
|
261
292
|
|
|
262
|
-
result, err := sendRequest("reconnect", params)
|
|
293
|
+
result, err := sendRequest(socketPath, "reconnect", params)
|
|
263
294
|
if err != nil {
|
|
264
295
|
return err
|
|
265
296
|
}
|
|
@@ -273,22 +304,21 @@ func handleReconnect() error {
|
|
|
273
304
|
return nil
|
|
274
305
|
}
|
|
275
306
|
|
|
276
|
-
func handleExec() error {
|
|
277
|
-
|
|
278
|
-
|
|
307
|
+
func handleExec(args []string, socketPath string) error {
|
|
308
|
+
fs := flag.NewFlagSet("exec", flag.ContinueOnError)
|
|
309
|
+
fs.SetOutput(os.Stderr)
|
|
279
310
|
|
|
280
|
-
sessionID :=
|
|
281
|
-
sudoPassword :=
|
|
282
|
-
askSudoPassword :=
|
|
311
|
+
sessionID := fs.String("s", "", "Session ID")
|
|
312
|
+
sudoPassword := fs.String("S", "", "sudo password")
|
|
313
|
+
askSudoPassword := fs.Bool("ask-sudo-pass", false, "Interactively ask for sudo password")
|
|
283
314
|
|
|
284
|
-
|
|
315
|
+
fs.Parse(args)
|
|
285
316
|
|
|
286
|
-
if
|
|
317
|
+
if fs.NArg() < 1 {
|
|
287
318
|
return fmt.Errorf("command required")
|
|
288
319
|
}
|
|
289
|
-
command := strings.Join(
|
|
320
|
+
command := strings.Join(fs.Args(), " ")
|
|
290
321
|
|
|
291
|
-
// If sudo password needed but not provided, prompt for it
|
|
292
322
|
if *askSudoPassword && *sudoPassword == "" {
|
|
293
323
|
p, err := readPassword()
|
|
294
324
|
if err != nil {
|
|
@@ -297,9 +327,7 @@ func handleExec() error {
|
|
|
297
327
|
*sudoPassword = p
|
|
298
328
|
}
|
|
299
329
|
|
|
300
|
-
// If command contains sudo and password provided, wrap the command
|
|
301
330
|
if *sudoPassword != "" && strings.Contains(command, "sudo") {
|
|
302
|
-
// Use printf to pipe password to sudo -S
|
|
303
331
|
command = fmt.Sprintf("printf '%%s\\n' '%s' | %s -S", *sudoPassword, command)
|
|
304
332
|
}
|
|
305
333
|
|
|
@@ -308,7 +336,7 @@ func handleExec() error {
|
|
|
308
336
|
Command: command,
|
|
309
337
|
}
|
|
310
338
|
|
|
311
|
-
result, err := sendRequest("exec", params)
|
|
339
|
+
result, err := sendRequest(socketPath, "exec", params)
|
|
312
340
|
if err != nil {
|
|
313
341
|
return err
|
|
314
342
|
}
|
|
@@ -330,8 +358,8 @@ func handleExec() error {
|
|
|
330
358
|
return nil
|
|
331
359
|
}
|
|
332
360
|
|
|
333
|
-
func handleList() error {
|
|
334
|
-
result, err := sendRequest("list", nil)
|
|
361
|
+
func handleList(socketPath string) error {
|
|
362
|
+
result, err := sendRequest(socketPath, "list", nil)
|
|
335
363
|
if err != nil {
|
|
336
364
|
return err
|
|
337
365
|
}
|
|
@@ -354,19 +382,22 @@ func handleList() error {
|
|
|
354
382
|
return nil
|
|
355
383
|
}
|
|
356
384
|
|
|
357
|
-
func handleUse() error {
|
|
358
|
-
flag.
|
|
385
|
+
func handleUse(args []string, socketPath string) error {
|
|
386
|
+
fs := flag.NewFlagSet("use", flag.ContinueOnError)
|
|
387
|
+
fs.SetOutput(os.Stderr)
|
|
359
388
|
|
|
360
|
-
|
|
389
|
+
fs.Parse(args)
|
|
390
|
+
|
|
391
|
+
if fs.NArg() < 1 {
|
|
361
392
|
return fmt.Errorf("session ID required")
|
|
362
393
|
}
|
|
363
|
-
sessionIDStr :=
|
|
394
|
+
sessionIDStr := fs.Arg(0)
|
|
364
395
|
|
|
365
396
|
params := protocol.UseParams{
|
|
366
397
|
SessionID: sessionIDStr,
|
|
367
398
|
}
|
|
368
399
|
|
|
369
|
-
_, err := sendRequest("use", params)
|
|
400
|
+
_, err := sendRequest(socketPath, "use", params)
|
|
370
401
|
if err != nil {
|
|
371
402
|
return err
|
|
372
403
|
}
|
|
@@ -375,13 +406,16 @@ func handleUse() error {
|
|
|
375
406
|
return nil
|
|
376
407
|
}
|
|
377
408
|
|
|
378
|
-
func handleForward() error {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
409
|
+
func handleForward(args []string, socketPath string) error {
|
|
410
|
+
fs := flag.NewFlagSet("forward", flag.ContinueOnError)
|
|
411
|
+
fs.SetOutput(os.Stderr)
|
|
412
|
+
|
|
413
|
+
sessionID := fs.String("s", "", "Session ID")
|
|
414
|
+
local := fs.Int("l", 0, "Local port")
|
|
415
|
+
remote := fs.Int("r", 0, "Remote port")
|
|
416
|
+
isRemote := fs.Bool("R", false, "Remote port forward")
|
|
383
417
|
|
|
384
|
-
|
|
418
|
+
fs.Parse(args)
|
|
385
419
|
|
|
386
420
|
if *local == 0 || *remote == 0 {
|
|
387
421
|
return fmt.Errorf("local and remote ports are required")
|
|
@@ -399,7 +433,7 @@ func handleForward() error {
|
|
|
399
433
|
Remote: *remote,
|
|
400
434
|
}
|
|
401
435
|
|
|
402
|
-
result, err := sendRequest("forward", params)
|
|
436
|
+
result, err := sendRequest(socketPath, "forward", params)
|
|
403
437
|
if err != nil {
|
|
404
438
|
return err
|
|
405
439
|
}
|
|
@@ -413,8 +447,8 @@ func handleForward() error {
|
|
|
413
447
|
return nil
|
|
414
448
|
}
|
|
415
449
|
|
|
416
|
-
func handleForwards() error {
|
|
417
|
-
result, err := sendRequest("forwards", nil)
|
|
450
|
+
func handleForwards(socketPath string) error {
|
|
451
|
+
result, err := sendRequest(socketPath, "forwards", nil)
|
|
418
452
|
if err != nil {
|
|
419
453
|
return err
|
|
420
454
|
}
|
|
@@ -437,19 +471,22 @@ func handleForwards() error {
|
|
|
437
471
|
return nil
|
|
438
472
|
}
|
|
439
473
|
|
|
440
|
-
func handleForwardClose() error {
|
|
441
|
-
flag.
|
|
474
|
+
func handleForwardClose(args []string, socketPath string) error {
|
|
475
|
+
fs := flag.NewFlagSet("forward-close", flag.ContinueOnError)
|
|
476
|
+
fs.SetOutput(os.Stderr)
|
|
477
|
+
|
|
478
|
+
fs.Parse(args)
|
|
442
479
|
|
|
443
|
-
if
|
|
480
|
+
if fs.NArg() < 1 {
|
|
444
481
|
return fmt.Errorf("forward ID required")
|
|
445
482
|
}
|
|
446
|
-
forwardID :=
|
|
483
|
+
forwardID := fs.Arg(0)
|
|
447
484
|
|
|
448
485
|
params := protocol.ForwardCloseParams{
|
|
449
486
|
ForwardID: forwardID,
|
|
450
487
|
}
|
|
451
488
|
|
|
452
|
-
_, err := sendRequest("forward_close", params)
|
|
489
|
+
_, err := sendRequest(socketPath, "forward_close", params)
|
|
453
490
|
if err != nil {
|
|
454
491
|
return err
|
|
455
492
|
}
|
|
@@ -458,19 +495,22 @@ func handleForwardClose() error {
|
|
|
458
495
|
return nil
|
|
459
496
|
}
|
|
460
497
|
|
|
461
|
-
func handleSCP() error {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
isDownload := flag.Bool("get", false, "Download mode (remote->local)")
|
|
498
|
+
func handleSCP(args []string, socketPath string) error {
|
|
499
|
+
fs := flag.NewFlagSet("scp", flag.ContinueOnError)
|
|
500
|
+
fs.SetOutput(os.Stderr)
|
|
465
501
|
|
|
466
|
-
|
|
502
|
+
sessionID := fs.String("s", "", "Session ID")
|
|
503
|
+
isUpload := fs.Bool("put", false, "Upload mode (local->remote)")
|
|
504
|
+
isDownload := fs.Bool("get", false, "Download mode (remote->local)")
|
|
467
505
|
|
|
468
|
-
|
|
506
|
+
fs.Parse(args)
|
|
507
|
+
|
|
508
|
+
if fs.NArg() < 2 {
|
|
469
509
|
return fmt.Errorf("source and destination paths required")
|
|
470
510
|
}
|
|
471
511
|
|
|
472
|
-
source :=
|
|
473
|
-
dest :=
|
|
512
|
+
source := fs.Arg(0)
|
|
513
|
+
dest := fs.Arg(1)
|
|
474
514
|
|
|
475
515
|
if !*isUpload && !*isDownload {
|
|
476
516
|
return fmt.Errorf("must specify -put or -get")
|
|
@@ -483,7 +523,7 @@ func handleSCP() error {
|
|
|
483
523
|
IsUpload: *isUpload,
|
|
484
524
|
}
|
|
485
525
|
|
|
486
|
-
result, err := sendRequest("scp", params)
|
|
526
|
+
result, err := sendRequest(socketPath, "scp", params)
|
|
487
527
|
if err != nil {
|
|
488
528
|
return err
|
|
489
529
|
}
|
|
@@ -502,12 +542,15 @@ func handleSCP() error {
|
|
|
502
542
|
return nil
|
|
503
543
|
}
|
|
504
544
|
|
|
505
|
-
func handleSFTP() error {
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
545
|
+
func handleSFTP(args []string, socketPath string) error {
|
|
546
|
+
fs := flag.NewFlagSet("sftp", flag.ContinueOnError)
|
|
547
|
+
fs.SetOutput(os.Stderr)
|
|
548
|
+
|
|
549
|
+
sessionID := fs.String("s", "", "Session ID")
|
|
550
|
+
command := fs.String("c", "", "SFTP command (ls, mkdir, rm)")
|
|
551
|
+
path := fs.String("p", ".", "Path")
|
|
509
552
|
|
|
510
|
-
|
|
553
|
+
fs.Parse(args)
|
|
511
554
|
|
|
512
555
|
if *command == "" {
|
|
513
556
|
return fmt.Errorf("SFTP command required (-c ls|mkdir|rm)")
|
|
@@ -520,7 +563,7 @@ func handleSFTP() error {
|
|
|
520
563
|
Command: "ls",
|
|
521
564
|
Path: *path,
|
|
522
565
|
}
|
|
523
|
-
result, err := sendRequest("sftp_list", params)
|
|
566
|
+
result, err := sendRequest(socketPath, "sftp_list", params)
|
|
524
567
|
if err != nil {
|
|
525
568
|
return err
|
|
526
569
|
}
|
|
@@ -540,7 +583,7 @@ func handleSFTP() error {
|
|
|
540
583
|
Command: "mkdir",
|
|
541
584
|
Path: *path,
|
|
542
585
|
}
|
|
543
|
-
_, err := sendRequest("sftp_mkdir", params)
|
|
586
|
+
_, err := sendRequest(socketPath, "sftp_mkdir", params)
|
|
544
587
|
if err != nil {
|
|
545
588
|
return err
|
|
546
589
|
}
|
|
@@ -552,7 +595,7 @@ func handleSFTP() error {
|
|
|
552
595
|
Command: "rm",
|
|
553
596
|
Path: *path,
|
|
554
597
|
}
|
|
555
|
-
_, err := sendRequest("sftp_remove", params)
|
|
598
|
+
_, err := sendRequest(socketPath, "sftp_remove", params)
|
|
556
599
|
if err != nil {
|
|
557
600
|
return err
|
|
558
601
|
}
|
|
@@ -566,47 +609,54 @@ func handleSFTP() error {
|
|
|
566
609
|
}
|
|
567
610
|
|
|
568
611
|
func printUsage() {
|
|
569
|
-
fmt.
|
|
612
|
+
fmt.Printf(`gssh - SSH Session Manager for Agents v%s
|
|
570
613
|
|
|
571
614
|
Usage:
|
|
572
615
|
gssh connect -u user -h host [-p port] [-i key_path] [-P password] [--ask-pass]
|
|
573
|
-
gssh disconnect [
|
|
574
|
-
gssh reconnect [
|
|
575
|
-
gssh exec [-s session_id] [-S password | --ask-sudo-pass] "
|
|
616
|
+
gssh disconnect [session_id]
|
|
617
|
+
gssh reconnect [session_id]
|
|
618
|
+
gssh exec [-s session_id] [-S password | --ask-sudo-pass] "command"
|
|
576
619
|
gssh list
|
|
577
620
|
gssh use <session_id>
|
|
578
|
-
gssh forward [-s session_id] -l local_port -r remote_port
|
|
621
|
+
gssh forward [-s session_id] -l local_port -r remote_port [-R]
|
|
579
622
|
gssh forwards
|
|
580
623
|
gssh forward-close <forward_id>
|
|
581
|
-
gssh scp [-s session_id]
|
|
582
|
-
gssh
|
|
583
|
-
|
|
584
|
-
|
|
624
|
+
gssh scp [-s session_id] -put <local> <remote>
|
|
625
|
+
gssh scp [-s session_id] -get <remote> <local>
|
|
626
|
+
gssh sftp [-s session_id] -c <ls|mkdir|rm> -p <path>
|
|
627
|
+
gssh -v, --version
|
|
585
628
|
|
|
586
629
|
Options:
|
|
587
|
-
-socket path
|
|
630
|
+
-socket path Unix socket path (default: /tmp/gssh.sock)
|
|
588
631
|
-s session_id Session ID
|
|
589
632
|
-u user Username
|
|
590
633
|
-h host Host
|
|
591
634
|
-p port Port (default: 22)
|
|
592
|
-
-P password Password
|
|
593
|
-
-i key_path
|
|
594
|
-
-S password
|
|
595
|
-
--ask-
|
|
635
|
+
-P password Password
|
|
636
|
+
-i key_path SSH key path
|
|
637
|
+
-S password sudo password
|
|
638
|
+
--ask-pass Ask for password interactively
|
|
639
|
+
--ask-sudo-pass Ask for sudo password interactively
|
|
596
640
|
-l local Local port
|
|
597
641
|
-r remote Remote port
|
|
598
|
-
-R Remote port forward
|
|
599
|
-
-put Upload
|
|
600
|
-
-get Download
|
|
642
|
+
-R Remote port forward (default: local)
|
|
643
|
+
-put Upload (local -> remote)
|
|
644
|
+
-get Download (remote -> local)
|
|
601
645
|
-c command SFTP command (ls, mkdir, rm)
|
|
602
646
|
-p path Path for SFTP command
|
|
603
|
-
|
|
604
|
-
|
|
647
|
+
-v, --version Show version
|
|
648
|
+
|
|
649
|
+
Examples:
|
|
650
|
+
gssh connect -u admin -h 192.168.1.1 -P password
|
|
651
|
+
gssh exec "ls -la"
|
|
652
|
+
gssh reconnect 549b6eff-f62c-4dae-a7e9-298815233cf4
|
|
653
|
+
gssh forward -l 8080 -r 80
|
|
654
|
+
gssh scp -put local.txt /home/user/remote.txt
|
|
655
|
+
`, version)
|
|
605
656
|
}
|
|
606
657
|
|
|
607
658
|
// Restore terminal on exit
|
|
608
659
|
func init() {
|
|
609
|
-
// Setup cleanup to restore terminal state on unexpected exit
|
|
610
660
|
c := make(chan os.Signal, 1)
|
|
611
661
|
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
|
|
612
662
|
go func() {
|
|
@@ -2,6 +2,7 @@ package session
|
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"fmt"
|
|
5
|
+
"strings"
|
|
5
6
|
"sync"
|
|
6
7
|
"time"
|
|
7
8
|
|
|
@@ -45,6 +46,53 @@ func NewManager() *Manager {
|
|
|
45
46
|
}
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
// needsShell returns true if the command needs to be executed through a shell
|
|
50
|
+
func needsShell(cmd string) bool {
|
|
51
|
+
// Check for heredoc
|
|
52
|
+
if strings.Contains(cmd, "<<") {
|
|
53
|
+
return true
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Check for newlines (multi-line commands)
|
|
57
|
+
if strings.Contains(cmd, "\n") {
|
|
58
|
+
return true
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Check for pipe
|
|
62
|
+
if strings.Contains(cmd, " | ") {
|
|
63
|
+
return true
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check for redirection
|
|
67
|
+
if strings.ContainsAny(cmd, "><") {
|
|
68
|
+
if strings.Contains(cmd, ">") || strings.Contains(cmd, "<") {
|
|
69
|
+
return true
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Check for logical operators
|
|
74
|
+
if strings.Contains(cmd, " && ") || strings.Contains(cmd, " || ") {
|
|
75
|
+
return true
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Check for background execution
|
|
79
|
+
if strings.HasSuffix(strings.TrimSpace(cmd), "&") {
|
|
80
|
+
return true
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Check for command substitution
|
|
84
|
+
if strings.Contains(cmd, "$(") || strings.Contains(cmd, "`") {
|
|
85
|
+
return true
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Check for environment variables (but not $$ which is PID)
|
|
89
|
+
if strings.Contains(cmd, "${") || (strings.Contains(cmd, "$") && !strings.Contains(cmd, "$$")) {
|
|
90
|
+
return true
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return false
|
|
94
|
+
}
|
|
95
|
+
|
|
48
96
|
// Connect creates a new SSH session
|
|
49
97
|
func (m *Manager) Connect(user, host string, port int, password, keyPath string) (*protocol.Session, error) {
|
|
50
98
|
m.mu.Lock()
|
|
@@ -101,6 +149,11 @@ func (m *Manager) Connect(user, host string, port int, password, keyPath string)
|
|
|
101
149
|
|
|
102
150
|
// Disconnect closes a session
|
|
103
151
|
func (m *Manager) Disconnect(sessionID string) error {
|
|
152
|
+
// Use default session if not specified
|
|
153
|
+
if sessionID == "" {
|
|
154
|
+
sessionID = m.defaultID
|
|
155
|
+
}
|
|
156
|
+
|
|
104
157
|
m.mu.Lock()
|
|
105
158
|
defer m.mu.Unlock()
|
|
106
159
|
|
|
@@ -125,6 +178,11 @@ func (m *Manager) Disconnect(sessionID string) error {
|
|
|
125
178
|
|
|
126
179
|
// Reconnect reconnects a session
|
|
127
180
|
func (m *Manager) Reconnect(sessionID string) (*protocol.Session, error) {
|
|
181
|
+
// Use default session if not specified
|
|
182
|
+
if sessionID == "" {
|
|
183
|
+
sessionID = m.defaultID
|
|
184
|
+
}
|
|
185
|
+
|
|
128
186
|
m.mu.Lock()
|
|
129
187
|
ms, ok := m.sessions[sessionID]
|
|
130
188
|
m.mu.Unlock()
|
|
@@ -194,9 +252,12 @@ func (m *Manager) Exec(sessionID, command string) (*protocol.ExecResult, error)
|
|
|
194
252
|
}
|
|
195
253
|
defer session.Close()
|
|
196
254
|
|
|
197
|
-
//
|
|
198
|
-
|
|
255
|
+
// 检测是否需要通过 shell 执行
|
|
199
256
|
fullCmd := command
|
|
257
|
+
if needsShell(command) {
|
|
258
|
+
// 使用 /bin/sh 更通用
|
|
259
|
+
fullCmd = fmt.Sprintf("/bin/sh -c %q", command)
|
|
260
|
+
}
|
|
200
261
|
|
|
201
262
|
output, err := session.CombinedOutput(fullCmd)
|
|
202
263
|
if err != nil {
|