Skip to content

Commit

Permalink
implement server APIs (#6)
Browse files Browse the repository at this point in the history
  • Loading branch information
Eric Schrock authored Feb 6, 2020
1 parent 641513a commit b20e205
Show file tree
Hide file tree
Showing 5 changed files with 702 additions and 87 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module github.com/titan-data/ssh-remote-go

require (
github.com/stretchr/testify v1.4.0
github.com/titan-data/remote-sdk-go v0.1.0
github.com/titan-data/remote-sdk-go v0.2.1
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876
)

Expand Down
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a
github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI=
github.com/hashicorp/go-hclog v0.10.1 h1:uyt/l0dWjJ879yiAu+T7FG3/6QX+zwm4bQ8P7XsYt3o=
github.com/hashicorp/go-hclog v0.10.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v0.11.0 h1:zf3QG3ap4KOMHzDLxBvq9ZtEFVSxQzVdH1ccl5NK2tU=
github.com/hashicorp/go-hclog v0.11.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-plugin v1.0.1 h1:4OtAfUGbnKC6yS48p0CtMX2oFYtzFZVv6rok3cRWgnE=
github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M=
Expand All @@ -37,6 +39,7 @@ github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQ
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
Expand All @@ -48,6 +51,8 @@ github.com/titan-data/remote-sdk-go v0.0.3 h1:kGLc7JP7znTcXyl3gRML2jEST99ebdhz0C
github.com/titan-data/remote-sdk-go v0.0.3/go.mod h1:b4McaOFiLYWv2/wCQW/sE2BcCSNz/Ae6iKKEtqw703w=
github.com/titan-data/remote-sdk-go v0.1.0 h1:Xab4sduqyGdo04eJ5mQ2lIfAYZDjHJ4AzlK4oJ5EqqE=
github.com/titan-data/remote-sdk-go v0.1.0/go.mod h1:u7w3Olu3EtjoFcHzxUOzelxedey4q2nveuKLvgRO7tg=
github.com/titan-data/remote-sdk-go v0.2.1 h1:Aa1CWqSbPIIvuacy27nMcbmLF2eymLIJs8m+yW8ki8E=
github.com/titan-data/remote-sdk-go v0.2.1/go.mod h1:IYCrMWL1hFGoYusrTHgseG/bLVuY72LpQSSdGOrcHb4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876 h1:sKJQZMuxjOAR/Uo2LBfU90onWEf1dF4C+0hPJCc9Mpc=
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
Expand Down
64 changes: 64 additions & 0 deletions ssh/mock_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright The Titan Project Contributors.
*/
package ssh

import (
"github.com/stretchr/testify/mock"
"golang.org/x/crypto/ssh"
"net"
)

type MockConn struct {
mock.Mock
}

func (m *MockConn) User() string {
args := m.Called()
return args.String(0)
}

func (m *MockConn) SessionID() []byte {
args := m.Called()
return args.Get(0).([]byte)
}

func (m *MockConn) ClientVersion() []byte {
args := m.Called()
return args.Get(0).([]byte)
}

func (m *MockConn) ServerVersion() []byte {
args := m.Called()
return args.Get(0).([]byte)
}

func (m *MockConn) RemoteAddr() net.Addr {
args := m.Called()
return args.Get(0).(net.Addr)
}

func (m *MockConn) LocalAddr() net.Addr {
args := m.Called()
return args.Get(0).(net.Addr)
}

func (m *MockConn) SendRequest(name string, wantReply bool, payload []byte) (bool, []byte, error) {
args := m.Called(name, wantReply, payload)
return args.Bool(0), args.Get(1).([]byte), args.Error(2)
}

func (m MockConn) OpenChannel(name string, data []byte) (ssh.Channel, <-chan *ssh.Request, error) {
args := m.Called(name, data)
return args.Get(0).(ssh.Channel), args.Get(1).(<-chan *ssh.Request), args.Error(2)
}

func (m MockConn) Close() error {
args := m.Called()
return args.Error(0)
}

func (m MockConn) Wait() error {
args := m.Called()
return args.Error(0)
}
167 changes: 158 additions & 9 deletions ssh/ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@
package ssh

import (
"bufio"
"encoding/json"
"errors"
"fmt"
"github.com/titan-data/remote-sdk-go/remote"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/terminal"
"io/ioutil"
"net/url"
Expand Down Expand Up @@ -83,22 +86,35 @@ func (s sshRemote) FromURL(rawUrl string, additionalProperties map[string]string
return result, nil
}

func getPort(port interface{}) (int, error) {
portval := 0
if p, ok := port.(int); ok {
portval = p
}
if p, ok := port.(float32); ok {
portval = int(p)
}
if p, ok := port.(float64); ok {
portval = int(p)
}
if portval <= 0 || portval > 65535 {
return 0, errors.New("invalid port")
}
return portval, nil
}

func (s sshRemote) ToURL(properties map[string]interface{}) (string, map[string]string, error) {
u := fmt.Sprintf("ssh://%s", properties["username"])
if properties["password"] != nil {
u += ":*****"
}
u += fmt.Sprintf("@%s", properties["address"])
if properties["port"] != nil {
var port = 0
if flt, ok := properties["port"].(float32); ok {
port = int(flt)
} else if flt, ok := properties["port"].(float64); ok {
port = int(flt)
} else {
port = properties["port"].(int)
if port, ok := properties["port"]; ok {
portval, err := getPort(port)
if err != nil {
return "", nil, err
}
u += fmt.Sprintf(":%d", port)
u += fmt.Sprintf(":%d", portval)
}
if properties["path"].(string)[0:1] != "/" {
u += "/~/"
Expand Down Expand Up @@ -139,6 +155,139 @@ func (s sshRemote) GetParameters(remoteProperties map[string]interface{}) (map[s
return result, nil
}

func (s sshRemote) ValidateRemote(properties map[string]interface{}) error {
err := remote.ValidateFields(properties, []string{"username", "address", "path"}, []string{"password", "port", "keyFile"})
if err != nil {
return err
}
if port, ok := properties["port"]; ok {
_, err := getPort(port)
return err
}
return nil
}

func (s sshRemote) ValidateParameters(parameters map[string]interface{}) error {
return remote.ValidateFields(parameters, []string{}, []string{"password", "key"})
}

/*
* This method will parse the remote configuration and parameters to determine if we should use password
* authentication or key-based authentication. It returns a pair where exactly one element must be set, either
* the first (password) or second (key).
*/
func getAuth(properties map[string]interface{}, parameters map[string]interface{}) (string, string, error) {
paramsPassword, paramsPasswordOk := parameters["password"]
paramsKey, paramsKeyOk := parameters["key"]
remotePassword, remotePasswordOk := properties["password"]
if paramsPasswordOk && paramsKeyOk {
return "", "", errors.New("only one of password or key can be specified")
}
if paramsKeyOk {
return "", paramsKey.(string), nil
}
if paramsPasswordOk {
return paramsPassword.(string), "", nil
}
if remotePasswordOk {
return remotePassword.(string), "", nil
}
return "", "", errors.New("one of password or key must be specified")
}

var dial = ssh.Dial

func getConnection(properties map[string]interface{}, parameters map[string]interface{}) (*ssh.Client, error) {
password, key, err := getAuth(properties, parameters)
if err != nil {
return nil, err
}
config := &ssh.ClientConfig{
User: properties["username"].(string),
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}

if key != "" {
parsed, err := ssh.ParsePrivateKey([]byte(key))
if err != nil {
return nil, err
}
config.Auth = []ssh.AuthMethod{ssh.PublicKeys(parsed)}
} else {
config.Auth = []ssh.AuthMethod{ssh.Password(password)}
}

return dial("tcp", properties["address"].(string), config)
}

func runCommand(conn *ssh.Client, command string) ([]byte, error) {
sess, err := conn.NewSession()
if err != nil {
return nil, err
}
defer sess.Close()

output, err := sess.CombinedOutput(command)
if err != nil {
return nil, fmt.Errorf("failed to execute '%s': %w\n%s", command, err, string(output))
}
return output, nil
}

var run = runCommand

func readCommit(conn *ssh.Client, properties map[string]interface{}, commitId string) (*remote.Commit, error) {
output, err := run(conn, fmt.Sprintf("cat \"%s/%s/metadata.json\"", properties["path"], commitId))
if err != nil {
return nil, err
}

commit := map[string]interface{}{}
err = json.Unmarshal(output, &commit)
if err != nil {
return nil, err
}

return &remote.Commit{Id: commitId, Properties: commit}, nil
}

func (s sshRemote) ListCommits(properties map[string]interface{}, parameters map[string]interface{}, tags []remote.Tag) ([]remote.Commit, error) {
conn, err := getConnection(properties, parameters)
if err != nil {
return nil, err
}
defer conn.Close()

output, err := run(conn, fmt.Sprintf("ls -1 \"%s\"", properties["path"]))
if err != nil {
return nil, err
}

var ret []remote.Commit
scanner := bufio.NewScanner(strings.NewReader(string(output)))
for scanner.Scan() {
commitId := strings.TrimSpace(scanner.Text())
commit, err := readCommit(conn, properties, commitId)
if err == nil && remote.MatchTags(commit.Properties, tags) {
ret = append(ret, remote.Commit{Id: commit.Id, Properties: commit.Properties})
}
}

remote.SortCommits(ret)

return ret, nil
}

func (s sshRemote) GetCommit(properties map[string]interface{}, parameters map[string]interface{}, commitId string) (*remote.Commit, error) {
conn, err := getConnection(properties, parameters)
if err != nil {
return nil, err
}
defer conn.Close()

return readCommit(conn, properties, commitId)
}

func init() {
remote.Register(sshRemote{})
}
Loading

0 comments on commit b20e205

Please sign in to comment.