├── go.mod ├── Makefile ├── test ├── id_rsa.pub ├── Dockerfile └── id_rsa ├── .github └── workflows │ └── ci.yml ├── LICENSE ├── README.md ├── go.sum ├── ssh_test.go ├── mocks └── ssh.go └── ssh.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/metrue/go-ssh-client 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/golang/mock v1.3.1 7 | github.com/mitchellh/go-homedir v1.1.0 8 | github.com/pkg/errors v0.9.1 9 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 10 | ) 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | go test -v ./... 3 | lint: 4 | golangci-lint run ./ 5 | start_ssh_server: 6 | @echo "start ssh server ..." 7 | docker build -t ssh-server -f test/Dockerfile . 8 | docker run -d --rm --name ssh-server -p 2222:22 ssh-server:latest 9 | clean: 10 | @echo "stop ssh server ..." 11 | docker stop ssh-server 12 | 13 | all: start_ssh_server test clean 14 | -------------------------------------------------------------------------------- /test/id_rsa.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFCV9M5sAhOpVQftVS1VDJlXiR7RSaSvwDIDrSoB1Kgz7C60YmRVA+9+bg0dAHLZdF2IFBHjRiVb222a3w1/Lenrl354vHlHGSoyOFi9R1IKdE3zKDhgx02LAhD+N54Q5qE91ENqM8h/ZbWi2MZI2SB09v7Xnx0gYdsrR7r56B/iWXvVbQM78xcnRahEKrBo7z/reil+DVW/OqtvoWLv14xmqKhboPqgAPTTJHqclQcLxK+l4mG5nGVeNM0OQphiFAlkupXpQdvEweMnn+O1sNuFPOWBSNcs1yqdhZMN0HUHGSescpbxAv3ysXPqh/SqxoMAfLW5XEV9SBBO2Cf9OT minghe@oldmac.local 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: ci 3 | jobs: 4 | Test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: setup Go 1.12 8 | uses: actions/setup-go@v1 9 | with: 10 | go-version: 1.12 11 | id: go 12 | 13 | - name: check out 14 | uses: actions/checkout@master 15 | 16 | - name: lint 17 | run: | 18 | docker run --rm -v $(pwd):/app -w /app golangci/golangci-lint \ 19 | golangci-lint run -v 20 | 21 | - name: unit test 22 | run: | 23 | make start_ssh_server 24 | go test -v ./... 25 | make clean 26 | -------------------------------------------------------------------------------- /test/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:16.04 2 | 3 | RUN apt-get update && apt-get install -y openssh-server curl 4 | RUN mkdir -p ~/.ssh 5 | RUN echo 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFCV9M5sAhOpVQftVS1VDJlXiR7RSaSvwDIDrSoB1Kgz7C60YmRVA+9+bg0dAHLZdF2IFBHjRiVb222a3w1/Lenrl354vHlHGSoyOFi9R1IKdE3zKDhgx02LAhD+N54Q5qE91ENqM8h/ZbWi2MZI2SB09v7Xnx0gYdsrR7r56B/iWXvVbQM78xcnRahEKrBo7z/reil+DVW/OqtvoWLv14xmqKhboPqgAPTTJHqclQcLxK+l4mG5nGVeNM0OQphiFAlkupXpQdvEweMnn+O1sNuFPOWBSNcs1yqdhZMN0HUHGSescpbxAv3ysXPqh/SqxoMAfLW5XEV9SBBO2Cf9OT minghe@oldmac.local' >> ~/.ssh/authorized_keys 6 | RUN mkdir /var/run/sshd 7 | RUN echo 'root:THEPASSWORDYOUCREATED' | chpasswd 8 | RUN sed -i 's/PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config 9 | 10 | # SSH login fix. Otherwise user is kicked off after login 11 | RUN sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/sshd 12 | 13 | ENV NOTVISIBLE "in users profile" 14 | RUN echo "export VISIBLE=now" >> /etc/profile 15 | 16 | EXPOSE 22 17 | CMD ["/usr/sbin/sshd", "-D"] 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-present Minghe Huang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-ssh-client 2 | 3 | This is a little pacakge helps you run command on remote host via SSH 4 | 5 | ![CI](https://github.com/metrue/go-ssh-client/workflows/ci/badge.svg) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/metrue/go-ssh-client)](https://goreportcard.com/report/github.com/metrue/go-ssh-client) 7 | [![Go Doc](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](http://godoc.org/github.com/metrue/go-ssh-client) 8 | [![asciicast](https://asciinema.org/a/WYvZVCSiAu6FuUksQuhTITIOU.svg)](https://asciinema.org/a/WYvZVCSiAu6FuUksQuhTITIOU) 9 | 10 | 11 | ```go 12 | package main 13 | 14 | import ( 15 | "log" 16 | "os" 17 | 18 | ssh "github.com/metrue/go-ssh-client" 19 | ) 20 | 21 | func main() { 22 | host := "127.0.0.1" 23 | script := ` 24 | x=1 25 | while [ $x -le 5 ]; do 26 | echo 'hello' 27 | x=$(( $x + 1 )) 28 | sleep 1 29 | done 30 | ` 31 | err := ssh.New(host). 32 | WithUser("root"). 33 | WithKey("/your/path/to/id_ras"). // Default is ~/.ssh/id_rsa 34 | WithPort("2222"). // Default is 22 35 | RunCommand(script, ssh.CommandOptions{ 36 | Stdout: os.Stdout, 37 | Stderr: os.Stderr, 38 | Stdin: os.Stdin, 39 | }) 40 | if err != nil { 41 | log.Fatal(err) 42 | } 43 | } 44 | ``` 45 | 46 | ## Test 47 | 48 | ``` 49 | $ make start_ssh_server 50 | $ make test 51 | $ make clean #clean up running Docker container 52 | ``` 53 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s= 2 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 3 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 4 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 5 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 6 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 7 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 8 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 9 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 10 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= 11 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 12 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 13 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 14 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 15 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 16 | golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= 17 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 18 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 19 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 20 | -------------------------------------------------------------------------------- /test/id_rsa: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn 3 | NhAAAAAwEAAQAAAQEAxQlfTObAITqVUH7VUtVQyZV4ke0Umkr8AyA60qAdSoM+wutGJkVQ 4 | Pvfm4NHQBy2XRdiBQR40YlW9ttmt8Nfy3p65d+eLx5RxkqMjhYvUdSCnRN8yg4YMdNiwIQ 5 | /jeeEOahPdRDajPIf2W1otjGSNkgdPb+158dIGHbK0e6+egf4ll71W0DO/MXJ0WoRCqwaO 6 | 8/63opfg1Vvzqrb6Fi79eMZqioW6D6oAD00yR6nJUHC8SvpeJhuZxlXjTNDkKYYhQJZLqV 7 | 6UHbxMHjJ5/jtbDbhTzlgUjXLNcqnYWTDdB1BxknrHKW8QL98rFz6of0qsaDAHy1uVxFfU 8 | gQTtgn/TkwAAA9D6tdLW+rXS1gAAAAdzc2gtcnNhAAABAQDFCV9M5sAhOpVQftVS1VDJlX 9 | iR7RSaSvwDIDrSoB1Kgz7C60YmRVA+9+bg0dAHLZdF2IFBHjRiVb222a3w1/Lenrl354vH 10 | lHGSoyOFi9R1IKdE3zKDhgx02LAhD+N54Q5qE91ENqM8h/ZbWi2MZI2SB09v7Xnx0gYdsr 11 | R7r56B/iWXvVbQM78xcnRahEKrBo7z/reil+DVW/OqtvoWLv14xmqKhboPqgAPTTJHqclQ 12 | cLxK+l4mG5nGVeNM0OQphiFAlkupXpQdvEweMnn+O1sNuFPOWBSNcs1yqdhZMN0HUHGSes 13 | cpbxAv3ysXPqh/SqxoMAfLW5XEV9SBBO2Cf9OTAAAAAwEAAQAAAQBERkQcjJSkrv0QQHLA 14 | 2iO9RiraPdF2yWbb2m4nj822hRXZStcq6betqg75dhpkclrJnATlwIacUGOFmZYZL2r70v 15 | onXzdjN7/G9PqZCuPali7/wWtqgaeNUYxuWGgVUnPBNBLm0RvtHJuz+eJwlGMt55SSDzLD 16 | JWhzH/pEJY2CMyaGLywTZeBfsfMiny4MOq72pzxcVUdsusHLBHOjk+qbSZPdI41yGHrzce 17 | CrYM3DC6acNjRGYVqYFNT8m8OlHawReaJKCteUFwNwJg+ix61KaUzkaTVuR4tvndpkC1k9 18 | 3/jdn+YNKo6jBpLmWk8WOHkbfWg0uOGL1GpF99GlcjeZAAAAgQDQWlQucpWiBF7ywhtpy5 19 | BVauaBgpQMm8RT5rpUhlR1Vg5Gr5LgS1i3NOZmLjkShLIT0hbBxnQuaMK+oG8NEE8f4Zig 20 | FtEbv9KTSK+GK40UUHGbqcF/rZWaRRrdDIgp9wcJ5CGRmCpaMhP//8aACJE9LG8njZbt3W 21 | W0XgEOFBZTtwAAAIEA/szan+51HdJFt/oEDpqXDQiqnAYaJ8RAkDvF5JWkryxdXnPo/WDd 22 | wmlc9P7WBb17aHuhPZ40vQcHr+ZJAhjz0k5i0agerD71BqnBYr2AE2Lsn4zw3goqk3Y5aR 23 | i7uZtqz62fW4759P4F72IymIeo0TpuecgwiBiHeXXXouHAIucAAACBAMX240ZtNEo2mW7B 24 | ZobtiYYHtj1eIfC7rXs+n2RYeDMBDD7jUKvMuofCFtZKd72IjHJZqNJ4NsE5jppf/sb3NB 25 | vMbau5BYnAj1b1yIQP8C8DEYPI2Q+3egi7dT+C8vQsbV/Q2Z7LKVeZujuvfLWKRBVH0Efh 26 | lbGNCfLUxaa8tSB1AAAAE21pbmdoZUBvbGRtYWMubG9jYWwBAgMEBQYH 27 | -----END OPENSSH PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /ssh_test.go: -------------------------------------------------------------------------------- 1 | package ssh 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestSSH(t *testing.T) { 12 | t.Run("Connectable", func(t *testing.T) { 13 | cases := []struct { 14 | host string 15 | connectable bool 16 | }{{ 17 | host: "127.0.0.1", 18 | connectable: true, 19 | }, 20 | { 21 | host: "192.3.0.1", 22 | connectable: false, 23 | }, 24 | } 25 | for _, c := range cases { 26 | client := New(c.host). 27 | WithUser("root"). 28 | WithPort("2222"). 29 | WithKey("./test/id_rsa") 30 | fmt.Println(client) 31 | ok, err := client.Connectable(5 * time.Second) 32 | if !ok { 33 | if err == nil { 34 | t.Fatalf("error should not be nil ") 35 | } 36 | } 37 | if ok != c.connectable { 38 | t.Fatalf("should get %v but got %v", c.connectable, ok) 39 | } 40 | } 41 | }) 42 | 43 | t.Run("public key", func(t *testing.T) { 44 | cases := []struct { 45 | cmd string 46 | stdout string 47 | stderr string 48 | }{ 49 | { 50 | cmd: "echo 1", 51 | stdout: "1\n", 52 | stderr: "", 53 | }, 54 | { 55 | cmd: "docker ps", 56 | stdout: "", 57 | stderr: "bash: docker: command not found\n", 58 | }, 59 | } 60 | 61 | for _, c := range cases { 62 | host := "127.0.0.1" 63 | var inPipe bytes.Buffer 64 | var outPipe bytes.Buffer 65 | var errPipe bytes.Buffer 66 | options := CommandOptions{ 67 | Stdin: bufio.NewReader(&inPipe), 68 | Stdout: bufio.NewWriter(&outPipe), 69 | Stderr: bufio.NewWriter(&errPipe), 70 | } 71 | _ = New(host). 72 | WithUser("root"). 73 | WithPort("2222"). 74 | WithKey("./test/id_rsa"). 75 | RunCommand(c.cmd, options) 76 | 77 | if errPipe.String() != c.stderr { 78 | t.Fatalf("should get %v but got %v", c.stderr, errPipe.String()) 79 | } 80 | if outPipe.String() != c.stdout { 81 | t.Fatalf("should get %v but got %v", c.stdout, outPipe.String()) 82 | } 83 | } 84 | }) 85 | 86 | t.Run("password", func(t *testing.T) { 87 | cases := []struct { 88 | cmd string 89 | stdout string 90 | stderr string 91 | }{ 92 | { 93 | cmd: "echo 1", 94 | stdout: "1\n", 95 | stderr: "", 96 | }, 97 | { 98 | cmd: "docker ps", 99 | stdout: "", 100 | stderr: "bash: docker: command not found\n", 101 | }, 102 | } 103 | 104 | for _, c := range cases { 105 | host := "127.0.0.1" 106 | var inPipe bytes.Buffer 107 | var outPipe bytes.Buffer 108 | var errPipe bytes.Buffer 109 | options := CommandOptions{ 110 | Stdin: bufio.NewReader(&inPipe), 111 | Stdout: bufio.NewWriter(&outPipe), 112 | Stderr: bufio.NewWriter(&errPipe), 113 | } 114 | _ = New(host). 115 | WithUser("root"). 116 | WithPort("2222"). 117 | WithPassword("THEPASSWORDYOUCREATED"). 118 | RunCommand(c.cmd, options) 119 | 120 | if errPipe.String() != c.stderr { 121 | t.Fatalf("should get %v but got %v", c.stderr, errPipe.String()) 122 | } 123 | 124 | if outPipe.String() != c.stdout { 125 | t.Fatalf("should get %v but got %v", c.stdout, outPipe.String()) 126 | } 127 | } 128 | }) 129 | } 130 | -------------------------------------------------------------------------------- /mocks/ssh.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: ssh.go 3 | 4 | // Package mock_ssh is a generated GoMock package. 5 | package mock_ssh 6 | 7 | import ( 8 | gomock "github.com/golang/mock/gomock" 9 | go_ssh_client "github.com/metrue/go-ssh-client" 10 | reflect "reflect" 11 | time "time" 12 | ) 13 | 14 | // MockClienter is a mock of Clienter interface 15 | type MockClienter struct { 16 | ctrl *gomock.Controller 17 | recorder *MockClienterMockRecorder 18 | } 19 | 20 | // MockClienterMockRecorder is the mock recorder for MockClienter 21 | type MockClienterMockRecorder struct { 22 | mock *MockClienter 23 | } 24 | 25 | // NewMockClienter creates a new mock instance 26 | func NewMockClienter(ctrl *gomock.Controller) *MockClienter { 27 | mock := &MockClienter{ctrl: ctrl} 28 | mock.recorder = &MockClienterMockRecorder{mock} 29 | return mock 30 | } 31 | 32 | // EXPECT returns an object that allows the caller to indicate expected use 33 | func (m *MockClienter) EXPECT() *MockClienterMockRecorder { 34 | return m.recorder 35 | } 36 | 37 | // WithServer mocks base method 38 | func (m *MockClienter) WithServer(add string) go_ssh_client.Client { 39 | m.ctrl.T.Helper() 40 | ret := m.ctrl.Call(m, "WithServer", add) 41 | ret0, _ := ret[0].(go_ssh_client.Client) 42 | return ret0 43 | } 44 | 45 | // WithServer indicates an expected call of WithServer 46 | func (mr *MockClienterMockRecorder) WithServer(add interface{}) *gomock.Call { 47 | mr.mock.ctrl.T.Helper() 48 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithServer", reflect.TypeOf((*MockClienter)(nil).WithServer), add) 49 | } 50 | 51 | // WithUser mocks base method 52 | func (m *MockClienter) WithUser(user string) go_ssh_client.Client { 53 | m.ctrl.T.Helper() 54 | ret := m.ctrl.Call(m, "WithUser", user) 55 | ret0, _ := ret[0].(go_ssh_client.Client) 56 | return ret0 57 | } 58 | 59 | // WithUser indicates an expected call of WithUser 60 | func (mr *MockClienterMockRecorder) WithUser(user interface{}) *gomock.Call { 61 | mr.mock.ctrl.T.Helper() 62 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithUser", reflect.TypeOf((*MockClienter)(nil).WithUser), user) 63 | } 64 | 65 | // WithPassword mocks base method 66 | func (m *MockClienter) WithPassword(password string) go_ssh_client.Client { 67 | m.ctrl.T.Helper() 68 | ret := m.ctrl.Call(m, "WithPassword", password) 69 | ret0, _ := ret[0].(go_ssh_client.Client) 70 | return ret0 71 | } 72 | 73 | // WithPassword indicates an expected call of WithPassword 74 | func (mr *MockClienterMockRecorder) WithPassword(password interface{}) *gomock.Call { 75 | mr.mock.ctrl.T.Helper() 76 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithPassword", reflect.TypeOf((*MockClienter)(nil).WithPassword), password) 77 | } 78 | 79 | // WithKey mocks base method 80 | func (m *MockClienter) WithKey(key string) go_ssh_client.Client { 81 | m.ctrl.T.Helper() 82 | ret := m.ctrl.Call(m, "WithKey", key) 83 | ret0, _ := ret[0].(go_ssh_client.Client) 84 | return ret0 85 | } 86 | 87 | // WithKey indicates an expected call of WithKey 88 | func (mr *MockClienterMockRecorder) WithKey(key interface{}) *gomock.Call { 89 | mr.mock.ctrl.T.Helper() 90 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithKey", reflect.TypeOf((*MockClienter)(nil).WithKey), key) 91 | } 92 | 93 | // WithPort mocks base method 94 | func (m *MockClienter) WithPort(port string) go_ssh_client.Client { 95 | m.ctrl.T.Helper() 96 | ret := m.ctrl.Call(m, "WithPort", port) 97 | ret0, _ := ret[0].(go_ssh_client.Client) 98 | return ret0 99 | } 100 | 101 | // WithPort indicates an expected call of WithPort 102 | func (mr *MockClienterMockRecorder) WithPort(port interface{}) *gomock.Call { 103 | mr.mock.ctrl.T.Helper() 104 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithPort", reflect.TypeOf((*MockClienter)(nil).WithPort), port) 105 | } 106 | 107 | // Connectable mocks base method 108 | func (m *MockClienter) Connectable(timeout time.Duration) (bool, error) { 109 | m.ctrl.T.Helper() 110 | ret := m.ctrl.Call(m, "Connectable", timeout) 111 | ret0, _ := ret[0].(bool) 112 | ret1, _ := ret[1].(error) 113 | return ret0, ret1 114 | } 115 | 116 | // Connectable indicates an expected call of Connectable 117 | func (mr *MockClienterMockRecorder) Connectable(timeout interface{}) *gomock.Call { 118 | mr.mock.ctrl.T.Helper() 119 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connectable", reflect.TypeOf((*MockClienter)(nil).Connectable), timeout) 120 | } 121 | 122 | // RunCommand mocks base method 123 | func (m *MockClienter) RunCommand(command string, options go_ssh_client.CommandOptions) error { 124 | m.ctrl.T.Helper() 125 | ret := m.ctrl.Call(m, "RunCommand", command, options) 126 | ret0, _ := ret[0].(error) 127 | return ret0 128 | } 129 | 130 | // RunCommand indicates an expected call of RunCommand 131 | func (mr *MockClienterMockRecorder) RunCommand(command, options interface{}) *gomock.Call { 132 | mr.mock.ctrl.T.Helper() 133 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RunCommand", reflect.TypeOf((*MockClienter)(nil).RunCommand), command, options) 134 | } 135 | -------------------------------------------------------------------------------- /ssh.go: -------------------------------------------------------------------------------- 1 | package ssh 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "log" 8 | "net" 9 | "path/filepath" 10 | "strings" 11 | "time" 12 | 13 | "github.com/mitchellh/go-homedir" 14 | "github.com/pkg/errors" 15 | "golang.org/x/crypto/ssh" 16 | ) 17 | 18 | // Clienter defines interface of SSH client 19 | type Clienter interface { 20 | WithServer(add string) Client 21 | WithUser(user string) Client 22 | WithPassword(password string) Client 23 | WithKey(key string) Client 24 | WithPort(port string) Client 25 | Connectable(timeout time.Duration) (bool, error) 26 | RunCommand(command string, options CommandOptions) error 27 | } 28 | 29 | // Client ssh client 30 | type Client struct { 31 | server string 32 | port string 33 | user string 34 | 35 | key string 36 | password string 37 | 38 | session *ssh.Session 39 | conn ssh.Conn 40 | } 41 | 42 | // New create a client 43 | func New(server string) Client { 44 | home, _ := homedir.Dir() 45 | return Client{ 46 | server: server, 47 | user: "root", 48 | port: "22", 49 | key: filepath.Join(home, ".ssh/id_rsa"), 50 | } 51 | } 52 | 53 | // WithServer with server 54 | func (c Client) WithServer(addr string) Client { 55 | return Client{ 56 | server: addr, 57 | port: c.port, 58 | user: c.user, 59 | key: c.key, 60 | password: c.password, 61 | } 62 | } 63 | 64 | // WithUser with key 65 | func (c Client) WithUser(user string) Client { 66 | return Client{ 67 | server: c.server, 68 | port: c.port, 69 | user: user, 70 | key: c.key, 71 | password: c.password, 72 | } 73 | } 74 | 75 | // WithPassword with key 76 | func (c Client) WithPassword(password string) Client { 77 | return Client{ 78 | server: c.server, 79 | port: c.port, 80 | user: c.user, 81 | key: c.key, 82 | password: password, 83 | } 84 | } 85 | 86 | // WithKey with key 87 | func (c Client) WithKey(keyfile string) Client { 88 | return Client{ 89 | server: c.server, 90 | port: c.port, 91 | user: c.user, 92 | key: keyfile, 93 | password: c.password, 94 | } 95 | } 96 | 97 | // WithPort with port 98 | func (c Client) WithPort(port string) Client { 99 | return Client{ 100 | server: c.server, 101 | port: port, 102 | user: c.user, 103 | key: c.key, 104 | password: c.password, 105 | } 106 | } 107 | 108 | // CommandOptions options for command 109 | type CommandOptions struct { 110 | Stdin io.Reader 111 | Stdout io.Writer 112 | Stderr io.Writer 113 | Timeout time.Duration 114 | Env []string 115 | } 116 | 117 | // RunCommand run command onto remote server via SSH 118 | func (c Client) RunCommand(command string, options CommandOptions) error { 119 | timeout := 20 * time.Second 120 | if options.Timeout > 0 { 121 | timeout = options.Timeout 122 | } 123 | client, err := c.connect(timeout) 124 | if err != nil { 125 | return err 126 | } 127 | 128 | defer func() { 129 | if err := client.disconnect(); err != nil { 130 | fmt.Println("-->", err) 131 | log.Println(err) 132 | } 133 | }() 134 | 135 | if options.Stdin != nil { 136 | stdin, err := client.session.StdinPipe() 137 | if err != nil { 138 | return fmt.Errorf("Unable to setup stdin for session: %v", err) 139 | } 140 | // nolint 141 | go io.Copy(stdin, options.Stdin) 142 | } 143 | 144 | if options.Stdout != nil { 145 | stdout, err := client.session.StdoutPipe() 146 | if err != nil { 147 | return fmt.Errorf("Unable to setup stdout for session: %v", err) 148 | } 149 | // nolint 150 | go io.Copy(options.Stdout, stdout) 151 | } 152 | 153 | if options.Stderr != nil { 154 | stderr, err := client.session.StderrPipe() 155 | if err != nil { 156 | return fmt.Errorf("Unable to setup stderr for session: %v", err) 157 | } 158 | // nolint 159 | go io.Copy(options.Stderr, stderr) 160 | } 161 | 162 | for _, env := range options.Env { 163 | variable := strings.Split(env, "=") 164 | if len(variable) != 2 { 165 | continue 166 | } 167 | 168 | if err := client.session.Setenv(variable[0], variable[1]); err != nil { 169 | return err 170 | } 171 | } 172 | 173 | return client.session.Run(command) 174 | } 175 | 176 | // Connectable check if client can connect to ssh server within timeout 177 | func (c Client) Connectable(timeout time.Duration) (bool, error) { 178 | client, err := c.connect(timeout) 179 | if err != nil { 180 | return false, err 181 | } 182 | 183 | defer func() { 184 | if err := client.disconnect(); err != nil { 185 | fmt.Println("-->", err) 186 | log.Println(err) 187 | } 188 | }() 189 | 190 | return true, nil 191 | } 192 | 193 | // Connect connect server 194 | func (c Client) connect(timeout time.Duration) (Client, error) { 195 | Auth := []ssh.AuthMethod{} 196 | 197 | if c.password != "" { 198 | Auth = append(Auth, ssh.Password(c.password)) 199 | } else if c.key != "" { 200 | publicKey, err := publicKey(c.key) 201 | if err != nil { 202 | return Client{}, err 203 | } 204 | Auth = append(Auth, publicKey) 205 | } else { 206 | return Client{}, fmt.Errorf("password or keyfile required for ssh connection ") 207 | } 208 | 209 | config := &ssh.ClientConfig{ 210 | User: c.user, 211 | Auth: Auth, 212 | Timeout: timeout, 213 | HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { return nil }, 214 | } 215 | 216 | addr := net.JoinHostPort(c.server, c.port) 217 | conn, err := ssh.Dial("tcp", addr, config) 218 | if err != nil { 219 | return Client{}, err 220 | } 221 | 222 | session, err := conn.NewSession() 223 | if err != nil { 224 | return Client{}, err 225 | } 226 | 227 | return Client{ 228 | server: c.server, 229 | port: c.port, 230 | user: c.user, 231 | key: c.key, 232 | password: c.password, 233 | 234 | conn: conn, 235 | session: session, 236 | }, nil 237 | } 238 | 239 | // Disconnect disconnect with server 240 | func (c Client) disconnect() error { 241 | if err := c.session.Close(); err != nil { 242 | // "https://github.com/golang/go/issues/28108" 243 | if err == io.EOF { 244 | return nil 245 | } 246 | return errors.Wrap(err, "session close failure") 247 | } 248 | if err := c.conn.Close(); err != nil { 249 | return errors.Wrap(err, "connection close failure") 250 | } 251 | return nil 252 | } 253 | 254 | func publicKey(file string) (ssh.AuthMethod, error) { 255 | buffer, err := ioutil.ReadFile(file) 256 | if err != nil { 257 | return nil, err 258 | } 259 | 260 | key, err := ssh.ParsePrivateKey(buffer) 261 | if err != nil { 262 | return nil, err 263 | } 264 | return ssh.PublicKeys(key), nil 265 | } 266 | 267 | var ( 268 | _ Clienter = Client{} 269 | ) 270 | --------------------------------------------------------------------------------