gssh-agent 1.0.3 → 1.0.7

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.
@@ -0,0 +1,27 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - '**'
7
+ pull_request:
8
+ branches:
9
+ - main
10
+
11
+ jobs:
12
+ test:
13
+ runs-on: ubuntu-latest
14
+
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+
18
+ - name: Set up Go
19
+ uses: actions/setup-go@v5
20
+ with:
21
+ go-version: '1.21'
22
+
23
+ - name: Build
24
+ run: go build ./...
25
+
26
+ - name: Test
27
+ run: go test ./...
@@ -0,0 +1,104 @@
1
+ name: Publish
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ jobs:
9
+ # npm 发布
10
+ publish-npm:
11
+ if: "contains(github.event.head_commit.message, 'publish')"
12
+ runs-on: ubuntu-latest
13
+ permissions:
14
+ contents: read
15
+ id-token: write
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - name: Set up Go
20
+ uses: actions/setup-go@v5
21
+ with:
22
+ go-version: '1.21'
23
+
24
+ - name: Get version
25
+ id: version
26
+ run: |
27
+ VERSION=$(echo "${{ github.event.head_commit.message }}" | sed -n 's/.*publish[: ]*\(.*\)/\1/p')
28
+ echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
29
+ echo "Publishing version: $VERSION"
30
+
31
+ - name: Build
32
+ run: |
33
+ go build -ldflags="-s -w" -o bin/gssh ./cmd/gssh
34
+
35
+ - name: Update package.json version
36
+ run: |
37
+ VERSION="${{ steps.version.outputs.VERSION }}"
38
+ npm version $VERSION --no-git-tag-version
39
+
40
+ - name: Build npm package
41
+ run: npm pack
42
+
43
+ - name: Setup Node
44
+ uses: actions/setup-node@v4
45
+ with:
46
+ node-version: '20'
47
+
48
+ - name: Configure npm
49
+ run: |
50
+ echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc
51
+ cat .npmrc
52
+
53
+ - name: Publish to npm
54
+ run: npm publish --access public
55
+
56
+ # GitHub Releases 发布
57
+ publish-github:
58
+ if: "startsWith(github.ref, 'refs/tags/v')"
59
+ runs-on: ubuntu-latest
60
+ permissions:
61
+ contents: write
62
+
63
+ strategy:
64
+ matrix:
65
+ include:
66
+ - goos: linux
67
+ goarch: amd64
68
+ - goos: linux
69
+ goarch: arm64
70
+ - goos: darwin
71
+ goarch: amd64
72
+ - goos: darwin
73
+ goarch: arm64
74
+ - goos: windows
75
+ goarch: amd64
76
+
77
+ steps:
78
+ - uses: actions/checkout@v4
79
+
80
+ - name: Set up Go
81
+ uses: actions/setup-go@v5
82
+ with:
83
+ go-version: '1.21'
84
+
85
+ - name: Build
86
+ env:
87
+ GOOS: ${{ matrix.goos }}
88
+ GOARCH: ${{ matrix.goarch }}
89
+ run: |
90
+ EXT=""
91
+ if [ "$GOOS" = "windows" ]; then
92
+ EXT=".exe"
93
+ fi
94
+ FILENAME="gssh-${GOOS}-${GOARCH}${EXT}"
95
+ go build -ldflags="-s -w" -o "$FILENAME" ./cmd/gssh
96
+ echo "$FILENAME" >> artifacts.txt
97
+
98
+ - name: Upload Release Asset
99
+ uses: softprops/action-gh-release@v2
100
+ with:
101
+ tag_name: ${{ github.ref }}
102
+ files: ${{ matrix.goos }}-${{ matrix.goarch }}*
103
+ env:
104
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
package/bin/gssh CHANGED
Binary file
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 before parsing
36
- for _, arg := range os.Args[1:] {
37
- if arg == "-h" || arg == "--help" {
38
- printUsage()
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.Parse()
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 flag.NArg() < 1 {
48
+ if len(args) < 1 {
46
49
  printUsage()
47
50
  os.Exit(1)
48
51
  }
49
52
 
50
- cmd := flag.Arg(0)
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()
75
+ err = handleForwardClose(subArgs, socketPath)
72
76
  case "scp":
73
- err = handleSCP()
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", *socketPath)
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
- user := flag.String("u", "", "Username")
195
- host := flag.String("h", "", "Host")
196
- port := flag.Int("p", 22, "Port")
197
- password := flag.String("P", "", "Password")
198
- keyPath := flag.String("i", "", "SSH key path")
199
- askPassword := flag.Bool("ask-pass", false, "Ask for password interactively")
200
- askPassphrase := flag.Bool("ask-passphrase", false, "Ask for key passphrase interactively")
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
- flag.CommandLine.Parse(flag.Args()[1:])
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
- sessionID := flag.String("s", "", "Session ID")
254
- flag.CommandLine.Parse(flag.Args()[1:])
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: *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
- sessionID := flag.String("s", "", "Session ID")
271
- flag.CommandLine.Parse(flag.Args()[1:])
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: *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
- // Get remaining args after "exec"
293
- args := flag.Args()[1:]
307
+ func handleExec(args []string, socketPath string) error {
308
+ fs := flag.NewFlagSet("exec", flag.ContinueOnError)
309
+ fs.SetOutput(os.Stderr)
294
310
 
295
- sessionID := flag.String("s", "", "Session ID")
296
- sudoPassword := flag.String("S", "", "sudo password (will prompt if empty and -s flag used)")
297
- askSudoPassword := flag.Bool("ask-sudo-pass", false, "Interactively ask for sudo password")
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
- flag.CommandLine.Parse(args)
316
+ fs.Parse(args)
300
317
 
301
- if flag.NArg() < 1 {
318
+ if fs.NArg() < 1 {
302
319
  return fmt.Errorf("command required")
303
320
  }
304
- command := strings.Join(flag.Args(), " ")
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.CommandLine.Parse(flag.Args()[1:])
387
+ func handleUse(args []string, socketPath string) error {
388
+ fs := flag.NewFlagSet("use", flag.ContinueOnError)
389
+ fs.SetOutput(os.Stderr)
374
390
 
375
- if flag.NArg() < 1 {
391
+ fs.Parse(args)
392
+
393
+ if fs.NArg() < 1 {
376
394
  return fmt.Errorf("session ID required")
377
395
  }
378
- sessionIDStr := flag.Arg(0)
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
- sessionID := flag.String("s", "", "Session ID")
395
- local := flag.Int("l", 0, "Local port")
396
- remote := flag.Int("r", 0, "Remote port")
397
- isRemote := flag.Bool("R", false, "Remote port forward")
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
- flag.CommandLine.Parse(flag.Args()[1:])
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.CommandLine.Parse(flag.Args()[1:])
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 flag.NArg() < 1 {
482
+ if fs.NArg() < 1 {
459
483
  return fmt.Errorf("forward ID required")
460
484
  }
461
- forwardID := flag.Arg(0)
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
- sessionID := flag.String("s", "", "Session ID")
478
- isUpload := flag.Bool("put", false, "Upload mode (local->remote)")
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
- flag.CommandLine.Parse(flag.Args()[1:])
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
- if flag.NArg() < 2 {
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 := flag.Arg(0)
488
- dest := flag.Arg(1)
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
- sessionID := flag.String("s", "", "Session ID")
522
- command := flag.String("c", "", "SFTP command (ls, mkdir, rm)")
523
- path := flag.String("p", ".", "Path")
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
- flag.CommandLine.Parse(flag.Args()[1:])
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 [-s session_id]
589
- gssh reconnect [-s session_id]
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>
@@ -621,16 +651,14 @@ Options:
621
651
  Examples:
622
652
  gssh connect -u admin -h 192.168.1.1 -P password
623
653
  gssh exec "ls -la"
654
+ gssh reconnect 549b6eff-f62c-4dae-a7e9-298815233cf4
624
655
  gssh forward -l 8080 -r 80
625
- gssh forward -l 9000 -r 3000 -R
626
656
  gssh scp -put local.txt /home/user/remote.txt
627
- gssh sftp -c ls -p /home/user
628
657
  `, version)
629
658
  }
630
659
 
631
660
  // Restore terminal on exit
632
661
  func init() {
633
- // Setup cleanup to restore terminal state on unexpected exit
634
662
  c := make(chan os.Signal, 1)
635
663
  signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
636
664
  go func() {
@@ -6,6 +6,7 @@ import (
6
6
  "log"
7
7
  "net"
8
8
  "sync"
9
+ "time"
9
10
 
10
11
  "golang.org/x/crypto/ssh"
11
12
  )
@@ -74,14 +75,22 @@ func (f *Forwarder) startLocalForward() {
74
75
  }
75
76
  f.mu.RUnlock()
76
77
 
78
+ // Set deadline to prevent blocking forever
79
+ f.listener.(*net.TCPListener).SetDeadline(time.Now().Add(5 * time.Second))
80
+
77
81
  conn, err := f.listener.Accept()
78
82
  if err != nil {
83
+ // Check if it's a timeout
84
+ if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
85
+ continue
86
+ }
79
87
  f.mu.RLock()
80
88
  closed := f.closed
81
89
  f.mu.RUnlock()
82
90
  if closed {
83
91
  return
84
92
  }
93
+ log.Printf("[portforward] Accept error: %v", err)
85
94
  continue
86
95
  }
87
96
 
@@ -105,8 +114,10 @@ func (f *Forwarder) handleLocalConnection(localConn net.Conn) {
105
114
 
106
115
  log.Printf("[portforward] Connection accepted from %s", localConn.RemoteAddr())
107
116
 
108
- remoteAddr := fmt.Sprintf("localhost:%d", f.RemotePort)
109
- remoteConn, err := f.sshClient.Dial("tcp", remoteAddr)
117
+ // Use 127.0.0.1 instead of localhost to avoid IPv6 issues
118
+ remoteAddr := fmt.Sprintf("127.0.0.1:%d", f.RemotePort)
119
+
120
+ remoteConn, err := net.DialTimeout("tcp", remoteAddr, 5*time.Second)
110
121
  if err != nil {
111
122
  log.Printf("[portforward] Failed to connect to remote %s: %v", remoteAddr, err)
112
123
  return
@@ -136,27 +147,41 @@ func (f *Forwarder) startRemoteForward() {
136
147
  defer f.wg.Done()
137
148
 
138
149
  // Request the SSH server to listen on remote:remotePort and forward connections to us
139
- // Payload format: string (address) + uint32 (port)
140
150
  addr := fmt.Sprintf(":%d", f.RemotePort)
141
151
  payload := ssh.Marshal(struct {
142
152
  Addr string
143
153
  Port uint32
144
154
  }{Addr: addr, Port: uint32(f.RemotePort)})
145
155
 
146
- ok, _, err := f.sshClient.SendRequest("tcpip-forward", true, payload)
147
- if err != nil {
148
- log.Printf("[portforward] Failed to send tcpip-forward request: %v", err)
149
- return
156
+ // Use timeout for the request
157
+ type result struct {
158
+ ok bool
159
+ err error
150
160
  }
151
- if !ok {
152
- log.Printf("[portforward] SSH server rejected tcpip-forward request for port %d", f.RemotePort)
161
+ resultCh := make(chan result, 1)
162
+ go func() {
163
+ ok, _, err := f.sshClient.SendRequest("tcpip-forward", true, payload)
164
+ resultCh <- result{ok: ok, err: err}
165
+ }()
166
+
167
+ select {
168
+ case r := <-resultCh:
169
+ if r.err != nil {
170
+ log.Printf("[portforward] Failed to send tcpip-forward request: %v", r.err)
171
+ return
172
+ }
173
+ if !r.ok {
174
+ log.Printf("[portforward] SSH server rejected tcpip-forward request for port %d", f.RemotePort)
175
+ return
176
+ }
177
+ case <-time.After(5 * time.Second):
178
+ log.Printf("[portforward] tcpip-forward request timed out")
153
179
  return
154
180
  }
155
181
 
156
182
  log.Printf("[portforward] Remote forward: SSH server listening on port %d", f.RemotePort)
157
183
 
158
- // Now we need to accept forwarded connections from the SSH server
159
- // The SSH server will open a "forwarded-tcpip" channel for each connection
184
+ // Accept forwarded connections in a loop
160
185
  for {
161
186
  f.mu.RLock()
162
187
  if f.closed {
@@ -165,7 +190,7 @@ func (f *Forwarder) startRemoteForward() {
165
190
  }
166
191
  f.mu.RUnlock()
167
192
 
168
- // Wait for a forwarded connection
193
+ // Wait for a forwarded connection with timeout
169
194
  ch, reqs, err := f.sshClient.OpenChannel("forwarded-tcpip", nil)
170
195
  if err != nil {
171
196
  f.mu.RLock()
@@ -174,7 +199,9 @@ func (f *Forwarder) startRemoteForward() {
174
199
  if closed {
175
200
  return
176
201
  }
202
+ // Log and continue - don't block
177
203
  log.Printf("[portforward] Error accepting forwarded connection: %v", err)
204
+ time.Sleep(1 * time.Second)
178
205
  continue
179
206
  }
180
207
 
@@ -182,8 +209,8 @@ func (f *Forwarder) startRemoteForward() {
182
209
  go ssh.DiscardRequests(reqs)
183
210
 
184
211
  // Connect to local port
185
- localAddr := fmt.Sprintf("localhost:%d", f.LocalPort)
186
- localConn, err := net.Dial("tcp", localAddr)
212
+ localAddr := fmt.Sprintf("127.0.0.1:%d", f.LocalPort)
213
+ localConn, err := net.DialTimeout("tcp", localAddr, 5*time.Second)
187
214
  if err != nil {
188
215
  log.Printf("[portforward] Failed to connect to local port %d: %v", f.LocalPort, err)
189
216
  ch.Close()
@@ -72,6 +72,7 @@ type ReconnectParams struct {
72
72
  type ExecParams struct {
73
73
  SessionID string `json:"session_id,omitempty"`
74
74
  Command string `json:"command"`
75
+ Timeout int `json:"timeout,omitempty"` // 超时时间(秒),0 表示无超时
75
76
  }
76
77
 
77
78
  type ForwardParams struct {
@@ -108,6 +109,7 @@ type SCPResult struct {
108
109
  // SFTPParams represents SFTP command parameters
109
110
  type SFTPParams struct {
110
111
  SessionID string `json:"session_id,omitempty"`
111
- Command string `json:"command"` // "ls", "cd", "pwd", "mkdir", "rm", "rmdir"
112
+ Command string `json:"command"`
113
+ Timeout int `json:"timeout,omitempty"` // 超时时间(秒),0 表示无超时 // "ls", "cd", "pwd", "mkdir", "rm", "rmdir"
112
114
  Path string `json:"path"`
113
115
  }
@@ -65,7 +65,6 @@ func needsShell(cmd string) bool {
65
65
 
66
66
  // Check for redirection
67
67
  if strings.ContainsAny(cmd, "><") {
68
- // Make sure it's not in a string context
69
68
  if strings.Contains(cmd, ">") || strings.Contains(cmd, "<") {
70
69
  return true
71
70
  }
@@ -150,6 +149,11 @@ func (m *Manager) Connect(user, host string, port int, password, keyPath string)
150
149
 
151
150
  // Disconnect closes a session
152
151
  func (m *Manager) Disconnect(sessionID string) error {
152
+ // Use default session if not specified
153
+ if sessionID == "" {
154
+ sessionID = m.defaultID
155
+ }
156
+
153
157
  m.mu.Lock()
154
158
  defer m.mu.Unlock()
155
159
 
@@ -174,6 +178,11 @@ func (m *Manager) Disconnect(sessionID string) error {
174
178
 
175
179
  // Reconnect reconnects a session
176
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
+
177
186
  m.mu.Lock()
178
187
  ms, ok := m.sessions[sessionID]
179
188
  m.mu.Unlock()
@@ -215,7 +224,7 @@ func (m *Manager) Reconnect(sessionID string) (*protocol.Session, error) {
215
224
  }
216
225
 
217
226
  // Exec executes a command on a session
218
- func (m *Manager) Exec(sessionID, command string) (*protocol.ExecResult, error) {
227
+ func (m *Manager) Exec(sessionID, command string, timeout int) (*protocol.ExecResult, error) {
219
228
  m.mu.RLock()
220
229
  var ms *ManagedSession
221
230
  if sessionID != "" {
@@ -246,12 +255,38 @@ func (m *Manager) Exec(sessionID, command string) (*protocol.ExecResult, error)
246
255
  // 检测是否需要通过 shell 执行
247
256
  fullCmd := command
248
257
  if needsShell(command) {
249
- fullCmd = fmt.Sprintf("/bin/zsh -c %q", command)
258
+ // 使用 /bin/sh 更通用
259
+ fullCmd = fmt.Sprintf("/bin/sh -c %q", command)
260
+ }
261
+
262
+ // 执行命令,支持超时
263
+ var output []byte
264
+ var exitErr *ssh.ExitError
265
+ var ok bool
266
+
267
+ if timeout > 0 {
268
+ done := make(chan struct{})
269
+ go func() {
270
+ output, err = session.CombinedOutput(fullCmd)
271
+ close(done)
272
+ }()
273
+ select {
274
+ case <-done:
275
+ // 命令执行完成
276
+ case <-time.After(time.Duration(timeout) * time.Second):
277
+ session.Signal(ssh.SIGKILL)
278
+ return &protocol.ExecResult{
279
+ Stdout: string(output),
280
+ Stderr: "",
281
+ ExitCode: -1,
282
+ }, fmt.Errorf("command timed out after %d seconds", timeout)
283
+ }
284
+ } else {
285
+ output, err = session.CombinedOutput(fullCmd)
250
286
  }
251
287
 
252
- output, err := session.CombinedOutput(fullCmd)
253
288
  if err != nil {
254
- exitErr, ok := err.(*ssh.ExitError)
289
+ exitErr, ok = err.(*ssh.ExitError)
255
290
  if ok {
256
291
  ms.LastCmd = command
257
292
  return &protocol.ExecResult{
package/package.json CHANGED
@@ -1,10 +1,9 @@
1
1
  {
2
2
  "name": "gssh-agent",
3
- "version": "1.0.3",
3
+ "version": "1.0.7",
4
4
  "description": "SSH Session Manager for Agents - Stateless SSH client with SFTP support",
5
5
  "bin": {
6
- "gssh": "./bin/gssh",
7
- "gssh-daemon": "./bin/gssh-daemon"
6
+ "gssh": "./bin/gssh"
8
7
  },
9
8
  "repository": {
10
9
  "type": "git",
@@ -60,7 +60,7 @@ func (h *Handler) Handle(data []byte) ([]byte, error) {
60
60
  if err := json.Unmarshal(req.Params, &params); err != nil {
61
61
  return h.errorResponse(req.ID, -32602, "Invalid params")
62
62
  }
63
- result, err = h.manager.Exec(params.SessionID, params.Command)
63
+ result, err = h.manager.Exec(params.SessionID, params.Command, params.Timeout)
64
64
 
65
65
  case "list":
66
66
  result = h.manager.List()
package/skill.md CHANGED
@@ -41,8 +41,14 @@ gssh exec "ls -la"
41
41
 
42
42
  # sudo 命令
43
43
  gssh exec -S password "sudo systemctl restart nginx"
44
+
45
+ # 后台运行(推荐格式)
46
+ gssh exec "nohup python3 app.py > /dev/null 2>&1 &"
47
+ gssh exec "python3 app.py &"
44
48
  ```
45
49
 
50
+ **后台运行说明**:推荐使用 `nohup command > /dev/null 2>&1 &` 格式,可确保命令立即返回且不阻塞。
51
+
46
52
  ## 文件传输
47
53
 
48
54
  ```bash
package/bin/daemon DELETED
Binary file
package/bin/gssh-daemon DELETED
Binary file