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 +0 -0
- package/internal/client/ssh.go +24 -2
- package/internal/portforward/forwarder.go +88 -19
- package/internal/session/manager.go +45 -20
- package/package.json +1 -1
- package/skill.md +57 -21
package/bin/daemon
ADDED
|
Binary file
|
package/internal/client/ssh.go
CHANGED
|
@@ -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
|
-
|
|
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
|
|
20
|
-
listener
|
|
21
|
-
conns
|
|
22
|
-
mu
|
|
23
|
-
closed
|
|
24
|
-
wg
|
|
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
|
|
129
|
-
//
|
|
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
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
|
27
|
-
Host
|
|
28
|
-
User
|
|
29
|
-
Port
|
|
30
|
-
Status
|
|
31
|
-
Password
|
|
32
|
-
KeyPath
|
|
33
|
-
SSHClient
|
|
34
|
-
LastCmd
|
|
35
|
-
Forwards
|
|
36
|
-
mu
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
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:
|
|
198
|
-
Stderr:
|
|
199
|
-
ExitCode:
|
|
222
|
+
Stdout: string(output),
|
|
223
|
+
Stderr: "",
|
|
224
|
+
ExitCode: 0,
|
|
200
225
|
}, nil
|
|
201
226
|
}
|
|
202
227
|
|
package/package.json
CHANGED
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
|
-
#
|
|
16
|
-
gssh connect -u
|
|
17
|
+
# 使用密码
|
|
18
|
+
gssh connect -u user -h host -p password
|
|
17
19
|
|
|
18
|
-
#
|
|
19
|
-
gssh
|
|
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
|
|
39
|
+
# 普通命令
|
|
40
|
+
gssh exec "ls -la"
|
|
41
41
|
|
|
42
|
-
#
|
|
43
|
-
gssh
|
|
42
|
+
# sudo 命令
|
|
43
|
+
gssh exec -S password "sudo systemctl restart nginx"
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
##
|
|
46
|
+
## 文件传输
|
|
47
47
|
|
|
48
48
|
```bash
|
|
49
|
-
|
|
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
|
|
64
|
+
# 本地转发: localhost:8080 -> remote:80
|
|
65
|
+
gssh forward -l 8080 -r 80
|
|
57
66
|
|
|
58
|
-
#
|
|
59
|
-
gssh
|
|
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 密码 |
|