From f0b67e93831d16b7f6618632ad44d718c8318b87 Mon Sep 17 00:00:00 2001 From: Gauravudia <60897972+Gauravudia@users.noreply.github.com> Date: Fri, 5 Apr 2024 14:41:59 +0530 Subject: [PATCH] feat: add sftp library (#393) Co-authored-by: Dilip Kola Co-authored-by: Akash Chetty --- go.mod | 2 +- sftp/client.go | 113 ++++++++++++ sftp/mock_sftp/mock_sftp_client.go | 79 +++++++++ sftp/sftp.go | 101 +++++++++++ sftp/sftp_test.go | 273 +++++++++++++++++++++++++++++ sftp/testdata/ssh/test_key | 39 +++++ sftp/testdata/ssh/test_key.pub | 1 + 7 files changed, 607 insertions(+), 1 deletion(-) create mode 100644 sftp/client.go create mode 100644 sftp/mock_sftp/mock_sftp_client.go create mode 100644 sftp/sftp.go create mode 100644 sftp/sftp_test.go create mode 100644 sftp/testdata/ssh/test_key create mode 100644 sftp/testdata/ssh/test_key.pub diff --git a/go.mod b/go.mod index bf470ddb..8720f222 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/mitchellh/mapstructure v1.5.0 github.com/ory/dockertest/v3 v3.10.0 github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 + github.com/pkg/sftp v1.13.6 github.com/prometheus/client_golang v1.19.0 github.com/prometheus/client_model v0.6.0 github.com/prometheus/common v0.51.1 @@ -113,7 +114,6 @@ require ( github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/pierrec/lz4/v4 v4.1.17 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/pkg/sftp v1.13.6 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/prometheus/procfs v0.12.0 // indirect diff --git a/sftp/client.go b/sftp/client.go new file mode 100644 index 00000000..448de0b1 --- /dev/null +++ b/sftp/client.go @@ -0,0 +1,113 @@ +//go:generate mockgen -destination=mock_sftp/mock_sftp_client.go -package mock_sftp github.com/rudderlabs/rudder-go-kit/sftp Client +package sftp + +import ( + "errors" + "fmt" + "io" + "time" + + "github.com/pkg/sftp" + "golang.org/x/crypto/ssh" +) + +// SSHConfig represents the configuration for SSH connection +type SSHConfig struct { + HostName string + Port int + User string + AuthMethod string + PrivateKey string + Password string // Password for password-based authentication + DialTimeout time.Duration +} + +// sshClientConfig constructs an SSH client configuration based on the provided SSHConfig. +func sshClientConfig(config *SSHConfig) (*ssh.ClientConfig, error) { + if config == nil { + return nil, errors.New("config should not be nil") + } + + if config.HostName == "" { + return nil, errors.New("hostname should not be empty") + } + + if config.Port == 0 { + return nil, errors.New("port should not be empty") + } + + if config.User == "" { + return nil, errors.New("user should not be empty") + } + + var authMethods ssh.AuthMethod + + switch config.AuthMethod { + case PasswordAuth: + authMethods = ssh.Password(config.Password) + case KeyAuth: + privateKey, err := ssh.ParsePrivateKey([]byte(config.PrivateKey)) + if err != nil { + return nil, fmt.Errorf("cannot parse private key: %w", err) + } + authMethods = ssh.PublicKeys(privateKey) + default: + return nil, errors.New("unsupported authentication method") + } + + sshConfig := &ssh.ClientConfig{ + User: config.User, + Auth: []ssh.AuthMethod{authMethods}, + Timeout: config.DialTimeout, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + } + + return sshConfig, nil +} + +// NewSSHClient establishes an SSH connection and returns an SSH client +func NewSSHClient(config *SSHConfig) (*ssh.Client, error) { + sshConfig, err := sshClientConfig(config) + if err != nil { + return nil, fmt.Errorf("cannot configure SSH client: %w", err) + } + + sshClient, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", config.HostName, config.Port), sshConfig) + if err != nil { + return nil, fmt.Errorf("cannot dial SSH host %q:%d: %w", config.HostName, config.Port, err) + } + return sshClient, nil +} + +type clientImpl struct { + client *sftp.Client +} + +type Client interface { + Create(path string) (io.WriteCloser, error) + Open(path string) (io.ReadCloser, error) + Remove(path string) error +} + +// newSFTPClient creates an SFTP client with existing SSH client +func newSFTPClient(client *ssh.Client) (Client, error) { + sftpClient, err := sftp.NewClient(client) + if err != nil { + return nil, fmt.Errorf("cannot create SFTP client: %w", err) + } + return &clientImpl{ + client: sftpClient, + }, nil +} + +func (c *clientImpl) Create(path string) (io.WriteCloser, error) { + return c.client.Create(path) +} + +func (c *clientImpl) Open(path string) (io.ReadCloser, error) { + return c.client.Open(path) +} + +func (c *clientImpl) Remove(path string) error { + return c.client.Remove(path) +} diff --git a/sftp/mock_sftp/mock_sftp_client.go b/sftp/mock_sftp/mock_sftp_client.go new file mode 100644 index 00000000..8cba806c --- /dev/null +++ b/sftp/mock_sftp/mock_sftp_client.go @@ -0,0 +1,79 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/rudderlabs/rudder-go-kit/sftp (interfaces: Client) + +// Package mock_sftp is a generated GoMock package. +package mock_sftp + +import ( + io "io" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// MockClient is a mock of Client interface. +type MockClient struct { + ctrl *gomock.Controller + recorder *MockClientMockRecorder +} + +// MockClientMockRecorder is the mock recorder for MockClient. +type MockClientMockRecorder struct { + mock *MockClient +} + +// NewMockClient creates a new mock instance. +func NewMockClient(ctrl *gomock.Controller) *MockClient { + mock := &MockClient{ctrl: ctrl} + mock.recorder = &MockClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockClient) EXPECT() *MockClientMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockClient) Create(arg0 string) (io.WriteCloser, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", arg0) + ret0, _ := ret[0].(io.WriteCloser) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Create indicates an expected call of Create. +func (mr *MockClientMockRecorder) Create(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockClient)(nil).Create), arg0) +} + +// Open mocks base method. +func (m *MockClient) Open(arg0 string) (io.ReadCloser, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Open", arg0) + ret0, _ := ret[0].(io.ReadCloser) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Open indicates an expected call of Open. +func (mr *MockClientMockRecorder) Open(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Open", reflect.TypeOf((*MockClient)(nil).Open), arg0) +} + +// Remove mocks base method. +func (m *MockClient) Remove(arg0 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Remove", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// Remove indicates an expected call of Remove. +func (mr *MockClientMockRecorder) Remove(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Remove", reflect.TypeOf((*MockClient)(nil).Remove), arg0) +} diff --git a/sftp/sftp.go b/sftp/sftp.go new file mode 100644 index 00000000..1bfb9786 --- /dev/null +++ b/sftp/sftp.go @@ -0,0 +1,101 @@ +package sftp + +import ( + "fmt" + "io" + "os" + "path/filepath" + + "golang.org/x/crypto/ssh" +) + +const ( + // PasswordAuth indicates password-based authentication + PasswordAuth = "passwordAuth" + // KeyAuth indicates key-based authentication + KeyAuth = "keyAuth" +) + +// FileManager is an interface for managing files on a remote server +type FileManager interface { + Upload(localFilePath, remoteDir string) error + Download(remoteFilePath, localDir string) error + Delete(remoteFilePath string) error +} + +// fileManagerImpl is a real implementation of FileManager +type fileManagerImpl struct { + client Client +} + +func NewFileManager(sshClient *ssh.Client) (FileManager, error) { + sftpClient, err := newSFTPClient(sshClient) + if err != nil { + return nil, fmt.Errorf("cannot create SFTP client: %w", err) + } + return &fileManagerImpl{client: sftpClient}, nil +} + +// Upload uploads a file to the remote server +func (fm *fileManagerImpl) Upload(localFilePath, remoteDir string) error { + localFile, err := os.Open(localFilePath) + if err != nil { + return fmt.Errorf("cannot open local file: %w", err) + } + defer func() { + _ = localFile.Close() + }() + + remoteFileName := filepath.Join(remoteDir, filepath.Base(localFilePath)) + remoteFile, err := fm.client.Create(remoteFileName) + if err != nil { + return fmt.Errorf("cannot create remote file: %w", err) + } + defer func() { + _ = remoteFile.Close() + }() + + _, err = io.Copy(remoteFile, localFile) + if err != nil { + return fmt.Errorf("error copying file: %w", err) + } + + return nil +} + +// Download downloads a file from the remote server +func (fm *fileManagerImpl) Download(remoteFilePath, localDir string) error { + remoteFile, err := fm.client.Open(remoteFilePath) + if err != nil { + return fmt.Errorf("cannot open remote file: %w", err) + } + defer func() { + _ = remoteFile.Close() + }() + + localFileName := filepath.Join(localDir, filepath.Base(remoteFilePath)) + localFile, err := os.Create(localFileName) + if err != nil { + return fmt.Errorf("cannot create local file: %w", err) + } + defer func() { + _ = localFile.Close() + }() + + _, err = io.Copy(localFile, remoteFile) + if err != nil { + return fmt.Errorf("cannot copy remote file content to local file: %w", err) + } + + return nil +} + +// Delete deletes a file on the remote server +func (fm *fileManagerImpl) Delete(remoteFilePath string) error { + err := fm.client.Remove(remoteFilePath) + if err != nil { + return fmt.Errorf("cannot delete file: %w", err) + } + + return nil +} diff --git a/sftp/sftp_test.go b/sftp/sftp_test.go new file mode 100644 index 00000000..32692851 --- /dev/null +++ b/sftp/sftp_test.go @@ -0,0 +1,273 @@ +package sftp + +import ( + "bytes" + "fmt" + "io" + "net" + "os" + "path/filepath" + "strconv" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/ory/dockertest/v3" + "github.com/stretchr/testify/require" + + "github.com/rudderlabs/rudder-go-kit/sftp/mock_sftp" + "github.com/rudderlabs/rudder-go-kit/testhelper/docker/resource/sshserver" +) + +type nopWriteCloser struct { + io.Writer +} + +func (nwc *nopWriteCloser) Close() error { + return nil +} + +func TestSSHClientConfig(t *testing.T) { + // Read private key + privateKey, err := os.ReadFile("testdata/ssh/test_key") + require.NoError(t, err) + + type testCase struct { + description string + config *SSHConfig + expectedError error + } + + testCases := []testCase{ + { + description: "WithNilConfig", + config: nil, + expectedError: fmt.Errorf("config should not be nil"), + }, + { + description: "WithEmptyHostName", + config: &SSHConfig{ + HostName: "", + Port: 22, + User: "someUser", + AuthMethod: "passwordAuth", + Password: "somePassword", + }, + expectedError: fmt.Errorf("hostname should not be empty"), + }, + { + description: "WithEmptyPort", + config: &SSHConfig{ + HostName: "someHostName", + User: "someUser", + AuthMethod: "passwordAuth", + Password: "somePassword", + }, + expectedError: fmt.Errorf("port should not be empty"), + }, + { + description: "WithPassword", + config: &SSHConfig{ + HostName: "someHostName", + Port: 22, + User: "someUser", + AuthMethod: "passwordAuth", + Password: "somePassword", + }, + expectedError: nil, + }, + { + description: "WithPrivateKey", + config: &SSHConfig{ + HostName: "someHostName", + Port: 22, + User: "someUser", + AuthMethod: "keyAuth", + PrivateKey: string(privateKey), + }, + expectedError: nil, + }, + { + description: "WithUnsupportedAuthMethod", + config: &SSHConfig{ + HostName: "HostName", + Port: 22, + User: "someUser", + AuthMethod: "invalidAuth", + PrivateKey: "somePrivateKey", + }, + expectedError: fmt.Errorf("unsupported authentication method"), + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + sshConfig, err := sshClientConfig(tc.config) + if tc.expectedError != nil { + + require.Error(t, tc.expectedError, err.Error()) + require.Nil(t, sshConfig) + } else { + require.NoError(t, err) + require.NotNil(t, sshConfig) + } + }) + } +} + +func TestUpload(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Create local directory within the temporary directory + localDir, err := os.MkdirTemp("", t.Name()) + require.NoError(t, err) + + // Set up local path within the directory + localFilePath := filepath.Join(localDir, "test_file.json") + + // Create local file and write data to it + localFile, err := os.Create(localFilePath) + require.NoError(t, err) + defer func() { _ = localFile.Close() }() + data := []byte(`{"foo": "bar"}`) + err = os.WriteFile(localFilePath, data, 0o644) + require.NoError(t, err) + + remoteBuf := bytes.NewBuffer(nil) + + mockSFTPClient := mock_sftp.NewMockClient(ctrl) + mockSFTPClient.EXPECT().Create(gomock.Any()).Return(&nopWriteCloser{remoteBuf}, nil) + + fileManager := &fileManagerImpl{client: mockSFTPClient} + + err = fileManager.Upload(localFilePath, "someRemoteDir") + require.NoError(t, err) + require.Equal(t, data, remoteBuf.Bytes()) +} + +func TestDownload(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Create local directory within the temporary directory + localDir, err := os.MkdirTemp("", t.Name()) + require.NoError(t, err) + + // Set up local file path within the directory + localFilePath := filepath.Join(localDir, "test_file.json") + + data := []byte(`{"foo": "bar"}`) + remoteBuf := bytes.NewBuffer(data) + remoteReader := io.NopCloser(remoteBuf) + + mockSFTPClient := mock_sftp.NewMockClient(ctrl) + mockSFTPClient.EXPECT().Open(gomock.Any()).Return(remoteReader, nil) + + fileManager := &fileManagerImpl{client: mockSFTPClient} + + err = fileManager.Download(filepath.Join("someRemoteDir", "test_file.json"), localDir) + require.NoError(t, err) + localFileContents, err := os.ReadFile(localFilePath) + require.NoError(t, err) + require.Equal(t, data, localFileContents) +} + +func TestDelete(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + remoteFilePath := "someRemoteFilePath" + mockSFTPClient := mock_sftp.NewMockClient(ctrl) + mockSFTPClient.EXPECT().Remove(remoteFilePath).Return(nil) + + fileManager := &fileManagerImpl{client: mockSFTPClient} + + err := fileManager.Delete(remoteFilePath) + require.NoError(t, err) +} + +func TestSFTP(t *testing.T) { + pool, err := dockertest.NewPool("") + require.NoError(t, err) + + // Let's setup the SSH server + publicKeyPath, err := filepath.Abs("testdata/ssh/test_key.pub") + require.NoError(t, err) + sshServer, err := sshserver.Setup(pool, t, + sshserver.WithPublicKeyPath(publicKeyPath), + sshserver.WithCredentials("linuxserver.io", ""), + ) + require.NoError(t, err) + sshServerHost := fmt.Sprintf("localhost:%d", sshServer.Port) + t.Logf("SSH server is listening on %s", sshServerHost) + + // Read private key + privateKey, err := os.ReadFile("testdata/ssh/test_key") + require.NoError(t, err) + + // Setup ssh client + hostname, portStr, err := net.SplitHostPort(sshServerHost) + require.NoError(t, err) + port, err := strconv.Atoi(portStr) + require.NoError(t, err) + sshClient, err := NewSSHClient(&SSHConfig{ + User: "linuxserver.io", + HostName: hostname, + Port: port, + AuthMethod: "keyAuth", + PrivateKey: string(privateKey), + DialTimeout: 10 * time.Second, + }) + require.NoError(t, err) + + // Create session + session, err := sshClient.NewSession() + require.NoError(t, err) + defer func() { _ = session.Close() }() + + remoteDir := filepath.Join("/tmp", "remote") + err = session.Run(fmt.Sprintf("mkdir -p %s", remoteDir)) + require.NoError(t, err) + + sftpManger, err := NewFileManager(sshClient) + require.NoError(t, err) + + // Create local and remote directories within the temporary directory + baseDir := t.TempDir() + localDir := filepath.Join(baseDir, "local") + + err = os.MkdirAll(localDir, 0o755) + require.NoError(t, err) + + // Set up local and remote file paths within their respective directories + localFilePath := filepath.Join(localDir, "test_file.json") + remoteFilePath := filepath.Join(remoteDir, "test_file.json") + + // Create local file and write data to it + localFile, err := os.Create(localFilePath) + require.NoError(t, err) + defer func() { _ = localFile.Close() }() + data := []byte(`{"foo": "bar"}`) + err = os.WriteFile(localFilePath, data, 0o644) + require.NoError(t, err) + + err = sftpManger.Upload(localFilePath, remoteDir) + require.NoError(t, err) + + err = sftpManger.Download(remoteFilePath, baseDir) + require.NoError(t, err) + + localFileContents, err := os.ReadFile(localFilePath) + require.NoError(t, err) + downloadedFileContents, err := os.ReadFile(filepath.Join(baseDir, "test_file.json")) + require.NoError(t, err) + // Compare the contents of the local file and the downloaded file from the remote server + require.Equal(t, localFileContents, downloadedFileContents) + + err = sftpManger.Delete(remoteFilePath) + require.NoError(t, err) + + err = sftpManger.Download(remoteFilePath, baseDir) + require.Error(t, err, "cannot open remote file: file does not exist") +} diff --git a/sftp/testdata/ssh/test_key b/sftp/testdata/ssh/test_key new file mode 100644 index 00000000..c095f906 --- /dev/null +++ b/sftp/testdata/ssh/test_key @@ -0,0 +1,39 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +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 +bG9jYWwBAgMEBQYH +-----END OPENSSH PRIVATE KEY----- diff --git a/sftp/testdata/ssh/test_key.pub b/sftp/testdata/ssh/test_key.pub new file mode 100644 index 00000000..d7cfb5fb --- /dev/null +++ b/sftp/testdata/ssh/test_key.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDR/+aqSRndz2rDwxPPkWgQ7c8Z5w7921QV8niDVQG70TuGL+nJVhh8t92x3luoDald1TRE7KbJV7rzcjVXMlyKFS3AHV5DE/WZSGaijSLErQhRQabg9GJ4Ia/6vT+Ay9phtUZQ8KLm0BRWHTa9kHunZxMhrPzvaW8KdGFhuEZQdJuE1JJA6OcK1l+O5lmcTSU8qNZX/Bsns++hz8VkyVY6ovUCMAq7AUfnOzVEys8MO/k8JmSyrhrsMPHOIR2rf2O8Dew6alBe/MEEEsE7O7uszf8w8lvp28AYlxjGSm18vhhdOxwgwg2vSDbhyf5OG+33KoD3euRv1EGTDpEgTAyVAqqx6sD6l+xqhbRKS+eUMrNLUMvBeKgvhRzUmmv/2NMwR9H5bu/uAk8HUkOdWB6fkUF+IypuwbVqZRL6aOA18LwjTS1H4JQfGmpRYA21xZOKeFKcsnaySP/ZCdyI8eQe6tEQrITAJV5yCf7FUrJGBUMIawn5CcB2fnX6L+Q7ee8= abhimanyubabbar@Abhimanyus-MacBook-Pro.local