Skip to content

Commit a2d60ca

Browse files
committed
golang lib ssh
1 parent 22f8a4e commit a2d60ca

File tree

14 files changed

+1245
-0
lines changed

14 files changed

+1245
-0
lines changed

README.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
2+
## 项目简介
3+
本项目是基于golang标准库 ssh 和 sftp 开发
4+
5+
本项目是对标准库进行一个简单的高层封装,使得可以在在 Windows Linux Mac 上非常容易的执行 ssh 命令,
6+
以及文件,文件夹的上传,下载等操作.
7+
8+
文件上传下载模仿rsync方式: 只和源有关.
9+
// rsync -av src/ dst ./src/* --> /root/dst/*
10+
// rsync -av src/ dst/ ./src/* --> /root/dst/*
11+
// rsync -av src dst ./src/* --> /root/dst/src/*
12+
// rsync -av src dst/ ./src/* --> /root/dst/src/*
13+
14+
## Example
15+
16+
### 在远程执行ssh命令
17+
```go
18+
package main
19+
import (
20+
"fmt"
21+
"github.com/pytool/ssh"
22+
)
23+
func main() {
24+
25+
c, err := ssh.NewClient("root", "localhost", "22", "ubuntu")
26+
if err != nil {
27+
panic(err)
28+
}
29+
defer c.Close()
30+
31+
output, err := c.Exec("uptime")
32+
if err != nil {
33+
panic(err)
34+
}
35+
36+
fmt.Printf("Uptime: %s\n", output)
37+
}
38+
39+
```
40+
### 文件下载
41+
```go
42+
package main
43+
44+
import (
45+
"github.com/pytool/ssh"
46+
)
47+
48+
func main() {
49+
50+
client, err := ssh.NewClient("root", "localhost", "22", "ubuntu")
51+
if err != nil {
52+
panic(err)
53+
}
54+
defer client.Close()
55+
var remotedir = "/root/test/"
56+
// download dir
57+
var local = "/home/ubuntu/go/src/github.com/pytool/ssh/test/download/"
58+
client.Download(remotedir, local)
59+
60+
// upload file
61+
var remotefile = "/root/test/file"
62+
63+
client.Download(remotefile, local)
64+
}
65+
66+
```
67+
68+
### 文件上传
69+
```go
70+
package main
71+
72+
import (
73+
"github.com/pytool/ssh"
74+
)
75+
76+
func main() {
77+
78+
client, err := ssh.NewClient("root", "localhost", "22", "ubuntu")
79+
if err != nil {
80+
panic(err)
81+
}
82+
defer client.Close()
83+
var remotedir = "/root/test/"
84+
// upload dir
85+
var local = "/home/ubuntu/go/src/github.com/pytool/ssh/test/upload/"
86+
client.Upload(local, remotedir)
87+
88+
// upload file
89+
local = "/home/ubuntu/go/src/github.com/pytool/ssh/test/upload/file"
90+
client.Upload(local, remotedir)
91+
}
92+
93+
```
94+
95+

auth.go

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
package ssh
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"io/ioutil"
7+
"net"
8+
"os"
9+
"strings"
10+
"syscall"
11+
12+
"golang.org/x/crypto/ssh"
13+
"golang.org/x/crypto/ssh/agent"
14+
"golang.org/x/crypto/ssh/terminal"
15+
)
16+
17+
// An implementation of ssh.KeyboardInteractiveChallenge that simply sends
18+
// back the password for all questions. The questions are logged.
19+
func passwordKeyboardInteractive(password string) ssh.KeyboardInteractiveChallenge {
20+
return func(user, instruction string, questions []string, echos []bool) ([]string, error) {
21+
// log.Printf("Keyboard interactive challenge: ")
22+
// log.Printf("-- User: %s", user)
23+
// log.Printf("-- Instructions: %s", instruction)
24+
// for i, question := range questions {
25+
// log.Printf("-- Question %d: %s", i+1, question)
26+
// }
27+
28+
// Just send the password back for all questions
29+
answers := make([]string, len(questions))
30+
for i := range answers {
31+
answers[i] = password
32+
}
33+
34+
return answers, nil
35+
}
36+
}
37+
38+
// WithKeyboardPassword Generate a password-auth'd ssh ClientConfig
39+
func WithKeyboardPassword(password string) (ssh.AuthMethod, error) {
40+
return ssh.KeyboardInteractive(passwordKeyboardInteractive(password)), nil
41+
}
42+
43+
// WithPassword Generate a password-auth'd ssh ClientConfig
44+
func WithPassword(password string) (ssh.AuthMethod, error) {
45+
return ssh.Password(password), nil
46+
}
47+
48+
// WithAgent use already authed user
49+
func WithAgent() (ssh.AuthMethod, error) {
50+
sock := os.Getenv("SSH_AUTH_SOCK")
51+
if sock != "" {
52+
// fmt.Println(errors.New("Agent Disabled"))
53+
return nil, errors.New("Agent Disabled")
54+
}
55+
socks, err := net.Dial("unix", sock)
56+
if err != nil {
57+
fmt.Println(err)
58+
return nil, err
59+
}
60+
// 1. 返回Signers函数的结果
61+
agent := agent.NewClient(socks)
62+
signers, err := agent.Signers()
63+
return ssh.PublicKeys(signers...), nil
64+
// 2. 返回Signers函数
65+
// getSigners := agent.NewClient(socks).Signers
66+
// return ssh.PublicKeysCallback(getSigners), nil
67+
68+
// 3.简写方式
69+
// if sshAgent, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")); err == nil {
70+
// return ssh.PublicKeysCallback(agent.NewClient(sshAgent).Signers)
71+
// }
72+
// return nil
73+
}
74+
75+
// WithPrivateKeys 设置多个 ~/.ssh/id_rsa
76+
func WithPrivateKeys(keyFiles []string, password string) (ssh.AuthMethod, error) {
77+
var signers []ssh.Signer
78+
79+
for _, key := range keyFiles {
80+
81+
buffer, err := ioutil.ReadFile(key)
82+
if err != nil {
83+
println(err.Error())
84+
// return
85+
}
86+
signer, err := ssh.ParsePrivateKeyWithPassphrase([]byte(buffer), []byte(password))
87+
if err != nil {
88+
println(err.Error())
89+
} else {
90+
signers = append(signers, signer)
91+
}
92+
}
93+
if signers == nil {
94+
return nil, errors.New("WithPrivateKeys: no keyfiles input")
95+
}
96+
return ssh.PublicKeys(signers...), nil
97+
}
98+
99+
// WithPrivateKey 自动监测是否带有密码
100+
func WithPrivateKey(keyfile string, password string) (ssh.AuthMethod, error) {
101+
pemBytes, err := ioutil.ReadFile(keyfile)
102+
if err != nil {
103+
println(err.Error())
104+
return nil, err
105+
}
106+
107+
var signer ssh.Signer
108+
signer, err = ssh.ParsePrivateKey(pemBytes)
109+
if err != nil {
110+
if strings.Contains(err.Error(), "cannot decode encrypted private keys") {
111+
if signer, err = ssh.ParsePrivateKeyWithPassphrase(pemBytes, []byte(password)); err == nil {
112+
return ssh.PublicKeys(signer), nil
113+
}
114+
}
115+
}
116+
return nil, err
117+
}
118+
119+
// WithPrivateKeyString 直接通过字符串
120+
func WithPrivateKeyString(key string, password string) (ssh.AuthMethod, error) {
121+
var signer ssh.Signer
122+
var err error
123+
if password == "" {
124+
signer, err = ssh.ParsePrivateKey([]byte(key))
125+
} else {
126+
signer, err = ssh.ParsePrivateKeyWithPassphrase([]byte(key), []byte(password))
127+
}
128+
if err != nil {
129+
println(err.Error())
130+
return nil, err
131+
}
132+
return ssh.PublicKeys(signer), nil
133+
}
134+
135+
// WithPrivateKeyTerminal 通过终端读取带密码的 PublicKey
136+
func WithPrivateKeyTerminal(keyfile string) (ssh.AuthMethod, error) {
137+
// fmt.Fprintf(os.Stderr, "This SSH key is encrypted. Please enter passphrase for key '%s':", priv.path)
138+
passphrase, err := terminal.ReadPassword(int(syscall.Stdin))
139+
if err != nil {
140+
println(err.Error())
141+
return nil, err
142+
}
143+
144+
fmt.Fprintln(os.Stderr)
145+
146+
pemBytes, err := ioutil.ReadFile(keyfile)
147+
if err != nil {
148+
149+
println(err.Error())
150+
return nil, err
151+
}
152+
signer, err := ssh.ParsePrivateKeyWithPassphrase(pemBytes, passphrase)
153+
if err != nil {
154+
155+
fmt.Println(err)
156+
return nil, err
157+
}
158+
159+
return ssh.PublicKeys(signer), nil
160+
}

client.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package ssh
2+
3+
import (
4+
"errors"
5+
"net"
6+
"os"
7+
"strconv"
8+
"time"
9+
10+
"github.com/pkg/sftp"
11+
"golang.org/x/crypto/ssh"
12+
)
13+
14+
const DefaultTimeout = 30 * time.Second
15+
16+
type Client struct {
17+
*Config
18+
SSHClient *ssh.Client
19+
SFTPClient *sftp.Client
20+
}
21+
22+
// NewClient 根据配置
23+
func NewClient(user, host, port, password string) (client *Client, err error) {
24+
p, err := strconv.Atoi(port)
25+
if err == nil || p == 0 {
26+
p = 22
27+
}
28+
if user == "" {
29+
user = "root"
30+
}
31+
var config = &Config{
32+
User: user,
33+
Host: host,
34+
Port: p,
35+
Password: password,
36+
// KeyFiles: []string{"~/.ssh/id_rsa"},
37+
}
38+
return New(config)
39+
}
40+
41+
// New 创建SSH client
42+
func New(config *Config) (client *Client, err error) {
43+
clientConfig := &ssh.ClientConfig{
44+
User: config.User,
45+
Timeout: DefaultTimeout,
46+
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
47+
}
48+
49+
// 2. 密码方式
50+
if config.Password != "" {
51+
clientConfig.Auth = append(clientConfig.Auth, ssh.Password(config.Password))
52+
}
53+
54+
// 3. privite key file
55+
if len(config.KeyFiles) == 0 {
56+
keyPath := os.Getenv("HOME") + "/.ssh/id_rsa"
57+
if auth, err := WithPrivateKey(keyPath, config.Password); err != nil {
58+
clientConfig.Auth = append(clientConfig.Auth, auth)
59+
}
60+
} else {
61+
if auth, err := WithPrivateKeys(config.KeyFiles, config.Password); err != nil {
62+
clientConfig.Auth = append(clientConfig.Auth, auth)
63+
}
64+
}
65+
// 1. agent
66+
if auth, err := WithAgent(); err != nil {
67+
clientConfig.Auth = append(clientConfig.Auth, auth)
68+
}
69+
if config.Port == 0 {
70+
config.Port = 22
71+
}
72+
// hostPort := config.Host + ":" + strconv.Itoa(config.Port)
73+
sshClient, err := ssh.Dial("tcp", net.JoinHostPort(config.Host, strconv.Itoa(config.Port)), clientConfig)
74+
75+
if err != nil {
76+
return client, errors.New("Failed to dial ssh: " + err.Error())
77+
}
78+
79+
// create sftp client
80+
var sftpClient *sftp.Client
81+
if sftpClient, err = sftp.NewClient(sshClient); err != nil {
82+
return client, errors.New("Failed to conn sftp: " + err.Error())
83+
}
84+
85+
return &Client{SSHClient: sshClient, SFTPClient: sftpClient}, nil
86+
}
87+
88+
// Execute cmd on the remote host and return stderr and stdout
89+
func (c *Client) Exec(cmd string) ([]byte, error) {
90+
session, err := c.SSHClient.NewSession()
91+
if err != nil {
92+
return nil, err
93+
}
94+
defer session.Close()
95+
96+
return session.CombinedOutput(cmd)
97+
}
98+
99+
// Close the underlying SSH connection
100+
func (c *Client) Close() {
101+
c.SFTPClient.Close()
102+
c.SSHClient.Close()
103+
}
104+
105+
func addPortToHost(host string) string {
106+
_, _, err := net.SplitHostPort(host)
107+
108+
// We got an error so blindly try to add a port number
109+
if err != nil {
110+
return net.JoinHostPort(host, "22")
111+
}
112+
113+
return host
114+
}

cmd/main.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/pytool/ssh"
7+
)
8+
9+
func main() {
10+
11+
client, err := ssh.NewClient("root", "localhost", "22", "ubuntu")
12+
if err != nil {
13+
panic(err)
14+
}
15+
defer client.Close()
16+
17+
output, err := client.Exec("uptime")
18+
if err != nil {
19+
panic(err)
20+
}
21+
22+
fmt.Printf("Uptime: %s\n", output)
23+
24+
}

0 commit comments

Comments
 (0)