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.
@@ -2,11 +2,14 @@ package client
2
2
 
3
3
  import (
4
4
  "fmt"
5
+ "io"
5
6
  "net"
6
7
  "os"
7
8
  "os/user"
8
9
  "path/filepath"
10
+ "time"
9
11
 
12
+ "github.com/pkg/sftp"
10
13
  "golang.org/x/crypto/ssh"
11
14
  )
12
15
 
@@ -100,7 +103,7 @@ func Connect(user, host string, port int, password, keyPath string) (*SSHClient,
100
103
  return NewSSHClient(user, host, port, authMethods...)
101
104
  }
102
105
 
103
- // Exec executes a command on the remote host
106
+ // Exec executes a command on the remote host using an interactive login shell
104
107
  func (c *SSHClient) Exec(cmd string) (string, string, int, error) {
105
108
  session, err := c.Client.NewSession()
106
109
  if err != nil {
@@ -108,7 +111,29 @@ func (c *SSHClient) Exec(cmd string) (string, string, int, error) {
108
111
  }
109
112
  defer session.Close()
110
113
 
111
- 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)
112
137
  if err != nil {
113
138
  exitErr, ok := err.(*ssh.ExitError)
114
139
  if ok {
@@ -130,6 +155,231 @@ func (c *SSHClient) LocalForward(localPort, remotePort int) (net.Listener, error
130
155
  return c.Client.Listen("tcp", fmt.Sprintf("localhost:%d", localPort))
131
156
  }
132
157
 
158
+ // SFTPClient wraps SFTP client for file transfers
159
+ type SFTPClient struct {
160
+ Client *sftp.Client
161
+ }
162
+
163
+ // NewSFTPClient creates a new SFTP client from SSH client
164
+ func (c *SSHClient) NewSFTPClient() (*SFTPClient, error) {
165
+ sftpClient, err := sftp.NewClient(c.Client)
166
+ if err != nil {
167
+ return nil, fmt.Errorf("failed to create SFTP client: %w", err)
168
+ }
169
+ return &SFTPClient{Client: sftpClient}, nil
170
+ }
171
+
172
+ // Upload uploads a local file to remote
173
+ func (s *SFTPClient) Upload(localPath, remotePath string) (int64, error) {
174
+ localFile, err := os.Open(localPath)
175
+ if err != nil {
176
+ return 0, fmt.Errorf("failed to open local file: %w", err)
177
+ }
178
+ defer localFile.Close()
179
+
180
+ localInfo, err := localFile.Stat()
181
+ if err != nil {
182
+ return 0, fmt.Errorf("failed to stat local file: %w", err)
183
+ }
184
+
185
+ remoteFile, err := s.Client.Create(remotePath)
186
+ if err != nil {
187
+ return 0, fmt.Errorf("failed to create remote file: %w", err)
188
+ }
189
+ defer remoteFile.Close()
190
+
191
+ written, err := io.Copy(remoteFile, localFile)
192
+ if err != nil {
193
+ return 0, fmt.Errorf("failed to upload file: %w", err)
194
+ }
195
+
196
+ // Set file permissions
197
+ s.Client.Chmod(remotePath, localInfo.Mode())
198
+
199
+ return written, nil
200
+ }
201
+
202
+ // Download downloads a remote file to local
203
+ func (s *SFTPClient) Download(remotePath, localPath string) (int64, error) {
204
+ remoteFile, err := s.Client.Open(remotePath)
205
+ if err != nil {
206
+ return 0, fmt.Errorf("failed to open remote file: %w", err)
207
+ }
208
+ defer remoteFile.Close()
209
+
210
+ localFile, err := os.Create(localPath)
211
+ if err != nil {
212
+ return 0, fmt.Errorf("failed to create local file: %w", err)
213
+ }
214
+ defer localFile.Close()
215
+
216
+ written, err := io.Copy(localFile, remoteFile)
217
+ if err != nil {
218
+ return 0, fmt.Errorf("failed to download file: %w", err)
219
+ }
220
+
221
+ // Get remote file info to set local permissions
222
+ remoteInfo, err := s.Client.Stat(remotePath)
223
+ if err == nil {
224
+ os.Chmod(localPath, remoteInfo.Mode())
225
+ }
226
+
227
+ return written, nil
228
+ }
229
+
230
+ // List lists files in a remote directory
231
+ func (s *SFTPClient) List(path string) ([]string, error) {
232
+ entries, err := s.Client.ReadDir(path)
233
+ if err != nil {
234
+ return nil, fmt.Errorf("failed to read directory: %w", err)
235
+ }
236
+
237
+ names := make([]string, 0, len(entries))
238
+ for _, entry := range entries {
239
+ names = append(names, entry.Name())
240
+ }
241
+ return names, nil
242
+ }
243
+
244
+ // Mkdir creates a remote directory
245
+ func (s *SFTPClient) Mkdir(path string) error {
246
+ return s.Client.Mkdir(path)
247
+ }
248
+
249
+ // Remove removes a remote file
250
+ func (s *SFTPClient) Remove(path string) error {
251
+ return s.Client.Remove(path)
252
+ }
253
+
254
+ // Stat gets remote file info
255
+ func (s *SFTPClient) Stat(path string) (os.FileInfo, error) {
256
+ return s.Client.Stat(path)
257
+ }
258
+
259
+ // Close closes the SFTP client
260
+ func (s *SFTPClient) Close() error {
261
+ return s.Client.Close()
262
+ }
263
+
264
+ // TransferResult represents the result of a file transfer
265
+ type TransferResult struct {
266
+ Bytes int64
267
+ Duration time.Duration
268
+ }
269
+
270
+ // TransferWithProgress uploads or downloads a file with progress callback
271
+ func (s *SFTPClient) TransferWithProgress(isUpload bool, src, dst string, progress func(int64)) (*TransferResult, error) {
272
+ start := time.Now()
273
+
274
+ if isUpload {
275
+ result, err := s.uploadWithProgress(src, dst, progress)
276
+ if err != nil {
277
+ return nil, err
278
+ }
279
+ return &TransferResult{
280
+ Bytes: result,
281
+ Duration: time.Since(start),
282
+ }, nil
283
+ } else {
284
+ result, err := s.downloadWithProgress(src, dst, progress)
285
+ if err != nil {
286
+ return nil, err
287
+ }
288
+ return &TransferResult{
289
+ Bytes: result,
290
+ Duration: time.Since(start),
291
+ }, nil
292
+ }
293
+ }
294
+
295
+ func (s *SFTPClient) uploadWithProgress(localPath, remotePath string, progress func(int64)) (int64, error) {
296
+ localFile, err := os.Open(localPath)
297
+ if err != nil {
298
+ return 0, fmt.Errorf("failed to open local file: %w", err)
299
+ }
300
+ defer localFile.Close()
301
+
302
+ localInfo, err := localFile.Stat()
303
+ if err != nil {
304
+ return 0, fmt.Errorf("failed to stat local file: %w", err)
305
+ }
306
+
307
+ remoteFile, err := s.Client.Create(remotePath)
308
+ if err != nil {
309
+ return 0, fmt.Errorf("failed to create remote file: %w", err)
310
+ }
311
+ defer remoteFile.Close()
312
+
313
+ // Create a writer with progress tracking
314
+ writer := &progressWriter{
315
+ w: remoteFile,
316
+ progress: progress,
317
+ total: localInfo.Size(),
318
+ }
319
+
320
+ written, err := io.Copy(writer, localFile)
321
+ if err != nil {
322
+ return 0, fmt.Errorf("failed to upload file: %w", err)
323
+ }
324
+
325
+ // Set file permissions
326
+ s.Client.Chmod(remotePath, localInfo.Mode())
327
+
328
+ return written, nil
329
+ }
330
+
331
+ func (s *SFTPClient) downloadWithProgress(remotePath, localPath string, progress func(int64)) (int64, error) {
332
+ remoteFile, err := s.Client.Open(remotePath)
333
+ if err != nil {
334
+ return 0, fmt.Errorf("failed to open remote file: %w", err)
335
+ }
336
+ defer remoteFile.Close()
337
+
338
+ remoteInfo, err := s.Client.Stat(remotePath)
339
+ if err != nil {
340
+ return 0, fmt.Errorf("failed to stat remote file: %w", err)
341
+ }
342
+
343
+ localFile, err := os.Create(localPath)
344
+ if err != nil {
345
+ return 0, fmt.Errorf("failed to create local file: %w", err)
346
+ }
347
+ defer localFile.Close()
348
+
349
+ // Create a writer with progress tracking
350
+ writer := &progressWriter{
351
+ w: localFile,
352
+ progress: progress,
353
+ total: remoteInfo.Size(),
354
+ }
355
+
356
+ written, err := io.Copy(writer, remoteFile)
357
+ if err != nil {
358
+ return 0, fmt.Errorf("failed to download file: %w", err)
359
+ }
360
+
361
+ // Set local file permissions
362
+ os.Chmod(localPath, remoteInfo.Mode())
363
+
364
+ return written, nil
365
+ }
366
+
367
+ type progressWriter struct {
368
+ w io.Writer
369
+ progress func(int64)
370
+ total int64
371
+ written int64
372
+ }
373
+
374
+ func (pw *progressWriter) Write(p []byte) (n int, err error) {
375
+ n, err = pw.w.Write(p)
376
+ pw.written += int64(n)
377
+ if pw.progress != nil {
378
+ pw.progress(pw.written)
379
+ }
380
+ return
381
+ }
382
+
133
383
  // expandPath expands ~ to home directory
134
384
  func expandPath(path string) string {
135
385
  if len(path) > 0 && path[0] == '~' {
@@ -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
  }
@@ -88,3 +88,26 @@ type ForwardCloseParams struct {
88
88
  type UseParams struct {
89
89
  SessionID string `json:"session_id"`
90
90
  }
91
+
92
+ // SCPParams represents SCP transfer parameters
93
+ type SCPParams struct {
94
+ SessionID string `json:"session_id,omitempty"`
95
+ Source string `json:"source"`
96
+ Dest string `json:"dest"`
97
+ IsUpload bool `json:"is_upload"` // true = upload (local->remote), false = download (remote->local)
98
+ }
99
+
100
+ // SCPResult represents SCP transfer result
101
+ type SCPResult struct {
102
+ Success bool `json:"success"`
103
+ Message string `json:"message"`
104
+ Bytes int64 `json:"bytes"`
105
+ Duration int64 `json:"duration_ms"`
106
+ }
107
+
108
+ // SFTPParams represents SFTP command parameters
109
+ type SFTPParams struct {
110
+ SessionID string `json:"session_id,omitempty"`
111
+ Command string `json:"command"` // "ls", "cd", "pwd", "mkdir", "rm", "rmdir"
112
+ Path string `json:"path"`
113
+ }