gssh-agent 1.0.0 → 1.0.2

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/README.md CHANGED
@@ -10,6 +10,7 @@ gssh 是一个供 Agent 使用的 SSH Session 管理工具。通过 Go 语言实
10
10
  - 断线自动重连
11
11
  - 支持密码认证和 SSH 密钥认证
12
12
  - 被连接机器零配置(普通 SSH 即可)
13
+ - SFTP 文件传输支持(上传/下载)
13
14
 
14
15
  ## 安装
15
16
 
@@ -124,6 +125,25 @@ gssh exec -s <session_id> "pwd"
124
125
  gssh forward -l 8080 -r 80
125
126
  ```
126
127
 
128
+ ### 文件传输(SFTP/SCP)
129
+
130
+ ```bash
131
+ # 上传文件(本地 -> 远程)
132
+ gssh scp -put /path/to/local/file.txt /path/to/remote/file.txt
133
+
134
+ # 下载文件(远程 -> 本地)
135
+ gssh scp -get /path/to/remote/file.txt /path/to/local/file.txt
136
+
137
+ # 列出远程目录
138
+ gssh sftp -c ls -p /path/to/remote/dir
139
+
140
+ # 创建远程目录
141
+ gssh sftp -c mkdir -p /path/to/remote/newdir
142
+
143
+ # 删除远程文件
144
+ gssh sftp -c rm -p /path/to/remote/file.txt
145
+ ```
146
+
127
147
  ### Session 管理
128
148
 
129
149
  ```bash
@@ -154,6 +174,10 @@ gssh reconnect -s <session_id>
154
174
  | `-l local` | 本地端口 |
155
175
  | `-r remote` | 远程端口 |
156
176
  | `-R` | 远程端口转发 |
177
+ | `-put` | 上传模式(本地 -> 远程) |
178
+ | `-get` | 下载模式(远程 -> 本地) |
179
+ | `-c command` | SFTP 命令(ls/mkdir/rm) |
180
+ | `-p path` | SFTP 路径 |
157
181
 
158
182
  ## 开发
159
183
 
package/bin/daemon CHANGED
Binary file
package/bin/gssh CHANGED
Binary file
package/bin/gssh-daemon CHANGED
Binary file
package/cmd/gssh/main.go CHANGED
@@ -7,9 +7,13 @@ import (
7
7
  "fmt"
8
8
  "net"
9
9
  "os"
10
+ "os/signal"
10
11
  "strings"
12
+ "syscall"
11
13
 
12
14
  "gssh/internal/protocol"
15
+
16
+ "golang.org/x/term"
13
17
  )
14
18
 
15
19
  const (
@@ -50,6 +54,10 @@ func main() {
50
54
  err = handleForwards()
51
55
  case "forward-close":
52
56
  err = handleForwardClose()
57
+ case "scp":
58
+ err = handleSCP()
59
+ case "sftp":
60
+ err = handleSFTP()
53
61
  case "help", "-h", "--help":
54
62
  printUsage()
55
63
  default:
@@ -64,6 +72,50 @@ func main() {
64
72
  }
65
73
  }
66
74
 
75
+ // readPassword reads password from terminal without echo
76
+ func readPassword() (string, error) {
77
+ // Check if stdin is a terminal
78
+ if !term.IsTerminal(int(os.Stdin.Fd())) {
79
+ // Try to read from stdin
80
+ reader := bufio.NewReader(os.Stdin)
81
+ fmt.Print("Password: ")
82
+ password, err := reader.ReadString('\n')
83
+ if err != nil {
84
+ return "", err
85
+ }
86
+ return strings.TrimSuffix(password, "\n"), nil
87
+ }
88
+
89
+ fmt.Print("Password: ")
90
+ bytePassword, err := term.ReadPassword(int(os.Stdin.Fd()))
91
+ fmt.Println()
92
+ if err != nil {
93
+ return "", err
94
+ }
95
+ return string(bytePassword), nil
96
+ }
97
+
98
+ // readPassphrase reads passphrase from terminal without echo
99
+ func readPassphrase() (string, error) {
100
+ if !term.IsTerminal(int(os.Stdin.Fd())) {
101
+ reader := bufio.NewReader(os.Stdin)
102
+ fmt.Print("Key passphrase: ")
103
+ passphrase, err := reader.ReadString('\n')
104
+ if err != nil {
105
+ return "", err
106
+ }
107
+ return strings.TrimSuffix(passphrase, "\n"), nil
108
+ }
109
+
110
+ fmt.Print("Key passphrase: ")
111
+ bytePassphrase, err := term.ReadPassword(int(os.Stdin.Fd()))
112
+ fmt.Println()
113
+ if err != nil {
114
+ return "", err
115
+ }
116
+ return string(bytePassphrase), nil
117
+ }
118
+
67
119
  func sendRequest(method string, params interface{}) ([]byte, error) {
68
120
  conn, err := net.Dial("unix", *socketPath)
69
121
  if err != nil {
@@ -129,6 +181,8 @@ func handleConnect() error {
129
181
  port := flag.Int("p", 22, "Port")
130
182
  password := flag.String("P", "", "Password")
131
183
  keyPath := flag.String("i", "", "SSH key path")
184
+ askPassword := flag.Bool("ask-pass", false, "Ask for password interactively")
185
+ askPassphrase := flag.Bool("ask-passphrase", false, "Ask for key passphrase interactively")
132
186
 
133
187
  flag.CommandLine.Parse(flag.Args()[1:])
134
188
 
@@ -136,6 +190,28 @@ func handleConnect() error {
136
190
  return fmt.Errorf("user and host are required")
137
191
  }
138
192
 
193
+ // If no password provided via flag and -ask-pass is set, read interactively
194
+ if *password == "" && *askPassword {
195
+ p, err := readPassword()
196
+ if err != nil {
197
+ return fmt.Errorf("failed to read password: %w", err)
198
+ }
199
+ *password = p
200
+ }
201
+
202
+ // If key path provided and -ask-passphrase is set, read interactively
203
+ if *keyPath != "" && *askPassphrase {
204
+ passphrase, err := readPassphrase()
205
+ if err != nil {
206
+ return fmt.Errorf("failed to read passphrase: %w", err)
207
+ }
208
+ // Note: Passphrase support would require modifying the SSH client
209
+ // For now, we'll just warn that it's not supported
210
+ if passphrase != "" {
211
+ fmt.Println("Note: Passphrase for keys is not yet supported, ignoring")
212
+ }
213
+ }
214
+
139
215
  params := protocol.ConnectParams{
140
216
  User: *user,
141
217
  Host: *host,
@@ -202,6 +278,9 @@ func handleExec() error {
202
278
  args := flag.Args()[1:]
203
279
 
204
280
  sessionID := flag.String("s", "", "Session ID")
281
+ sudoPassword := flag.String("S", "", "sudo password (will prompt if empty and -s flag used)")
282
+ askSudoPassword := flag.Bool("ask-sudo-pass", false, "Interactively ask for sudo password")
283
+
205
284
  flag.CommandLine.Parse(args)
206
285
 
207
286
  if flag.NArg() < 1 {
@@ -209,6 +288,21 @@ func handleExec() error {
209
288
  }
210
289
  command := strings.Join(flag.Args(), " ")
211
290
 
291
+ // If sudo password needed but not provided, prompt for it
292
+ if *askSudoPassword && *sudoPassword == "" {
293
+ p, err := readPassword()
294
+ if err != nil {
295
+ return fmt.Errorf("failed to read sudo password: %w", err)
296
+ }
297
+ *sudoPassword = p
298
+ }
299
+
300
+ // If command contains sudo and password provided, wrap the command
301
+ if *sudoPassword != "" && strings.Contains(command, "sudo") {
302
+ // Use printf to pipe password to sudo -S
303
+ command = fmt.Sprintf("printf '%%s\\n' '%s' | %s -S", *sudoPassword, command)
304
+ }
305
+
212
306
  params := protocol.ExecParams{
213
307
  SessionID: *sessionID,
214
308
  Command: command,
@@ -364,31 +458,160 @@ func handleForwardClose() error {
364
458
  return nil
365
459
  }
366
460
 
461
+ func handleSCP() error {
462
+ sessionID := flag.String("s", "", "Session ID")
463
+ isUpload := flag.Bool("put", false, "Upload mode (local->remote)")
464
+ isDownload := flag.Bool("get", false, "Download mode (remote->local)")
465
+
466
+ flag.CommandLine.Parse(flag.Args()[1:])
467
+
468
+ if flag.NArg() < 2 {
469
+ return fmt.Errorf("source and destination paths required")
470
+ }
471
+
472
+ source := flag.Arg(0)
473
+ dest := flag.Arg(1)
474
+
475
+ if !*isUpload && !*isDownload {
476
+ return fmt.Errorf("must specify -put or -get")
477
+ }
478
+
479
+ params := protocol.SCPParams{
480
+ SessionID: *sessionID,
481
+ Source: source,
482
+ Dest: dest,
483
+ IsUpload: *isUpload,
484
+ }
485
+
486
+ result, err := sendRequest("scp", params)
487
+ if err != nil {
488
+ return err
489
+ }
490
+
491
+ var scpResult protocol.SCPResult
492
+ if err := json.Unmarshal(result, &scpResult); err != nil {
493
+ return fmt.Errorf("failed to parse result: %w", err)
494
+ }
495
+
496
+ if scpResult.Success {
497
+ fmt.Printf("Success: %s\n", scpResult.Message)
498
+ } else {
499
+ return fmt.Errorf("failed: %s", scpResult.Message)
500
+ }
501
+
502
+ return nil
503
+ }
504
+
505
+ func handleSFTP() error {
506
+ sessionID := flag.String("s", "", "Session ID")
507
+ command := flag.String("c", "", "SFTP command (ls, mkdir, rm)")
508
+ path := flag.String("p", ".", "Path")
509
+
510
+ flag.CommandLine.Parse(flag.Args()[1:])
511
+
512
+ if *command == "" {
513
+ return fmt.Errorf("SFTP command required (-c ls|mkdir|rm)")
514
+ }
515
+
516
+ switch *command {
517
+ case "ls":
518
+ params := protocol.SFTPParams{
519
+ SessionID: *sessionID,
520
+ Command: "ls",
521
+ Path: *path,
522
+ }
523
+ result, err := sendRequest("sftp_list", params)
524
+ if err != nil {
525
+ return err
526
+ }
527
+
528
+ var files []string
529
+ if err := json.Unmarshal(result, &files); err != nil {
530
+ return fmt.Errorf("failed to parse result: %w", err)
531
+ }
532
+
533
+ for _, f := range files {
534
+ fmt.Println(f)
535
+ }
536
+
537
+ case "mkdir":
538
+ params := protocol.SFTPParams{
539
+ SessionID: *sessionID,
540
+ Command: "mkdir",
541
+ Path: *path,
542
+ }
543
+ _, err := sendRequest("sftp_mkdir", params)
544
+ if err != nil {
545
+ return err
546
+ }
547
+ fmt.Printf("Directory created: %s\n", *path)
548
+
549
+ case "rm":
550
+ params := protocol.SFTPParams{
551
+ SessionID: *sessionID,
552
+ Command: "rm",
553
+ Path: *path,
554
+ }
555
+ _, err := sendRequest("sftp_remove", params)
556
+ if err != nil {
557
+ return err
558
+ }
559
+ fmt.Printf("File removed: %s\n", *path)
560
+
561
+ default:
562
+ return fmt.Errorf("unknown SFTP command: %s", *command)
563
+ }
564
+
565
+ return nil
566
+ }
567
+
367
568
  func printUsage() {
368
569
  fmt.Println(`gssh - SSH Session Manager for Agents (Stateless)
369
570
 
370
571
  Usage:
371
- gssh connect -u user -h host [-p port] [-i key_path] [-P password]
572
+ gssh connect -u user -h host [-p port] [-i key_path] [-P password] [--ask-pass]
372
573
  gssh disconnect [-s session_id]
373
574
  gssh reconnect [-s session_id]
374
- gssh exec [-s session_id] "command"
575
+ gssh exec [-s session_id] [-S password | --ask-sudo-pass] "sudo command"
375
576
  gssh list
376
577
  gssh use <session_id>
377
578
  gssh forward [-s session_id] -l local_port -r remote_port
378
579
  gssh forwards
379
580
  gssh forward-close <forward_id>
581
+ gssh scp [-s session_id] [-put|-get] <source> <dest>
582
+ gssh sftp [-s session_id] -c <command> -p <path>
380
583
 
381
584
  Note: For sudo commands, use key-based authentication or configure passwordless sudo.
382
585
 
383
586
  Options:
384
- -socket path Unix socket path (default: /tmp/gssh.sock)
385
- -s session_id Session ID
386
- -u user Username
387
- -h host Host
388
- -p port Port (default: 22)
389
- -P password Password
390
- -i key_path SSH key path
391
- -l local Local port
392
- -r remote Remote port
393
- -R Remote port forward`)
587
+ -socket path Unix socket path (default: /tmp/gssh.sock)
588
+ -s session_id Session ID
589
+ -u user Username
590
+ -h host Host
591
+ -p port Port (default: 22)
592
+ -P password Password (or use --ask-pass for interactive input)
593
+ -i key_path SSH key path
594
+ -S password sudo password (for executing sudo commands)
595
+ --ask-sudo-pass Interactively ask for sudo password
596
+ -l local Local port
597
+ -r remote Remote port
598
+ -R Remote port forward
599
+ -put Upload mode (local -> remote)
600
+ -get Download mode (remote -> local)
601
+ -c command SFTP command (ls, mkdir, rm)
602
+ -p path Path for SFTP command
603
+ --ask-pass Interactively ask for SSH password (secure)
604
+ --ask-passphrase Interactively ask for key passphrase (secure)`)
605
+ }
606
+
607
+ // Restore terminal on exit
608
+ func init() {
609
+ // Setup cleanup to restore terminal state on unexpected exit
610
+ c := make(chan os.Signal, 1)
611
+ signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
612
+ go func() {
613
+ <-c
614
+ term.Restore(int(os.Stdin.Fd()), &term.State{})
615
+ os.Exit(1)
616
+ }()
394
617
  }
package/go.mod CHANGED
@@ -4,7 +4,12 @@ go 1.21
4
4
 
5
5
  require (
6
6
  github.com/google/uuid v1.5.0
7
+ github.com/pkg/sftp v1.13.6
7
8
  golang.org/x/crypto v0.18.0
9
+ golang.org/x/term v0.16.0
8
10
  )
9
11
 
10
- require golang.org/x/sys v0.16.0 // indirect
12
+ require (
13
+ github.com/kr/fs v0.1.0 // indirect
14
+ golang.org/x/sys v0.16.0 // indirect
15
+ )
package/go.sum CHANGED
@@ -1,8 +1,54 @@
1
+ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2
+ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
3
+ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1
4
  github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
2
5
  github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
6
+ github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
7
+ github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
8
+ github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
9
+ github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
10
+ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
11
+ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
12
+ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
13
+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
14
+ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
15
+ github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
16
+ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
17
+ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
18
+ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
19
+ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
20
+ golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
3
21
  golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
4
22
  golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
23
+ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
24
+ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
25
+ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
26
+ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
27
+ golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
28
+ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
29
+ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
30
+ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
31
+ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
32
+ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
33
+ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
34
+ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
35
+ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
5
36
  golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
6
37
  golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
38
+ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
39
+ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
40
+ golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
7
41
  golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
8
42
  golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
43
+ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
44
+ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
45
+ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
46
+ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
47
+ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
48
+ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
49
+ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
50
+ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
51
+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
52
+ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
53
+ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
54
+ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=