gssh-agent 1.0.4 → 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
@@ -309,6 +309,7 @@ func handleExec(args []string, socketPath string) error {
309
309
  fs.SetOutput(os.Stderr)
310
310
 
311
311
  sessionID := fs.String("s", "", "Session ID")
312
+ timeout := fs.Int("t", 0, "Timeout in seconds (0 = no timeout)")
312
313
  sudoPassword := fs.String("S", "", "sudo password")
313
314
  askSudoPassword := fs.Bool("ask-sudo-pass", false, "Interactively ask for sudo password")
314
315
 
@@ -334,6 +335,7 @@ func handleExec(args []string, socketPath string) error {
334
335
  params := protocol.ExecParams{
335
336
  SessionID: *sessionID,
336
337
  Command: command,
338
+ Timeout: *timeout,
337
339
  }
338
340
 
339
341
  result, err := sendRequest(socketPath, "exec", params)
@@ -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
  }
@@ -224,7 +224,7 @@ func (m *Manager) Reconnect(sessionID string) (*protocol.Session, error) {
224
224
  }
225
225
 
226
226
  // Exec executes a command on a session
227
- func (m *Manager) Exec(sessionID, command string) (*protocol.ExecResult, error) {
227
+ func (m *Manager) Exec(sessionID, command string, timeout int) (*protocol.ExecResult, error) {
228
228
  m.mu.RLock()
229
229
  var ms *ManagedSession
230
230
  if sessionID != "" {
@@ -259,9 +259,34 @@ func (m *Manager) Exec(sessionID, command string) (*protocol.ExecResult, error)
259
259
  fullCmd = fmt.Sprintf("/bin/sh -c %q", command)
260
260
  }
261
261
 
262
- output, err := session.CombinedOutput(fullCmd)
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)
286
+ }
287
+
263
288
  if err != nil {
264
- exitErr, ok := err.(*ssh.ExitError)
289
+ exitErr, ok = err.(*ssh.ExitError)
265
290
  if ok {
266
291
  ms.LastCmd = command
267
292
  return &protocol.ExecResult{
package/package.json CHANGED
@@ -1,10 +1,9 @@
1
1
  {
2
2
  "name": "gssh-agent",
3
- "version": "1.0.4",
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