gssh-agent 1.0.1 → 1.0.3
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/bin/gssh +0 -0
- package/cmd/gssh/main.go +42 -18
- package/internal/client/ssh.go +24 -2
- package/internal/portforward/forwarder.go +88 -19
- package/internal/session/manager.go +96 -20
- package/package.json +1 -1
- package/skill.md +57 -21
package/bin/daemon
ADDED
|
Binary file
|
package/bin/gssh
CHANGED
|
Binary file
|
package/cmd/gssh/main.go
CHANGED
|
@@ -18,6 +18,7 @@ import (
|
|
|
18
18
|
|
|
19
19
|
const (
|
|
20
20
|
defaultSocketPath = "/tmp/gssh.sock"
|
|
21
|
+
version = "0.1.0"
|
|
21
22
|
)
|
|
22
23
|
|
|
23
24
|
var (
|
|
@@ -25,6 +26,20 @@ var (
|
|
|
25
26
|
)
|
|
26
27
|
|
|
27
28
|
func main() {
|
|
29
|
+
// Check for version flag before parsing
|
|
30
|
+
if len(os.Args) > 1 && (os.Args[1] == "-v" || os.Args[1] == "--version") {
|
|
31
|
+
fmt.Printf("gssh version %s\n", version)
|
|
32
|
+
os.Exit(0)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Check for help flag before parsing
|
|
36
|
+
for _, arg := range os.Args[1:] {
|
|
37
|
+
if arg == "-h" || arg == "--help" {
|
|
38
|
+
printUsage()
|
|
39
|
+
os.Exit(0)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
28
43
|
flag.Parse()
|
|
29
44
|
|
|
30
45
|
if flag.NArg() < 1 {
|
|
@@ -58,7 +73,7 @@ func main() {
|
|
|
58
73
|
err = handleSCP()
|
|
59
74
|
case "sftp":
|
|
60
75
|
err = handleSFTP()
|
|
61
|
-
case "help"
|
|
76
|
+
case "help":
|
|
62
77
|
printUsage()
|
|
63
78
|
default:
|
|
64
79
|
fmt.Fprintf(os.Stderr, "Unknown command: %s\n", cmd)
|
|
@@ -566,42 +581,51 @@ func handleSFTP() error {
|
|
|
566
581
|
}
|
|
567
582
|
|
|
568
583
|
func printUsage() {
|
|
569
|
-
fmt.
|
|
584
|
+
fmt.Printf(`gssh - SSH Session Manager for Agents v%s
|
|
570
585
|
|
|
571
586
|
Usage:
|
|
572
587
|
gssh connect -u user -h host [-p port] [-i key_path] [-P password] [--ask-pass]
|
|
573
588
|
gssh disconnect [-s session_id]
|
|
574
589
|
gssh reconnect [-s session_id]
|
|
575
|
-
gssh exec [-s session_id] [-S password | --ask-sudo-pass] "
|
|
590
|
+
gssh exec [-s session_id] [-S password | --ask-sudo-pass] "command"
|
|
576
591
|
gssh list
|
|
577
592
|
gssh use <session_id>
|
|
578
|
-
gssh forward [-s session_id] -l local_port -r remote_port
|
|
593
|
+
gssh forward [-s session_id] -l local_port -r remote_port [-R]
|
|
579
594
|
gssh forwards
|
|
580
595
|
gssh forward-close <forward_id>
|
|
581
|
-
gssh scp [-s session_id]
|
|
582
|
-
gssh
|
|
583
|
-
|
|
584
|
-
|
|
596
|
+
gssh scp [-s session_id] -put <local> <remote>
|
|
597
|
+
gssh scp [-s session_id] -get <remote> <local>
|
|
598
|
+
gssh sftp [-s session_id] -c <ls|mkdir|rm> -p <path>
|
|
599
|
+
gssh -v, --version
|
|
585
600
|
|
|
586
601
|
Options:
|
|
587
|
-
-socket path
|
|
602
|
+
-socket path Unix socket path (default: /tmp/gssh.sock)
|
|
588
603
|
-s session_id Session ID
|
|
589
604
|
-u user Username
|
|
590
605
|
-h host Host
|
|
591
606
|
-p port Port (default: 22)
|
|
592
|
-
-P password Password
|
|
593
|
-
-i key_path
|
|
594
|
-
-S password
|
|
595
|
-
--ask-
|
|
607
|
+
-P password Password
|
|
608
|
+
-i key_path SSH key path
|
|
609
|
+
-S password sudo password
|
|
610
|
+
--ask-pass Ask for password interactively
|
|
611
|
+
--ask-sudo-pass Ask for sudo password interactively
|
|
596
612
|
-l local Local port
|
|
597
613
|
-r remote Remote port
|
|
598
|
-
-R Remote port forward
|
|
599
|
-
-put Upload
|
|
600
|
-
-get Download
|
|
614
|
+
-R Remote port forward (default: local)
|
|
615
|
+
-put Upload (local -> remote)
|
|
616
|
+
-get Download (remote -> local)
|
|
601
617
|
-c command SFTP command (ls, mkdir, rm)
|
|
602
618
|
-p path Path for SFTP command
|
|
603
|
-
|
|
604
|
-
|
|
619
|
+
-v, --version Show version
|
|
620
|
+
|
|
621
|
+
Examples:
|
|
622
|
+
gssh connect -u admin -h 192.168.1.1 -P password
|
|
623
|
+
gssh exec "ls -la"
|
|
624
|
+
gssh forward -l 8080 -r 80
|
|
625
|
+
gssh forward -l 9000 -r 3000 -R
|
|
626
|
+
gssh scp -put local.txt /home/user/remote.txt
|
|
627
|
+
gssh sftp -c ls -p /home/user
|
|
628
|
+
`, version)
|
|
605
629
|
}
|
|
606
630
|
|
|
607
631
|
// Restore terminal on exit
|
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
|
}
|
|
@@ -2,6 +2,7 @@ package session
|
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"fmt"
|
|
5
|
+
"strings"
|
|
5
6
|
"sync"
|
|
6
7
|
"time"
|
|
7
8
|
|
|
@@ -10,6 +11,7 @@ import (
|
|
|
10
11
|
"gssh/internal/protocol"
|
|
11
12
|
|
|
12
13
|
"github.com/google/uuid"
|
|
14
|
+
"golang.org/x/crypto/ssh"
|
|
13
15
|
)
|
|
14
16
|
|
|
15
17
|
// Manager manages SSH sessions
|
|
@@ -23,17 +25,17 @@ type Manager struct {
|
|
|
23
25
|
|
|
24
26
|
// ManagedSession wraps a session with additional state
|
|
25
27
|
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
|
|
28
|
+
ID string
|
|
29
|
+
Host string
|
|
30
|
+
User string
|
|
31
|
+
Port int
|
|
32
|
+
Status string
|
|
33
|
+
Password string
|
|
34
|
+
KeyPath string
|
|
35
|
+
SSHClient *client.SSHClient
|
|
36
|
+
LastCmd string
|
|
37
|
+
Forwards map[string]*portforward.Forwarder
|
|
38
|
+
mu sync.RWMutex
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
// NewManager creates a new session manager
|
|
@@ -44,6 +46,54 @@ func NewManager() *Manager {
|
|
|
44
46
|
}
|
|
45
47
|
}
|
|
46
48
|
|
|
49
|
+
// needsShell returns true if the command needs to be executed through a shell
|
|
50
|
+
func needsShell(cmd string) bool {
|
|
51
|
+
// Check for heredoc
|
|
52
|
+
if strings.Contains(cmd, "<<") {
|
|
53
|
+
return true
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Check for newlines (multi-line commands)
|
|
57
|
+
if strings.Contains(cmd, "\n") {
|
|
58
|
+
return true
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Check for pipe
|
|
62
|
+
if strings.Contains(cmd, " | ") {
|
|
63
|
+
return true
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check for redirection
|
|
67
|
+
if strings.ContainsAny(cmd, "><") {
|
|
68
|
+
// Make sure it's not in a string context
|
|
69
|
+
if strings.Contains(cmd, ">") || strings.Contains(cmd, "<") {
|
|
70
|
+
return true
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check for logical operators
|
|
75
|
+
if strings.Contains(cmd, " && ") || strings.Contains(cmd, " || ") {
|
|
76
|
+
return true
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Check for background execution
|
|
80
|
+
if strings.HasSuffix(strings.TrimSpace(cmd), "&") {
|
|
81
|
+
return true
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Check for command substitution
|
|
85
|
+
if strings.Contains(cmd, "$(") || strings.Contains(cmd, "`") {
|
|
86
|
+
return true
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Check for environment variables (but not $$ which is PID)
|
|
90
|
+
if strings.Contains(cmd, "${") || (strings.Contains(cmd, "$") && !strings.Contains(cmd, "$$")) {
|
|
91
|
+
return true
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return false
|
|
95
|
+
}
|
|
96
|
+
|
|
47
97
|
// Connect creates a new SSH session
|
|
48
98
|
func (m *Manager) Connect(user, host string, port int, password, keyPath string) (*protocol.Session, error) {
|
|
49
99
|
m.mu.Lock()
|
|
@@ -180,23 +230,49 @@ func (m *Manager) Exec(sessionID, command string) (*protocol.ExecResult, error)
|
|
|
180
230
|
}
|
|
181
231
|
|
|
182
232
|
ms.mu.RLock()
|
|
183
|
-
defer ms.mu.RUnlock()
|
|
184
|
-
|
|
185
233
|
if ms.SSHClient == nil {
|
|
234
|
+
ms.mu.RUnlock()
|
|
186
235
|
return nil, fmt.Errorf("session not connected")
|
|
187
236
|
}
|
|
237
|
+
ms.mu.RUnlock()
|
|
188
238
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
239
|
+
// 复用 SSH 连接,创建新的 session 执行命令
|
|
240
|
+
session, err := ms.SSHClient.Client.NewSession()
|
|
241
|
+
if err != nil {
|
|
242
|
+
return nil, fmt.Errorf("failed to create session: %w", err)
|
|
192
243
|
}
|
|
244
|
+
defer session.Close()
|
|
193
245
|
|
|
194
|
-
|
|
246
|
+
// 检测是否需要通过 shell 执行
|
|
247
|
+
fullCmd := command
|
|
248
|
+
if needsShell(command) {
|
|
249
|
+
fullCmd = fmt.Sprintf("/bin/zsh -c %q", command)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
output, err := session.CombinedOutput(fullCmd)
|
|
253
|
+
if err != nil {
|
|
254
|
+
exitErr, ok := err.(*ssh.ExitError)
|
|
255
|
+
if ok {
|
|
256
|
+
ms.LastCmd = command
|
|
257
|
+
return &protocol.ExecResult{
|
|
258
|
+
Stdout: string(output),
|
|
259
|
+
Stderr: "",
|
|
260
|
+
ExitCode: exitErr.ExitStatus(),
|
|
261
|
+
}, nil
|
|
262
|
+
}
|
|
263
|
+
ms.LastCmd = command
|
|
264
|
+
return &protocol.ExecResult{
|
|
265
|
+
Stdout: string(output),
|
|
266
|
+
Stderr: "",
|
|
267
|
+
ExitCode: 0,
|
|
268
|
+
}, nil
|
|
269
|
+
}
|
|
195
270
|
|
|
271
|
+
ms.LastCmd = command
|
|
196
272
|
return &protocol.ExecResult{
|
|
197
|
-
Stdout:
|
|
198
|
-
Stderr:
|
|
199
|
-
ExitCode:
|
|
273
|
+
Stdout: string(output),
|
|
274
|
+
Stderr: "",
|
|
275
|
+
ExitCode: 0,
|
|
200
276
|
}, nil
|
|
201
277
|
}
|
|
202
278
|
|
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 密码 |
|