gssh-agent 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,175 @@
1
+ # gssh - SSH Session Manager for Agents
2
+
3
+ gssh 是一个供 Agent 使用的 SSH Session 管理工具。通过 Go 语言实现,支持多个 SSH 会话管理、命令执行、端口转发和自动重连。
4
+
5
+ ## 特性
6
+
7
+ - 多个 SSH Session 并发管理
8
+ - 命令执行与结果透传(stdout/stderr/exit code)
9
+ - TCP 端口转发(本地/远程)
10
+ - 断线自动重连
11
+ - 支持密码认证和 SSH 密钥认证
12
+ - 被连接机器零配置(普通 SSH 即可)
13
+
14
+ ## 安装
15
+
16
+ ### 方式一:Homebrew(推荐)
17
+
18
+ ```bash
19
+ # 添加 tap(未来会支持)
20
+ # brew tap forechoandlook/gssh
21
+ # brew install gssh
22
+
23
+ # 暂时使用手动安装方式
24
+ git clone https://github.com/forechoandlook/gssh.git
25
+ cd gssh
26
+ go build -o bin/daemon cmd/daemon/main.go
27
+ go build -o bin/gssh cmd/gssh/main.go
28
+ ./homebrew/install.sh
29
+ ```
30
+
31
+ ### 方式二:直接安装
32
+
33
+ ```bash
34
+ # 克隆项目
35
+ git clone https://github.com/forechoandlook/gssh.git
36
+ cd gssh
37
+
38
+ # 构建
39
+ go build -o bin/daemon cmd/daemon/main.go
40
+ go build -o bin/gssh cmd/gssh/main.go
41
+
42
+ # 复制到系统路径
43
+ cp bin/daemon /usr/local/bin/gssh-daemon
44
+ cp bin/gssh /usr/local/bin/gssh
45
+ ```
46
+
47
+ ## 服务管理(macOS)
48
+
49
+ ### 使用 launchctl
50
+
51
+ ```bash
52
+ # 启动服务
53
+ launchctl start com.gssh.daemon
54
+
55
+ # 停止服务
56
+ launchctl stop com.gssh.daemon
57
+
58
+ # 查看状态
59
+ launchctl list | grep gssh
60
+
61
+ # 卸载服务
62
+ launchctl unload ~/Library/LaunchAgents/gssh.plist
63
+ rm ~/Library/LaunchAgents/gssh.plist
64
+ ```
65
+
66
+ ### 使用 Homebrew services
67
+
68
+ ```bash
69
+ # 启动(首次安装后自动启动)
70
+ brew services start gssh
71
+
72
+ # 停止
73
+ brew services stop gssh
74
+
75
+ # 查看状态
76
+ brew services list
77
+
78
+ # 重启
79
+ brew services restart gssh
80
+ ```
81
+
82
+ ### 使用 PM2(跨平台)
83
+
84
+ ```bash
85
+ # 安装 PM2
86
+ npm install -g pm2
87
+
88
+ # 启动 daemon
89
+ pm2 start gssh-daemon.sh --name gssh
90
+
91
+ # 保存并设置开机自启
92
+ pm2 save
93
+ pm2 startup
94
+ ```
95
+
96
+ ## 使用方法
97
+
98
+ ### 连接 SSH(密码认证)
99
+
100
+ ```bash
101
+ gssh connect -u admin1 -h 139.196.175.163 -p 7080 -P 1234
102
+ ```
103
+
104
+ ### 连接 SSH(密钥认证)
105
+
106
+ ```bash
107
+ gssh connect -u admin1 -h example.com -i ~/.ssh/id_rsa
108
+ ```
109
+
110
+ ### 执行命令
111
+
112
+ ```bash
113
+ # 使用默认 session
114
+ gssh exec "ls -la"
115
+
116
+ # 指定 session
117
+ gssh exec -s <session_id> "pwd"
118
+ ```
119
+
120
+ ### 端口转发
121
+
122
+ ```bash
123
+ # 本地端口转发:本地 8080 -> 远程 80
124
+ gssh forward -l 8080 -r 80
125
+ ```
126
+
127
+ ### Session 管理
128
+
129
+ ```bash
130
+ # 列出所有 session
131
+ gssh list
132
+
133
+ # 切换默认 session
134
+ gssh use <session_id>
135
+
136
+ # 断开 session
137
+ gssh disconnect -s <session_id>
138
+
139
+ # 重连 session
140
+ gssh reconnect -s <session_id>
141
+ ```
142
+
143
+ ## 命令行选项
144
+
145
+ | 选项 | 说明 |
146
+ |------|------|
147
+ | `-socket path` | Unix socket 路径(默认:/tmp/gssh.sock) |
148
+ | `-s session_id` | Session ID |
149
+ | `-u user` | 用户名 |
150
+ | `-h host` | 主机地址 |
151
+ | `-p port` | 端口(默认:22) |
152
+ | `-P password` | 密码 |
153
+ | `-i key_path` | SSH 密钥路径 |
154
+ | `-l local` | 本地端口 |
155
+ | `-r remote` | 远程端口 |
156
+ | `-R` | 远程端口转发 |
157
+
158
+ ## 开发
159
+
160
+ ### 运行测试
161
+
162
+ ```bash
163
+ go test ./...
164
+ ```
165
+
166
+ ### 代码结构
167
+
168
+ - `cmd/daemon/` - daemon 主程序
169
+ - `cmd/gssh/` - CLI 主程序
170
+ - `internal/client/` - SSH 客户端封装
171
+ - `internal/session/` - Session 管理器
172
+ - `internal/portforward/` - 端口转发
173
+ - `internal/protocol/` - 协议类型定义
174
+ - `pkg/rpc/` - RPC 处理器
175
+ - `homebrew/` - Homebrew 安装文件
package/bin/daemon ADDED
Binary file
package/bin/gssh ADDED
Binary file
Binary file
@@ -0,0 +1,45 @@
1
+ package main
2
+
3
+ import (
4
+ "flag"
5
+ "log"
6
+ "os"
7
+ "os/signal"
8
+ "syscall"
9
+
10
+ "gssh/internal/session"
11
+ "gssh/pkg/rpc"
12
+ )
13
+
14
+ const (
15
+ defaultSocketPath = "/tmp/gssh.sock"
16
+ )
17
+
18
+ func main() {
19
+ socketPath := flag.String("socket", defaultSocketPath, "Unix socket path")
20
+ flag.Parse()
21
+
22
+ log.SetFlags(log.LstdFlags | log.Lshortfile)
23
+ log.SetOutput(os.Stderr)
24
+
25
+ log.Printf("Starting gssh daemon on %s", *socketPath)
26
+
27
+ // Create session manager
28
+ manager := session.NewManager()
29
+
30
+ // Start RPC server
31
+ go func() {
32
+ if err := rpc.ServeUnixSocket(manager, *socketPath); err != nil {
33
+ log.Printf("RPC server error: %v", err)
34
+ os.Exit(1)
35
+ }
36
+ }()
37
+
38
+ // Wait for interrupt signal
39
+ sigCh := make(chan os.Signal, 1)
40
+ signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
41
+ <-sigCh
42
+
43
+ log.Println("Shutting down...")
44
+ os.Remove(*socketPath)
45
+ }
@@ -0,0 +1,394 @@
1
+ package main
2
+
3
+ import (
4
+ "bufio"
5
+ "encoding/json"
6
+ "flag"
7
+ "fmt"
8
+ "net"
9
+ "os"
10
+ "strings"
11
+
12
+ "gssh/internal/protocol"
13
+ )
14
+
15
+ const (
16
+ defaultSocketPath = "/tmp/gssh.sock"
17
+ )
18
+
19
+ var (
20
+ socketPath = flag.String("socket", defaultSocketPath, "Unix socket path")
21
+ )
22
+
23
+ func main() {
24
+ flag.Parse()
25
+
26
+ if flag.NArg() < 1 {
27
+ printUsage()
28
+ os.Exit(1)
29
+ }
30
+
31
+ cmd := flag.Arg(0)
32
+
33
+ var err error
34
+ switch cmd {
35
+ case "connect":
36
+ err = handleConnect()
37
+ case "disconnect":
38
+ err = handleDisconnect()
39
+ case "reconnect":
40
+ err = handleReconnect()
41
+ case "exec":
42
+ err = handleExec()
43
+ case "list", "ls":
44
+ err = handleList()
45
+ case "use":
46
+ err = handleUse()
47
+ case "forward":
48
+ err = handleForward()
49
+ case "forwards":
50
+ err = handleForwards()
51
+ case "forward-close":
52
+ err = handleForwardClose()
53
+ case "help", "-h", "--help":
54
+ printUsage()
55
+ default:
56
+ fmt.Fprintf(os.Stderr, "Unknown command: %s\n", cmd)
57
+ printUsage()
58
+ os.Exit(1)
59
+ }
60
+
61
+ if err != nil {
62
+ fmt.Fprintf(os.Stderr, "Error: %v\n", err)
63
+ os.Exit(1)
64
+ }
65
+ }
66
+
67
+ func sendRequest(method string, params interface{}) ([]byte, error) {
68
+ conn, err := net.Dial("unix", *socketPath)
69
+ if err != nil {
70
+ return nil, fmt.Errorf("failed to connect to daemon: %w", err)
71
+ }
72
+ defer conn.Close()
73
+
74
+ paramsJSON, err := json.Marshal(params)
75
+ if err != nil {
76
+ return nil, fmt.Errorf("failed to marshal params: %w", err)
77
+ }
78
+
79
+ req := protocol.Request{
80
+ JSONRPC: "2.0",
81
+ Method: method,
82
+ Params: paramsJSON,
83
+ ID: 1,
84
+ }
85
+
86
+ reqData, err := json.Marshal(req)
87
+ if err != nil {
88
+ return nil, fmt.Errorf("failed to marshal request: %w", err)
89
+ }
90
+
91
+ // Add newline to signal end of request
92
+ reqData = append(reqData, '\n')
93
+
94
+ _, err = conn.Write(reqData)
95
+ if err != nil {
96
+ return nil, fmt.Errorf("failed to send request: %w", err)
97
+ }
98
+
99
+ // Read line by line
100
+ reader := bufio.NewReader(conn)
101
+ line, err := reader.ReadBytes('\n')
102
+ if err != nil {
103
+ return nil, fmt.Errorf("failed to read response: %w", err)
104
+ }
105
+
106
+ // Remove trailing newline
107
+ line = line[:len(line)-1]
108
+
109
+ var resp protocol.Response
110
+ if err := json.Unmarshal(line, &resp); err != nil {
111
+ return nil, fmt.Errorf("failed to parse response: %w", err)
112
+ }
113
+
114
+ if resp.Error != nil {
115
+ return nil, fmt.Errorf("RPC error: %s", resp.Error.Message)
116
+ }
117
+
118
+ result, err := json.Marshal(resp.Result)
119
+ if err != nil {
120
+ return nil, fmt.Errorf("failed to marshal result: %w", err)
121
+ }
122
+
123
+ return result, nil
124
+ }
125
+
126
+ func handleConnect() error {
127
+ user := flag.String("u", "", "Username")
128
+ host := flag.String("h", "", "Host")
129
+ port := flag.Int("p", 22, "Port")
130
+ password := flag.String("P", "", "Password")
131
+ keyPath := flag.String("i", "", "SSH key path")
132
+
133
+ flag.CommandLine.Parse(flag.Args()[1:])
134
+
135
+ if *user == "" || *host == "" {
136
+ return fmt.Errorf("user and host are required")
137
+ }
138
+
139
+ params := protocol.ConnectParams{
140
+ User: *user,
141
+ Host: *host,
142
+ Port: *port,
143
+ Password: *password,
144
+ KeyPath: *keyPath,
145
+ }
146
+
147
+ result, err := sendRequest("connect", params)
148
+ if err != nil {
149
+ return err
150
+ }
151
+
152
+ var session protocol.Session
153
+ if err := json.Unmarshal(result, &session); err != nil {
154
+ return fmt.Errorf("failed to parse result: %w", err)
155
+ }
156
+
157
+ fmt.Printf("Connected: %s@%s (ID: %s)\n", session.User, session.Host, session.ID)
158
+ return nil
159
+ }
160
+
161
+ func handleDisconnect() error {
162
+ sessionID := flag.String("s", "", "Session ID")
163
+ flag.CommandLine.Parse(flag.Args()[1:])
164
+
165
+ params := protocol.DisconnectParams{
166
+ SessionID: *sessionID,
167
+ }
168
+
169
+ _, err := sendRequest("disconnect", params)
170
+ if err != nil {
171
+ return err
172
+ }
173
+
174
+ fmt.Println("Disconnected")
175
+ return nil
176
+ }
177
+
178
+ func handleReconnect() error {
179
+ sessionID := flag.String("s", "", "Session ID")
180
+ flag.CommandLine.Parse(flag.Args()[1:])
181
+
182
+ params := protocol.ReconnectParams{
183
+ SessionID: *sessionID,
184
+ }
185
+
186
+ result, err := sendRequest("reconnect", params)
187
+ if err != nil {
188
+ return err
189
+ }
190
+
191
+ var session protocol.Session
192
+ if err := json.Unmarshal(result, &session); err != nil {
193
+ return fmt.Errorf("failed to parse result: %w", err)
194
+ }
195
+
196
+ fmt.Printf("Reconnected: %s@%s (ID: %s)\n", session.User, session.Host, session.ID)
197
+ return nil
198
+ }
199
+
200
+ func handleExec() error {
201
+ // Get remaining args after "exec"
202
+ args := flag.Args()[1:]
203
+
204
+ sessionID := flag.String("s", "", "Session ID")
205
+ flag.CommandLine.Parse(args)
206
+
207
+ if flag.NArg() < 1 {
208
+ return fmt.Errorf("command required")
209
+ }
210
+ command := strings.Join(flag.Args(), " ")
211
+
212
+ params := protocol.ExecParams{
213
+ SessionID: *sessionID,
214
+ Command: command,
215
+ }
216
+
217
+ result, err := sendRequest("exec", params)
218
+ if err != nil {
219
+ return err
220
+ }
221
+
222
+ var execResult protocol.ExecResult
223
+ if err := json.Unmarshal(result, &execResult); err != nil {
224
+ return fmt.Errorf("failed to parse result: %w", err)
225
+ }
226
+
227
+ fmt.Print(execResult.Stdout)
228
+ if execResult.Stderr != "" {
229
+ fmt.Fprintf(os.Stderr, "%s", execResult.Stderr)
230
+ }
231
+
232
+ if execResult.ExitCode != 0 {
233
+ os.Exit(execResult.ExitCode)
234
+ }
235
+
236
+ return nil
237
+ }
238
+
239
+ func handleList() error {
240
+ result, err := sendRequest("list", nil)
241
+ if err != nil {
242
+ return err
243
+ }
244
+
245
+ var sessions []*protocol.Session
246
+ if err := json.Unmarshal(result, &sessions); err != nil {
247
+ return fmt.Errorf("failed to parse result: %w", err)
248
+ }
249
+
250
+ if len(sessions) == 0 {
251
+ fmt.Println("No sessions")
252
+ return nil
253
+ }
254
+
255
+ fmt.Println("ID HOST USER STATUS")
256
+ for _, s := range sessions {
257
+ fmt.Printf("%-38s %-17s %-7s %s\n", s.ID, s.Host, s.User, s.Status)
258
+ }
259
+
260
+ return nil
261
+ }
262
+
263
+ func handleUse() error {
264
+ flag.CommandLine.Parse(flag.Args()[1:])
265
+
266
+ if flag.NArg() < 1 {
267
+ return fmt.Errorf("session ID required")
268
+ }
269
+ sessionIDStr := flag.Arg(0)
270
+
271
+ params := protocol.UseParams{
272
+ SessionID: sessionIDStr,
273
+ }
274
+
275
+ _, err := sendRequest("use", params)
276
+ if err != nil {
277
+ return err
278
+ }
279
+
280
+ fmt.Printf("Using session: %s\n", sessionIDStr)
281
+ return nil
282
+ }
283
+
284
+ func handleForward() error {
285
+ sessionID := flag.String("s", "", "Session ID")
286
+ local := flag.Int("l", 0, "Local port")
287
+ remote := flag.Int("r", 0, "Remote port")
288
+ isRemote := flag.Bool("R", false, "Remote port forward")
289
+
290
+ flag.CommandLine.Parse(flag.Args()[1:])
291
+
292
+ if *local == 0 || *remote == 0 {
293
+ return fmt.Errorf("local and remote ports are required")
294
+ }
295
+
296
+ forwardType := "local"
297
+ if *isRemote {
298
+ forwardType = "remote"
299
+ }
300
+
301
+ params := protocol.ForwardParams{
302
+ SessionID: *sessionID,
303
+ Type: forwardType,
304
+ Local: *local,
305
+ Remote: *remote,
306
+ }
307
+
308
+ result, err := sendRequest("forward", params)
309
+ if err != nil {
310
+ return err
311
+ }
312
+
313
+ var forward protocol.Forward
314
+ if err := json.Unmarshal(result, &forward); err != nil {
315
+ return fmt.Errorf("failed to parse result: %w", err)
316
+ }
317
+
318
+ fmt.Printf("Forward started: %s %d:%d (ID: %s)\n", forward.Type, forward.Local, forward.Remote, forward.ID)
319
+ return nil
320
+ }
321
+
322
+ func handleForwards() error {
323
+ result, err := sendRequest("forwards", nil)
324
+ if err != nil {
325
+ return err
326
+ }
327
+
328
+ var forwards []*protocol.Forward
329
+ if err := json.Unmarshal(result, &forwards); err != nil {
330
+ return fmt.Errorf("failed to parse result: %w", err)
331
+ }
332
+
333
+ if len(forwards) == 0 {
334
+ fmt.Println("No forwards")
335
+ return nil
336
+ }
337
+
338
+ fmt.Println("ID LOCAL REMOTE TYPE")
339
+ for _, f := range forwards {
340
+ fmt.Printf("%-5s %-7d %-8d %s\n", f.ID[:8], f.Local, f.Remote, f.Type)
341
+ }
342
+
343
+ return nil
344
+ }
345
+
346
+ func handleForwardClose() error {
347
+ flag.CommandLine.Parse(flag.Args()[1:])
348
+
349
+ if flag.NArg() < 1 {
350
+ return fmt.Errorf("forward ID required")
351
+ }
352
+ forwardID := flag.Arg(0)
353
+
354
+ params := protocol.ForwardCloseParams{
355
+ ForwardID: forwardID,
356
+ }
357
+
358
+ _, err := sendRequest("forward_close", params)
359
+ if err != nil {
360
+ return err
361
+ }
362
+
363
+ fmt.Printf("Forward %s closed\n", forwardID)
364
+ return nil
365
+ }
366
+
367
+ func printUsage() {
368
+ fmt.Println(`gssh - SSH Session Manager for Agents (Stateless)
369
+
370
+ Usage:
371
+ gssh connect -u user -h host [-p port] [-i key_path] [-P password]
372
+ gssh disconnect [-s session_id]
373
+ gssh reconnect [-s session_id]
374
+ gssh exec [-s session_id] "command"
375
+ gssh list
376
+ gssh use <session_id>
377
+ gssh forward [-s session_id] -l local_port -r remote_port
378
+ gssh forwards
379
+ gssh forward-close <forward_id>
380
+
381
+ Note: For sudo commands, use key-based authentication or configure passwordless sudo.
382
+
383
+ 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`)
394
+ }
package/go.mod ADDED
@@ -0,0 +1,10 @@
1
+ module gssh
2
+
3
+ go 1.21
4
+
5
+ require (
6
+ github.com/google/uuid v1.5.0
7
+ golang.org/x/crypto v0.18.0
8
+ )
9
+
10
+ require golang.org/x/sys v0.16.0 // indirect
package/go.sum ADDED
@@ -0,0 +1,8 @@
1
+ github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
2
+ github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
3
+ golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
4
+ golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
5
+ golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
6
+ golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
7
+ golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
8
+ golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
Binary file
@@ -0,0 +1,22 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>Label</key>
6
+ <string>com.gssh.daemon</string>
7
+ <key>ProgramArguments</key>
8
+ <array>
9
+ <string>/usr/local/bin/gssh-daemon</string>
10
+ <string>-socket</string>
11
+ <string>/tmp/gssh.sock</string>
12
+ </array>
13
+ <key>RunAtLoad</key>
14
+ <true/>
15
+ <key>KeepAlive</key>
16
+ <true/>
17
+ <key>StandardOutPath</key>
18
+ <string>/var/log/gssh.log</string>
19
+ <key>StandardErrorPath</key>
20
+ <string>/var/log/gssh.error.log</string>
21
+ </dict>
22
+ </plist>
@@ -0,0 +1,43 @@
1
+ #!/bin/bash
2
+ # gssh service installation script
3
+
4
+ set -e
5
+
6
+ # Detect architecture
7
+ ARCH=$(uname -m)
8
+ if [ "$ARCH" = "arm64" ]; then
9
+ PREFIX="/opt/homebrew"
10
+ else
11
+ PREFIX="/usr/local"
12
+ fi
13
+
14
+ DAEMON_BIN="$PREFIX/bin/gssh-daemon"
15
+ CLI_BIN="$PREFIX/bin/gssh"
16
+
17
+ echo "Installing gssh to $PREFIX/bin/..."
18
+
19
+ # Copy binaries
20
+ cp bin/daemon "$DAEMON_BIN"
21
+ cp bin/gssh "$CLI_BIN"
22
+
23
+ chmod +x "$DAEMON_BIN" "$CLI_BIN"
24
+
25
+ # Install launchd plist
26
+ PLIST_DIR="$HOME/Library/LaunchAgents"
27
+ mkdir -p "$PLIST_DIR"
28
+ cp homebrew/gssh.plist "$PLIST_DIR/"
29
+
30
+ echo "Loading gssh service..."
31
+ launchctl load "$PLIST_DIR/gssh.plist"
32
+
33
+ echo "gssh installed successfully!"
34
+ echo ""
35
+ echo "Usage:"
36
+ echo " Start: launchctl start com.gssh.daemon"
37
+ echo " Stop: launchctl stop com.gssh.daemon"
38
+ echo " Status: launchctl list | grep gssh"
39
+ echo ""
40
+ echo "Or use Homebrew services:"
41
+ echo " brew services start gssh"
42
+ echo " brew services stop gssh"
43
+ echo " brew services list"