gssh-agent 1.0.1 → 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/bin/daemon ADDED
Binary file
@@ -103,7 +103,7 @@ func Connect(user, host string, port int, password, keyPath string) (*SSHClient,
103
103
  return NewSSHClient(user, host, port, authMethods...)
104
104
  }
105
105
 
106
- // Exec executes a command on the remote host
106
+ // Exec executes a command on the remote host using an interactive login shell
107
107
  func (c *SSHClient) Exec(cmd string) (string, string, int, error) {
108
108
  session, err := c.Client.NewSession()
109
109
  if err != nil {
@@ -111,7 +111,29 @@ func (c *SSHClient) Exec(cmd string) (string, string, int, error) {
111
111
  }
112
112
  defer session.Close()
113
113
 
114
- output, err := session.CombinedOutput(cmd)
114
+ // Get terminal info for PTY
115
+ // Use default term if not available
116
+ termEnv := os.Getenv("TERM")
117
+ if termEnv == "" {
118
+ termEnv = "xterm-256color"
119
+ }
120
+
121
+ // Request PTY for interactive shell
122
+ termWidth := 80
123
+ termHeight := 24
124
+ if err := session.RequestPty(termEnv, termWidth, termHeight, ssh.TerminalModes{
125
+ ssh.ECHO: 1,
126
+ ssh.TTY_OP_ISPEED: 14400,
127
+ ssh.TTY_OP_OSPEED: 14400,
128
+ }); err != nil {
129
+ return "", "", 0, fmt.Errorf("failed to request PTY: %w", err)
130
+ }
131
+
132
+ // Execute command using login shell to load user config files (.zprofile, .bash_profile, etc.)
133
+ // Using -l for login shell, -c to execute command
134
+ fullCmd := fmt.Sprintf("/bin/zsh -l -c %q", cmd)
135
+
136
+ output, err := session.CombinedOutput(fullCmd)
115
137
  if err != nil {
116
138
  exitErr, ok := err.(*ssh.ExitError)
117
139
  if ok {
@@ -3,6 +3,7 @@ package portforward
3
3
  import (
4
4
  "fmt"
5
5
  "io"
6
+ "log"
6
7
  "net"
7
8
  "sync"
8
9
 
@@ -16,12 +17,12 @@ type Forwarder struct {
16
17
  LocalPort int
17
18
  RemotePort int
18
19
 
19
- sshClient *ssh.Client
20
- listener net.Listener
21
- conns map[net.Conn]bool
22
- mu sync.RWMutex
23
- closed bool
24
- wg sync.WaitGroup
20
+ sshClient *ssh.Client
21
+ listener net.Listener
22
+ conns map[net.Conn]bool
23
+ mu sync.RWMutex
24
+ closed bool
25
+ wg sync.WaitGroup
25
26
  }
26
27
 
27
28
  // NewForwarder creates a new port forwarder
@@ -42,6 +43,10 @@ func NewForwarder(sshClient *ssh.Client, forwardType string, localPort, remotePo
42
43
  return nil, fmt.Errorf("failed to listen on %s: %w", addr, err)
43
44
  }
44
45
  f.listener = listener
46
+ log.Printf("[portforward] Local forward: localhost:%d -> remote:%d", localPort, remotePort)
47
+ } else if forwardType == "remote" {
48
+ // Remote port forward: remote:remotePort -> localhost:localPort
49
+ log.Printf("[portforward] Remote forward: remote:%d -> localhost:%d", remotePort, localPort)
45
50
  }
46
51
 
47
52
  return f, nil
@@ -51,7 +56,7 @@ func NewForwarder(sshClient *ssh.Client, forwardType string, localPort, remotePo
51
56
  func (f *Forwarder) Start() {
52
57
  if f.Type == "local" {
53
58
  f.startLocalForward()
54
- } else {
59
+ } else if f.Type == "remote" {
55
60
  f.startRemoteForward()
56
61
  }
57
62
  }
@@ -98,13 +103,18 @@ func (f *Forwarder) handleLocalConnection(localConn net.Conn) {
98
103
  f.mu.Unlock()
99
104
  }()
100
105
 
106
+ log.Printf("[portforward] Connection accepted from %s", localConn.RemoteAddr())
107
+
101
108
  remoteAddr := fmt.Sprintf("localhost:%d", f.RemotePort)
102
109
  remoteConn, err := f.sshClient.Dial("tcp", remoteAddr)
103
110
  if err != nil {
111
+ log.Printf("[portforward] Failed to connect to remote %s: %v", remoteAddr, err)
104
112
  return
105
113
  }
106
114
  defer remoteConn.Close()
107
115
 
116
+ log.Printf("[portforward] Tunnel established to remote %s", remoteAddr)
117
+
108
118
  // Bidirectional copy
109
119
  done := make(chan struct{})
110
120
  go func() {
@@ -119,14 +129,34 @@ func (f *Forwarder) handleLocalConnection(localConn net.Conn) {
119
129
  }
120
130
 
121
131
  // startRemoteForward handles remote port forwarding
132
+ // Remote port forward: remote:remotePort -> localhost:localPort
122
133
  func (f *Forwarder) startRemoteForward() {
123
- // Remote port forwarding: remote:remotePort -> localhost:localPort
124
134
  f.wg.Add(1)
125
135
  go func() {
126
136
  defer f.wg.Done()
127
137
 
128
- // Request the server to forward remote port
129
- // This is a simplified implementation
138
+ // Request the SSH server to listen on remote:remotePort and forward connections to us
139
+ // Payload format: string (address) + uint32 (port)
140
+ addr := fmt.Sprintf(":%d", f.RemotePort)
141
+ payload := ssh.Marshal(struct {
142
+ Addr string
143
+ Port uint32
144
+ }{Addr: addr, Port: uint32(f.RemotePort)})
145
+
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
150
+ }
151
+ if !ok {
152
+ log.Printf("[portforward] SSH server rejected tcpip-forward request for port %d", f.RemotePort)
153
+ return
154
+ }
155
+
156
+ log.Printf("[portforward] Remote forward: SSH server listening on port %d", f.RemotePort)
157
+
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
130
160
  for {
131
161
  f.mu.RLock()
132
162
  if f.closed {
@@ -135,9 +165,46 @@ func (f *Forwarder) startRemoteForward() {
135
165
  }
136
166
  f.mu.RUnlock()
137
167
 
138
- // Wait for connections on remote side
139
- // In practice, SSH server handles this
140
- // This would need SSH channel forwarding
168
+ // Wait for a forwarded connection
169
+ ch, reqs, err := f.sshClient.OpenChannel("forwarded-tcpip", nil)
170
+ if err != nil {
171
+ f.mu.RLock()
172
+ closed := f.closed
173
+ f.mu.RUnlock()
174
+ if closed {
175
+ return
176
+ }
177
+ log.Printf("[portforward] Error accepting forwarded connection: %v", err)
178
+ continue
179
+ }
180
+
181
+ // Discard any requests
182
+ go ssh.DiscardRequests(reqs)
183
+
184
+ // Connect to local port
185
+ localAddr := fmt.Sprintf("localhost:%d", f.LocalPort)
186
+ localConn, err := net.Dial("tcp", localAddr)
187
+ if err != nil {
188
+ log.Printf("[portforward] Failed to connect to local port %d: %v", f.LocalPort, err)
189
+ ch.Close()
190
+ continue
191
+ }
192
+
193
+ f.mu.Lock()
194
+ f.conns[localConn] = true
195
+ f.mu.Unlock()
196
+
197
+ log.Printf("[portforward] Remote connection forwarded to localhost:%d", f.LocalPort)
198
+
199
+ // Bidirectional copy
200
+ go func() {
201
+ io.Copy(ch, localConn)
202
+ ch.Close()
203
+ localConn.Close()
204
+ }()
205
+ go func() {
206
+ io.Copy(localConn, ch)
207
+ }()
141
208
  }
142
209
  }()
143
210
  }
@@ -175,13 +242,15 @@ func (f *Forwarder) Restart(sshClient *ssh.Client) {
175
242
  f.listener.Close()
176
243
  }
177
244
 
178
- // Create new listener
179
- addr := fmt.Sprintf("localhost:%d", f.LocalPort)
180
- listener, err := net.Listen("tcp", addr)
181
- if err != nil {
182
- return
245
+ if f.Type == "local" {
246
+ // Create new listener
247
+ addr := fmt.Sprintf("localhost:%d", f.LocalPort)
248
+ listener, err := net.Listen("tcp", addr)
249
+ if err != nil {
250
+ return
251
+ }
252
+ f.listener = listener
183
253
  }
184
- f.listener = listener
185
254
 
186
255
  f.Start()
187
256
  }
@@ -10,6 +10,7 @@ import (
10
10
  "gssh/internal/protocol"
11
11
 
12
12
  "github.com/google/uuid"
13
+ "golang.org/x/crypto/ssh"
13
14
  )
14
15
 
15
16
  // Manager manages SSH sessions
@@ -23,17 +24,17 @@ type Manager struct {
23
24
 
24
25
  // ManagedSession wraps a session with additional state
25
26
  type ManagedSession struct {
26
- ID string
27
- Host string
28
- User string
29
- Port int
30
- Status string
31
- Password string
32
- KeyPath string
33
- SSHClient *client.SSHClient
34
- LastCmd string
35
- Forwards map[string]*portforward.Forwarder
36
- mu sync.RWMutex
27
+ ID string
28
+ Host string
29
+ User string
30
+ Port int
31
+ Status string
32
+ Password string
33
+ KeyPath string
34
+ SSHClient *client.SSHClient
35
+ LastCmd string
36
+ Forwards map[string]*portforward.Forwarder
37
+ mu sync.RWMutex
37
38
  }
38
39
 
39
40
  // NewManager creates a new session manager
@@ -180,23 +181,47 @@ func (m *Manager) Exec(sessionID, command string) (*protocol.ExecResult, error)
180
181
  }
181
182
 
182
183
  ms.mu.RLock()
183
- defer ms.mu.RUnlock()
184
-
185
184
  if ms.SSHClient == nil {
185
+ ms.mu.RUnlock()
186
186
  return nil, fmt.Errorf("session not connected")
187
187
  }
188
+ ms.mu.RUnlock()
188
189
 
189
- stdout, stderr, exitCode, err := ms.SSHClient.Exec(command)
190
- if err != nil && exitCode == 0 {
191
- return nil, err
190
+ // 复用 SSH 连接,创建新的 session 执行命令
191
+ session, err := ms.SSHClient.Client.NewSession()
192
+ if err != nil {
193
+ return nil, fmt.Errorf("failed to create session: %w", err)
192
194
  }
195
+ defer session.Close()
193
196
 
194
- ms.LastCmd = command
197
+ // 直接执行命令,不通过 shell
198
+
199
+ fullCmd := command
195
200
 
201
+ output, err := session.CombinedOutput(fullCmd)
202
+ if err != nil {
203
+ exitErr, ok := err.(*ssh.ExitError)
204
+ if ok {
205
+ ms.LastCmd = command
206
+ return &protocol.ExecResult{
207
+ Stdout: string(output),
208
+ Stderr: "",
209
+ ExitCode: exitErr.ExitStatus(),
210
+ }, nil
211
+ }
212
+ ms.LastCmd = command
213
+ return &protocol.ExecResult{
214
+ Stdout: string(output),
215
+ Stderr: "",
216
+ ExitCode: 0,
217
+ }, nil
218
+ }
219
+
220
+ ms.LastCmd = command
196
221
  return &protocol.ExecResult{
197
- Stdout: stdout,
198
- Stderr: stderr,
199
- ExitCode: exitCode,
222
+ Stdout: string(output),
223
+ Stderr: "",
224
+ ExitCode: 0,
200
225
  }, nil
201
226
  }
202
227
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gssh-agent",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "SSH Session Manager for Agents - Stateless SSH client with SFTP support",
5
5
  "bin": {
6
6
  "gssh": "./bin/gssh",
package/skill.md CHANGED
@@ -7,19 +7,18 @@ triggers:
7
7
  - ssh session
8
8
  - ssh 连接
9
9
  - ssh 管理
10
+ - sftp
11
+ - scp 文件传输
10
12
  ---
11
13
 
12
- ## 快速开始
14
+ ## 连接认证
13
15
 
14
16
  ```bash
15
- # 连接 SSH(交互式输入密码)
16
- gssh connect -u admin -h 192.168.1.100 --ask-pass
17
+ # 使用密码
18
+ gssh connect -u user -h host -p password
17
19
 
18
- # 执行命令
19
- gssh exec "ls -la"
20
-
21
- # 断开连接
22
- gssh disconnect
20
+ # SSH 密钥
21
+ gssh connect -u user -h host -i ~/.ssh/id_rsa
23
22
  ```
24
23
 
25
24
  ## 核心命令
@@ -28,33 +27,70 @@ gssh disconnect
28
27
  |------|------|
29
28
  | `connect` | 建立 SSH 连接 |
30
29
  | `exec` | 执行命令 |
31
- | `scp` | 文件传输(上/下载) |
30
+ | `scp` | 文件传输 |
32
31
  | `sftp` | SFTP 操作 |
33
32
  | `forward` | 端口转发 |
34
33
  | `list` | 列出 session |
34
+ | `use` | 切换默认 session |
35
35
 
36
- ## 认证
36
+ ## 执行命令
37
37
 
38
38
  ```bash
39
- # 交互式密码(推荐)
40
- gssh connect -u user -h host --ask-pass
39
+ # 普通命令
40
+ gssh exec "ls -la"
41
41
 
42
- # SSH 密钥
43
- gssh connect -u user -h host -i ~/.ssh/id_rsa
42
+ # sudo 命令
43
+ gssh exec -S password "sudo systemctl restart nginx"
44
44
  ```
45
45
 
46
- ## sudo 命令
46
+ ## 文件传输
47
47
 
48
48
  ```bash
49
- gssh exec --ask-sudo-pass "sudo apt update"
49
+ # 上传本地 -> 远程
50
+ gssh scp -put local.txt /remote/path/
51
+
52
+ # 下载远程 -> 本地
53
+ gssh scp -get /remote/file.txt ./
54
+
55
+ # SFTP 目录操作
56
+ gssh sftp -c ls -p /home/user
57
+ gssh sftp -c mkdir -p /home/user/newdir
58
+ gssh sftp -c rm -p /home/user/file.txt
50
59
  ```
51
60
 
52
- ## 文件传输
61
+ ## 端口转发
53
62
 
54
63
  ```bash
55
- # 上传
56
- gssh scp -put local.txt /remote/path/
64
+ # 本地转发: localhost:8080 -> remote:80
65
+ gssh forward -l 8080 -r 80
57
66
 
58
- # 下载
59
- gssh scp -get /remote/file.txt ./
67
+ # 远程转发: remote:9000 -> localhost:3000
68
+ gssh forward -R -l 9000 -r 3000
60
69
  ```
70
+
71
+ ## Session 管理
72
+
73
+ ```bash
74
+ gssh list # 列出所有 session
75
+ gssh use <session-id> # 切换默认 session
76
+ gssh disconnect # 断开
77
+ gssh reconnect -s <id> # 重连
78
+ ```
79
+
80
+ ## 选项汇总
81
+
82
+ | 选项 | 说明 |
83
+ |------|------|
84
+ | `-s` | session ID |
85
+ | `-u` | 用户名 |
86
+ | `-h` | 主机地址 |
87
+ | `-p` | 端口(默认 22) |
88
+ | `-P` | SSH 密码 |
89
+ | `-i` | SSH 密钥 |
90
+ | `-l` | 本地端口 |
91
+ | `-r` | 远程端口 |
92
+ | `-R` | 远程端口转发 |
93
+ | `-put/-get` | 上传/下载 |
94
+ | `-S` | sudo 密码 |
95
+ | `--ask-pass` | 交互输入 SSH 密码 |
96
+ | `--ask-sudo-pass` | 交互输入 sudo 密码 |