diff --git a/Makefile b/Makefile index 08dc6f4..6feb945 100644 --- a/Makefile +++ b/Makefile @@ -85,6 +85,6 @@ test-coverage: # use tests-integration for check integration tests cases # you need an installed docker to work -.PHONY: tests-integration -tests-integration: +.PHONY: test-e2e +test-e2e: GO_TESTING=true go test -tags=integration ./... \ No newline at end of file diff --git a/README.md b/README.md index ba732a7..6982db2 100644 --- a/README.md +++ b/README.md @@ -405,7 +405,7 @@ make test-coverage Run test e2e: ```bash -make tests-integration +make test-e2e ``` ## 🤝 Feedback diff --git a/configs/envconst/file.go b/configs/envconst/file.go index 4e1375d..09ca248 100644 --- a/configs/envconst/file.go +++ b/configs/envconst/file.go @@ -2,6 +2,7 @@ package envconst const ( FilenameConnections = "connection.json" + FilenameConfigSSH = "config" FilenameConfig = "configs" FilenameLogger = "log.log" ) diff --git a/configs/envconst/space.go b/configs/envconst/space.go new file mode 100644 index 0000000..3c6bfab --- /dev/null +++ b/configs/envconst/space.go @@ -0,0 +1,6 @@ +package envconst + +const ( + TypeStorageSpace = "storage" + TypeConfigSpace = "config" +) diff --git a/configs/envconst/storage.go b/configs/envconst/storage.go new file mode 100644 index 0000000..aa0960d --- /dev/null +++ b/configs/envconst/storage.go @@ -0,0 +1,7 @@ +package envconst + +const ( + TypeLocalStorage = "local" + TypeGitStorage = "git" + TypeS3Storage = "s3" +) diff --git a/configs/envname/name.go b/configs/envname/name.go index 6a8051c..9fed746 100644 --- a/configs/envname/name.go +++ b/configs/envname/name.go @@ -4,4 +4,6 @@ const ( Theme = "THEME" Logger = "LOGGER" Testing = "GO_TESTING" + Space = "SPACE" + Storage = "STORAGE" ) diff --git a/e2e/kernel/connect_test.go b/e2e/kernel/connect_test.go index 623a5df..da33701 100644 --- a/e2e/kernel/connect_test.go +++ b/e2e/kernel/connect_test.go @@ -4,6 +4,7 @@ package kernel import ( "context" + ssh2 "github.com/misha-ssh/kernel/pkg/ssh" "os/exec" "testing" "time" @@ -59,7 +60,7 @@ func TestIntegrationDefaultConnect(t *testing.T) { }, } - sshConnector := &connect.Ssh{ + sshConnector := &ssh2.Ssh{ Connection: connection, } @@ -127,7 +128,7 @@ func TestIntegrationPrivateKeyConnect(t *testing.T) { }, } - sshConnector := &connect.Ssh{ + sshConnector := &ssh2.Ssh{ Connection: connection, } session, err := sshConnector.Session() @@ -193,7 +194,7 @@ func TestIntegrationPrivateKeyConnectWithPassphrase(t *testing.T) { }, } - sshConnector := &connect.Ssh{ + sshConnector := &ssh2.Ssh{ Connection: connection, } session, err := sshConnector.Session() diff --git a/examples/ssh/external_port/main.go b/examples/ssh/external_port/main.go index 56a3b72..2bd5a0b 100644 --- a/examples/ssh/external_port/main.go +++ b/examples/ssh/external_port/main.go @@ -1,6 +1,7 @@ package main import ( + "github.com/misha-ssh/kernel/pkg/ssh" "time" "github.com/misha-ssh/kernel/pkg/connect" @@ -8,7 +9,7 @@ import ( // main for success connect start make command: up-ssh-port func main() { - ssh := &connect.Ssh{ + ssh := &ssh.Ssh{ Connection: &connect.Connect{ Alias: "test", Login: "root", diff --git a/examples/ssh/password/main.go b/examples/ssh/password/main.go index be6fe8e..cae98a7 100644 --- a/examples/ssh/password/main.go +++ b/examples/ssh/password/main.go @@ -1,6 +1,7 @@ package main import ( + "github.com/misha-ssh/kernel/pkg/ssh" "time" "github.com/misha-ssh/kernel/pkg/connect" @@ -8,7 +9,7 @@ import ( // main for success connect start make command: up-ssh func main() { - ssh := &connect.Ssh{ + ssh := &ssh.Ssh{ Connection: &connect.Connect{ Alias: "test", Login: "root", diff --git a/examples/ssh/private_key/main.go b/examples/ssh/private_key/main.go index c523ae9..c059cf4 100644 --- a/examples/ssh/private_key/main.go +++ b/examples/ssh/private_key/main.go @@ -1,6 +1,7 @@ package main import ( + "github.com/misha-ssh/kernel/pkg/ssh" "time" "github.com/misha-ssh/kernel/pkg/connect" @@ -8,7 +9,7 @@ import ( // main for success connect start make command: up-ssh-key func main() { - ssh := &connect.Ssh{ + ssh := &ssh.Ssh{ Connection: &connect.Connect{ Alias: "test", Login: "root", diff --git a/examples/ssh/private_key_passphare/main.go b/examples/ssh/private_key_passphare/main.go index 8db41f4..7e8f05c 100644 --- a/examples/ssh/private_key_passphare/main.go +++ b/examples/ssh/private_key_passphare/main.go @@ -1,6 +1,7 @@ package main import ( + "github.com/misha-ssh/kernel/pkg/ssh" "time" "github.com/misha-ssh/kernel/pkg/connect" @@ -8,7 +9,7 @@ import ( // main for success connect start make command: up-ssh-key-pass func main() { - ssh := &connect.Ssh{ + ssh := &ssh.Ssh{ Connection: &connect.Connect{ Alias: "test", Login: "root", diff --git a/internal/logger/storage.go b/internal/logger/storage.go index 08fedb4..72ec47b 100644 --- a/internal/logger/storage.go +++ b/internal/logger/storage.go @@ -11,25 +11,27 @@ import ( "github.com/misha-ssh/kernel/internal/storage" ) -const FileName = envconst.FilenameLogger +const Filename = envconst.FilenameLogger var ( - DirectionApp = storage.GetAppDir() - ErrGetStorageInfo = errors.New("err get info use log - storage") ErrCreateStorage = errors.New("err at created log file") ErrGetOpenFile = errors.New("err get open log file") ) -type StorageLogger struct{} +type StorageLogger struct { + storage *storage.Local +} func NewStorageLogger() *StorageLogger { - return &StorageLogger{} + return &StorageLogger{ + storage: storage.NewLocal(), + } } -func (sl *StorageLogger) createLogFile() error { - if !storage.Exists(DirectionApp, FileName) { - err := storage.Create(DirectionApp, FileName) +func (s *StorageLogger) createLogFile() error { + if !s.storage.Exists(Filename) { + err := s.storage.Create(Filename) if err != nil { return err } @@ -38,8 +40,8 @@ func (sl *StorageLogger) createLogFile() error { return nil } -func (sl *StorageLogger) log(value any, status StatusLog) error { - err := sl.createLogFile() +func (s *StorageLogger) log(value any, status StatusLog) error { + err := s.createLogFile() if err != nil { return ErrCreateStorage } @@ -51,7 +53,7 @@ func (sl *StorageLogger) log(value any, status StatusLog) error { logInfo := fmt.Sprintf("|%v| file: %s, line: %v, message: %#v", status, calledFile, line, value) - openLogFile, err := storage.GetOpenFile(DirectionApp, FileName, os.O_WRONLY|os.O_APPEND|os.O_CREATE) + openLogFile, err := s.storage.GetOpenFile(Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE) defer func(openLogFile *os.File) { err = openLogFile.Close() }(openLogFile) @@ -65,29 +67,29 @@ func (sl *StorageLogger) log(value any, status StatusLog) error { return nil } -func (sl *StorageLogger) Error(value any) { - err := sl.log(value, ErrorStatus) +func (s *StorageLogger) Error(value any) { + err := s.log(value, ErrorStatus) if err != nil { panic(err) } } -func (sl *StorageLogger) Debug(value any) { - err := sl.log(value, DebugStatus) +func (s *StorageLogger) Debug(value any) { + err := s.log(value, DebugStatus) if err != nil { panic(err) } } -func (sl *StorageLogger) Info(value any) { - err := sl.log(value, InfoStatus) +func (s *StorageLogger) Info(value any) { + err := s.log(value, InfoStatus) if err != nil { panic(err) } } -func (sl *StorageLogger) Warn(value any) { - err := sl.log(value, WarnStatus) +func (s *StorageLogger) Warn(value any) { + err := s.log(value, WarnStatus) if err != nil { panic(err) } diff --git a/internal/setup/setup.go b/internal/setup/setup.go index f2bf98e..9c62a73 100644 --- a/internal/setup/setup.go +++ b/internal/setup/setup.go @@ -3,6 +3,8 @@ package setup import ( "encoding/json" "errors" + "github.com/misha-ssh/kernel/internal/space" + "github.com/misha-ssh/kernel/pkg/ssh" "os/user" "github.com/misha-ssh/kernel/configs/envconst" @@ -17,7 +19,9 @@ import ( var ( ErrCreateFileConnection = errors.New("err create file connection") - ErrSetLoggerFromConfig = errors.New("err set logger from configs") + ErrSetLoggerFromConfig = errors.New("err set logger from config") + ErrSetStorageFromConfig = errors.New("err set storage from config") + ErrSetSpaceFromConfig = errors.New("err set space from config") ErrSetDefaultValue = errors.New("err set default value") ErrMarshalJson = errors.New("failed to marshal json") ErrWriteJson = errors.New("failed to write json") @@ -92,8 +96,10 @@ func initFileConfig() error { } defaultValues := map[string]string{ - envname.Theme: envconst.Theme, - envname.Logger: envconst.TypeStorageLogger, + envname.Theme: envconst.Theme, + envname.Logger: envconst.TypeStorageLogger, + envname.Space: envconst.TypeStorageSpace, + envname.Storage: envconst.TypeLocalStorage, } for key, value := range defaultValues { @@ -163,6 +169,42 @@ func initLoggerFromConfig() error { return nil } +func initStorageFromConfig() error { + storageType := config.Get(envname.Storage) + + switch storageType { + case envconst.TypeLocalStorage: + storage.Set(storage.NewLocal()) + default: + return ErrSetStorageFromConfig + } + + return nil +} + +func initSpaceFromConfig() error { + storageType := config.Get(envname.Space) + + switch storageType { + case envconst.TypeStorageSpace: + space.Set( + &space.Storage{ + Storage: storage.Get(), + }, + ) + case envconst.TypeConfigSpace: + space.Set( + &space.SSHConfig{ + Config: ssh.NewConfig(), + }, + ) + default: + return ErrSetSpaceFromConfig + } + + return nil +} + // Init performs complete application initialization: // 1. Config file setup // 2. Logger configuration @@ -187,6 +229,16 @@ func Init() { panic(err) } + err = initStorageFromConfig() + if err != nil { + panic(err) + } + + err = initSpaceFromConfig() + if err != nil { + panic(err) + } + err = initFileConnections() if err != nil { panic(err) diff --git a/internal/space/space.go b/internal/space/space.go new file mode 100644 index 0000000..d1d15e8 --- /dev/null +++ b/internal/space/space.go @@ -0,0 +1,29 @@ +package space + +import ( + "github.com/misha-ssh/kernel/internal/storage" + "github.com/misha-ssh/kernel/pkg/connect" +) + +type Space interface { + GetConnections() (*connect.Connections, error) + SaveConnection(connection *connect.Connect) error + UpdateConnection(connection *connect.Connect) (*connect.Connect, error) + DeleteConnection(connection *connect.Connect) error +} + +var defaultSpace Space + +func Get() Space { + if defaultSpace == nil { + defaultSpace = &Storage{ + Storage: storage.Get(), + } + } + + return defaultSpace +} + +func Set(space Space) { + defaultSpace = space +} diff --git a/internal/space/ssh_config.go b/internal/space/ssh_config.go new file mode 100644 index 0000000..78e5fa0 --- /dev/null +++ b/internal/space/ssh_config.go @@ -0,0 +1,26 @@ +package space + +import ( + "github.com/misha-ssh/kernel/pkg/connect" + "github.com/misha-ssh/kernel/pkg/ssh" +) + +type SSHConfig struct { + Config *ssh.Config +} + +func (s *SSHConfig) GetConnections() (*connect.Connections, error) { + return nil, nil +} + +func (s *SSHConfig) SaveConnection(connection *connect.Connect) error { + return nil +} + +func (s *SSHConfig) UpdateConnection(connection *connect.Connect) (*connect.Connect, error) { + return nil, nil +} + +func (s *SSHConfig) DeleteConnection(connection *connect.Connect) error { + return nil +} diff --git a/internal/space/storage.go b/internal/space/storage.go new file mode 100644 index 0000000..3f71a21 --- /dev/null +++ b/internal/space/storage.go @@ -0,0 +1,26 @@ +package space + +import ( + "github.com/misha-ssh/kernel/internal/storage" + "github.com/misha-ssh/kernel/pkg/connect" +) + +type Storage struct { + Storage storage.Storage +} + +func (s *Storage) GetConnections() (*connect.Connections, error) { + return nil, nil +} + +func (s *Storage) SaveConnection(connection *connect.Connect) error { + return nil +} + +func (s *Storage) UpdateConnection(connection *connect.Connect) (*connect.Connect, error) { + return nil, nil +} + +func (s *Storage) DeleteConnection(connection *connect.Connect) error { + return nil +} diff --git a/internal/storage/git.go b/internal/storage/git.go new file mode 100644 index 0000000..6c98bb5 --- /dev/null +++ b/internal/storage/git.go @@ -0,0 +1,4 @@ +package storage + +// Git todo add logic +type Git struct{} diff --git a/internal/storage/git_test.go b/internal/storage/git_test.go new file mode 100644 index 0000000..82be054 --- /dev/null +++ b/internal/storage/git_test.go @@ -0,0 +1 @@ +package storage diff --git a/internal/storage/helper.go b/internal/storage/helper.go deleted file mode 100644 index 60d4724..0000000 --- a/internal/storage/helper.go +++ /dev/null @@ -1,96 +0,0 @@ -package storage - -import ( - "errors" - "os" - "os/user" - "path/filepath" - "strings" - - "github.com/misha-ssh/kernel/configs/envconst" - "github.com/misha-ssh/kernel/configs/envname" -) - -const CharHidden = "." - -// GetAppDir get dir application -func GetAppDir() string { - usr, err := user.Current() - if err != nil { - panic(err) - } - - hiddenDir := CharHidden + envconst.AppName - - if os.Getenv(envname.Testing) == envconst.IsTesting { - return filepath.Join(os.TempDir(), hiddenDir) - } - - return filepath.Join(usr.HomeDir, hiddenDir) -} - -// GetUserPrivateKey get file with ssh keys -func GetUserPrivateKey() ([]string, error) { - var privateKeys []string - - homeDir, err := os.UserHomeDir() - if err != nil { - panic(err) - } - - listDeniedPatternKeys := []string{ - ".pub", - "known_hosts", - "config", - "authorized_keys", - } - - keysDir := filepath.Join(homeDir, envconst.DirectionsUserPrivateKey) - - keys, err := os.ReadDir(keysDir) - if err != nil || len(keys) == 0 { - return []string{}, errors.New("cannot find user private keys") - } - - for _, key := range keys { - if key.IsDir() { - continue - } - - keyName := key.Name() - - containsPattern := false - for _, pattern := range listDeniedPatternKeys { - if strings.Contains(keyName, pattern) { - containsPattern = true - break - } - } - - if !containsPattern { - privateKeys = append(privateKeys, filepath.Join(keysDir, keyName)) - } - } - - if len(privateKeys) == 0 { - return []string{}, errors.New("cannot find user private keys") - } - - return privateKeys, nil -} - -// GetPrivateKeysDir get dir where save private keys -func GetPrivateKeysDir() string { - return filepath.Join(GetAppDir(), envconst.DirectionPrivateKeys) -} - -// GetDirectionAndFilename get dir and filename from full path -func GetDirectionAndFilename(fullPath string) (string, string) { - return filepath.Dir(fullPath), - filepath.Base(fullPath) -} - -// GetFullPath get full path from dir and filename -func GetFullPath(direction string, filename string) string { - return filepath.Join(direction, filename) -} diff --git a/internal/storage/helper_test.go b/internal/storage/helper_test.go deleted file mode 100644 index 09c3cf3..0000000 --- a/internal/storage/helper_test.go +++ /dev/null @@ -1,180 +0,0 @@ -//go:build unit - -package storage - -import ( - "os" - "os/user" - "path/filepath" - "reflect" - "testing" - - "github.com/misha-ssh/kernel/configs/envconst" - "github.com/misha-ssh/kernel/configs/envname" - "github.com/stretchr/testify/require" -) - -func TestGetAppDir(t *testing.T) { - originalTesting := os.Getenv(envname.Testing) - defer func() { - require.NoError(t, os.Setenv(envname.Testing, originalTesting)) - }() - - tests := []struct { - name string - want func() string - isSetTesting bool - }{ - { - name: "success - get app dir", - want: func() string { - usr, err := user.Current() - require.NoError(t, err) - - return filepath.Join(usr.HomeDir, CharHidden+envconst.AppName) - }, - isSetTesting: false, - }, - { - name: "success - get test app dir", - want: func() string { - return filepath.Join(os.TempDir(), CharHidden+envconst.AppName) - }, - isSetTesting: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.isSetTesting { - require.NoError(t, os.Setenv(envname.Testing, "true")) - } else { - require.NoError(t, os.Setenv(envname.Testing, "false")) - } - - require.Equal(t, GetAppDir(), tt.want()) - }) - } -} - -func TestGetDirectionAndFilename(t *testing.T) { - tests := []struct { - name string - fullPath string - wantDir string - wantFile string - }{ - { - name: "simple path", - fullPath: "/home/user/file.txt", - wantDir: "/home/user", - wantFile: "file.txt", - }, - { - name: "nested path", - fullPath: "/home/user/documents/file.txt", - wantDir: "/home/user/documents", - wantFile: "file.txt", - }, - { - name: "current dir file", - fullPath: "file.txt", - wantDir: ".", - wantFile: "file.txt", - }, - { - name: "empty path", - fullPath: "", - wantDir: ".", - wantFile: ".", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotDir, gotFile := GetDirectionAndFilename(tt.fullPath) - require.Equal(t, gotDir, tt.wantDir) - require.Equal(t, gotFile, tt.wantFile) - }) - } -} - -func TestGetFullPath(t *testing.T) { - tests := []struct { - name string - direction string - filename string - want string - }{ - { - name: "simple join", - direction: "/home/user", - filename: "file.txt", - want: "/home/user/file.txt", - }, - { - name: "empty dir", - direction: "", - filename: "file.txt", - want: "file.txt", - }, - { - name: "empty filename", - direction: "/home/user", - filename: "", - want: "/home/user", - }, - { - name: "both empty", - direction: "", - filename: "", - want: "", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - require.Equal(t, GetFullPath(tt.direction, tt.filename), tt.want) - }) - } -} - -func TestGetPrivateKeysDir(t *testing.T) { - tests := []struct { - name string - want func() string - }{ - { - name: "success - get private keys dir", - want: func() string { - return filepath.Join(GetAppDir(), envconst.DirectionPrivateKeys) - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - require.Equal(t, GetPrivateKeysDir(), tt.want()) - }) - } -} - -func TestGetUserPrivateKey(t *testing.T) { - tests := []struct { - name string - want []string - wantErr bool - }{ - // TODO: Add test cases. - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := GetUserPrivateKey() - if (err != nil) != tt.wantErr { - t.Errorf("GetUserPrivateKey() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("GetUserPrivateKey() got = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/internal/storage/local.go b/internal/storage/local.go new file mode 100644 index 0000000..efb73a1 --- /dev/null +++ b/internal/storage/local.go @@ -0,0 +1,103 @@ +package storage + +import ( + "errors" + "os" + "path/filepath" + "strings" +) + +type Local struct { + Path string +} + +var ErrEmptyDirectory = errors.New("empty directory") +var ErrDeleteDirectory = errors.New("get dir, delete only file") + +func NewLocal() *Local { + return &Local{ + Path: GetAppDir(), + } +} + +// Create creates a new file at the specified path, including parent directories if needed. +// Returns error if file creation fails. +func (l *Local) Create(filename string) error { + if strings.TrimSpace(l.Path) == "" { + return ErrEmptyDirectory + } + + if _, err := os.Stat(l.Path); os.IsNotExist(err) { + err = os.Mkdir(l.Path, os.ModePerm) + if err != nil { + return err + } + } + + if strings.TrimSpace(filename) != "" { + return l.Write(filename, "") + } + + return nil +} + +// Delete removes the specified file. Returns error if deletion fails. +func (l *Local) Delete(filename string) error { + file := filepath.Join(l.Path, filename) + info, err := os.Stat(file) + if err != nil { + return err + } + + if info.IsDir() { + return ErrDeleteDirectory + } + + return os.Remove(filepath.Join(l.Path, filename)) +} + +// Exists checks if a file exists at the given path and is not a directory. +// Returns boolean indicating existence. +func (l *Local) Exists(filename string) bool { + info, err := os.Stat(filepath.Join(l.Path, filename)) + if errors.Is(err, os.ErrNotExist) { + return false + } + + return !info.IsDir() +} + +// Get reads and returns the contents of a file as a string. +// Returns error if file cannot be read. +func (l *Local) Get(filename string) (string, error) { + data, err := os.ReadFile(filepath.Join(l.Path, filename)) + if err != nil { + return "", err + } + + return string(data), nil +} + +// Write saves data to a file, overwriting existing content. +// Creates file if it doesn't exist. Returns error on failure. +func (l *Local) Write(filename string, data string) error { + err := os.WriteFile(filepath.Join(l.Path, filename), []byte(data), os.ModePerm) + if err != nil { + return err + } + + return nil +} + +// GetOpenFile opens a file with specified flags (os.O_RDWR, etc.) and returns the file handle. +// Returns error if file cannot be opened. +func (l *Local) GetOpenFile(filename string, flags int) (*os.File, error) { + file := filepath.Join(l.Path, filename) + + openFile, err := os.OpenFile(file, flags, os.ModePerm) + if err != nil { + return nil, err + } + + return openFile, nil +} diff --git a/internal/storage/local_test.go b/internal/storage/local_test.go new file mode 100644 index 0000000..9960883 --- /dev/null +++ b/internal/storage/local_test.go @@ -0,0 +1,353 @@ +package storage + +import ( + "bytes" + "github.com/stretchr/testify/require" + "os" + "path/filepath" + "testing" +) + +func TestCreate(t *testing.T) { + type args struct { + direction string + filename string + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "success - create file", + args: args{ + direction: t.TempDir(), + filename: "test.txt", + }, + wantErr: false, + }, + { + name: "success - create dir", + args: args{ + direction: t.TempDir() + "/new_dir", + filename: "", + }, + wantErr: false, + }, + { + name: "fail - empty dir", + args: args{ + direction: "", + filename: "new.txt", + }, + wantErr: true, + }, + { + name: "fail - empty dir and filename", + args: args{ + direction: "", + filename: "", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + localStorage := new(Local) + + err := localStorage.Create(tt.args.direction, tt.args.filename) + require.Equal(t, tt.wantErr, err != nil) + }) + } +} + +func TestDelete(t *testing.T) { + type args struct { + direction string + filename string + } + tests := []struct { + name string + args args + isCreateFile bool + wantErr bool + }{ + { + name: "success - delete file", + args: args{ + direction: t.TempDir(), + filename: "test.txt", + }, + isCreateFile: true, + wantErr: false, + }, + { + name: "fail - delete non exists file", + args: args{ + direction: t.TempDir(), + filename: "nonexistent.txt", + }, + isCreateFile: false, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + localStorage := new(Local) + + if tt.isCreateFile { + require.NoError(t, localStorage.Create(tt.args.direction, tt.args.filename)) + } + + err := localStorage.Delete(tt.args.direction, tt.args.filename) + require.Equal(t, tt.wantErr, err != nil) + }) + } +} + +func TestExists(t *testing.T) { + tempDir := t.TempDir() + + type args struct { + direction string + filename string + } + tests := []struct { + name string + args args + isCreateFile bool + want bool + }{ + { + name: "success - is exists", + args: args{ + direction: tempDir, + filename: "test.txt", + }, + isCreateFile: true, + want: true, + }, + { + name: "fail - is not exists", + args: args{ + direction: tempDir, + filename: "nonexistent.txt", + }, + isCreateFile: false, + want: false, + }, + { + name: "fail - is not exists empty file", + args: args{ + direction: tempDir, + filename: "", + }, + isCreateFile: false, + want: false, + }, + { + name: "fail - is not exists empty dir", + args: args{ + direction: "", + filename: "text.txt", + }, + isCreateFile: false, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + localStorage := new(Local) + + if tt.isCreateFile { + require.NoError(t, localStorage.Create(tt.args.direction, tt.args.filename)) + } + + require.Equal(t, localStorage.Exists(tt.args.direction, tt.args.filename), tt.want) + }) + } +} + +func TestGet(t *testing.T) { + tempDir := t.TempDir() + + testFiles := map[string][]byte{ + "test.txt": []byte("test data"), + "empty.txt": []byte(""), + "large.txt": make([]byte, 1024*1024), + "large-repeat.txt": bytes.Repeat([]byte("x"), 1024*1024), + } + + for filename, data := range testFiles { + filePath := filepath.Join(tempDir, filename) + require.NoError(t, os.WriteFile(filePath, data, 0644)) + } + + tests := []struct { + name string + direction string + filename string + want string + wantErr bool + }{ + { + name: "success - read test file", + direction: tempDir, + filename: "test.txt", + want: string(testFiles["test.txt"]), + wantErr: false, + }, + { + name: "success - read empty file", + direction: tempDir, + filename: "empty.txt", + want: string(testFiles["empty.txt"]), + wantErr: false, + }, + { + name: "success - read large file", + direction: tempDir, + filename: "large.txt", + want: string(testFiles["large.txt"]), + wantErr: false, + }, + { + name: "success - read large file", + direction: tempDir, + filename: "large-repeat.txt", + want: string(testFiles["large-repeat.txt"]), + wantErr: false, + }, + { + name: "fail - non-existent file", + direction: tempDir, + filename: "nonexistent.txt", + want: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + localStorage := new(Local) + + got, err := localStorage.Get(tt.direction, tt.filename) + require.Equal(t, tt.wantErr, err != nil) + require.Equal(t, got, tt.want) + }) + } +} + +func TestWrite(t *testing.T) { + type args struct { + direction string + filename string + data string + } + tests := []struct { + name string + args args + wantErr bool + want string + }{ + { + name: "write to new file", + args: args{ + direction: t.TempDir(), + filename: "test.txt", + data: "Hello, World!", + }, + wantErr: false, + want: "Hello, World!", + }, + { + name: "write to existing file (overwrite)", + args: args{ + direction: t.TempDir(), + filename: "test.txt", + data: "New content", + }, + wantErr: false, + want: "New content", + }, + { + name: "write empty data to new file", + args: args{ + direction: t.TempDir(), + filename: "empty.txt", + data: "", + }, + wantErr: false, + want: "", + }, + { + name: "write to invalid filename", + args: args{ + direction: t.TempDir(), + filename: "", + data: "Invalid", + }, + wantErr: true, + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + localStorage := new(Local) + + err := localStorage.Write(tt.args.direction, tt.args.filename, tt.args.data) + require.Equal(t, tt.wantErr, err != nil) + + got, err := localStorage.Get(tt.args.direction, tt.args.filename) + require.Equal(t, tt.wantErr, err != nil) + require.Equal(t, got, tt.want) + }) + } +} + +func TestGetOpenFile(t *testing.T) { + type args struct { + direction string + filename string + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "write to new file", + args: args{ + direction: t.TempDir(), + filename: "test.txt", + }, + wantErr: false, + }, + { + name: "error on invalid directory", + args: args{ + direction: "invalidDir" + t.TempDir(), + filename: "test.txt", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + localStorage := new(Local) + + flags := os.O_WRONLY | os.O_APPEND | os.O_CREATE + got, err := localStorage.GetOpenFile(tt.args.direction, tt.args.filename, flags) + require.Equal(t, tt.wantErr, err != nil) + + _, err = got.Write([]byte("test")) + require.Equal(t, tt.wantErr, err != nil) + + err = got.Close() + require.Equal(t, tt.wantErr, err != nil) + + fileIsExists := localStorage.Exists(tt.args.direction, tt.args.filename) + require.Equal(t, fileIsExists, !tt.wantErr) + }) + } +} diff --git a/internal/storage/s3.go b/internal/storage/s3.go new file mode 100644 index 0000000..1ff4769 --- /dev/null +++ b/internal/storage/s3.go @@ -0,0 +1,4 @@ +package storage + +// S3 todo add logic +type S3 struct{} diff --git a/internal/storage/s3_test.go b/internal/storage/s3_test.go new file mode 100644 index 0000000..82be054 --- /dev/null +++ b/internal/storage/s3_test.go @@ -0,0 +1 @@ +package storage diff --git a/internal/storage/storage.go b/internal/storage/storage.go index 023455e..94075ce 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -1,93 +1,76 @@ package storage import ( - "errors" + "github.com/misha-ssh/kernel/configs/envconst" + "github.com/misha-ssh/kernel/configs/envname" "os" + "os/user" "path/filepath" - "strings" ) -var ErrEmptyDirectory = errors.New("empty directory") -var ErrDeleteDirectory = errors.New("get dir, delete only file") +type Storage interface { + Create(filename string) error + Delete(filename string) error + Exists(filename string) bool + Get(filename string) (string, error) + Write(filename string, data string) error + GetOpenFile(filename string, flags int) (*os.File, error) +} -// Create creates a new file at the specified path, including parent directories if needed. -// Returns error if file creation fails. -func Create(path string, filename string) error { - if strings.TrimSpace(path) == "" { - return ErrEmptyDirectory - } +var defaultStorage Storage - if _, err := os.Stat(path); os.IsNotExist(err) { - err = os.Mkdir(path, os.ModePerm) - if err != nil { - return err - } +func Get() Storage { + if defaultStorage == nil { + defaultStorage = NewLocal() } - if strings.TrimSpace(filename) != "" { - return Write(path, filename, "") - } + return defaultStorage +} - return nil +func Set(storage Storage) { + defaultStorage = storage } -// Delete removes the specified file. Returns error if deletion fails. -func Delete(path string, filename string) error { - file := filepath.Join(path, filename) - info, err := os.Stat(file) - if err != nil { - return err - } +const CharHidden = "." - if info.IsDir() { - return ErrDeleteDirectory +// GetAppDir get dir application +func GetAppDir() string { + usr, err := user.Current() + if err != nil { + panic(err) } - return os.Remove(filepath.Join(path, filename)) -} + hiddenDir := CharHidden + envconst.AppName -// Exists checks if a file exists at the given path and is not a directory. -// Returns boolean indicating existence. -func Exists(path string, filename string) bool { - info, err := os.Stat(filepath.Join(path, filename)) - if errors.Is(err, os.ErrNotExist) { - return false + if os.Getenv(envname.Testing) == envconst.IsTesting { + return filepath.Join(os.TempDir(), hiddenDir) } - return !info.IsDir() + return filepath.Join(usr.HomeDir, hiddenDir) } -// Get reads and returns the contents of a file as a string. -// Returns error if file cannot be read. -func Get(path string, filename string) (string, error) { - data, err := os.ReadFile(filepath.Join(path, filename)) +// GetDirSSH get dir ssh +func GetDirSSH() string { + homeDir, err := os.UserHomeDir() if err != nil { - return "", err + panic(err) } - return string(data), nil + return filepath.Join(homeDir, envconst.DirectionsUserPrivateKey) } -// Write saves data to a file, overwriting existing content. -// Creates file if it doesn't exist. Returns error on failure. -func Write(path string, filename string, data string) error { - err := os.WriteFile(filepath.Join(path, filename), []byte(data), os.ModePerm) - if err != nil { - return err - } - - return nil +// GetPrivateKeysDir get dir where save private keys +func GetPrivateKeysDir() string { + return filepath.Join(GetAppDir(), envconst.DirectionPrivateKeys) } -// GetOpenFile opens a file with specified flags (os.O_RDWR, etc.) and returns the file handle. -// Returns error if file cannot be opened. -func GetOpenFile(path string, filename string, flags int) (*os.File, error) { - file := filepath.Join(path, filename) - - openFile, err := os.OpenFile(file, flags, os.ModePerm) - if err != nil { - return nil, err - } +// GetDirectionAndFilename get dir and filename from full path +func GetDirectionAndFilename(fullPath string) (string, string) { + return filepath.Dir(fullPath), + filepath.Base(fullPath) +} - return openFile, nil +// GetFullPath get full path from dir and filename +func GetFullPath(direction string, filename string) string { + return filepath.Join(direction, filename) } diff --git a/internal/storage/storage_test.go b/internal/storage/storage_test.go index fa3bdee..4db59dc 100644 --- a/internal/storage/storage_test.go +++ b/internal/storage/storage_test.go @@ -3,342 +3,155 @@ package storage import ( - "bytes" + "github.com/misha-ssh/kernel/configs/envconst" + "github.com/misha-ssh/kernel/configs/envname" "os" + "os/user" "path/filepath" "testing" "github.com/stretchr/testify/require" ) -func TestCreate(t *testing.T) { - type args struct { - direction string - filename string - } - tests := []struct { - name string - args args - wantErr bool - }{ - { - name: "success - create file", - args: args{ - direction: t.TempDir(), - filename: "test.txt", - }, - wantErr: false, - }, - { - name: "success - create dir", - args: args{ - direction: t.TempDir() + "/new_dir", - filename: "", - }, - wantErr: false, - }, - { - name: "fail - empty dir", - args: args{ - direction: "", - filename: "new.txt", - }, - wantErr: true, - }, - { - name: "fail - empty dir and filename", - args: args{ - direction: "", - filename: "", - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := Create(tt.args.direction, tt.args.filename) - require.Equal(t, tt.wantErr, err != nil) - }) - } -} +func TestGetAppDir(t *testing.T) { + originalTesting := os.Getenv(envname.Testing) + defer func() { + require.NoError(t, os.Setenv(envname.Testing, originalTesting)) + }() -func TestDelete(t *testing.T) { - type args struct { - direction string - filename string - } tests := []struct { name string - args args - isCreateFile bool - wantErr bool + want func() string + isSetTesting bool }{ { - name: "success - delete file", - args: args{ - direction: t.TempDir(), - filename: "test.txt", + name: "success - get app dir", + want: func() string { + usr, err := user.Current() + require.NoError(t, err) + + return filepath.Join(usr.HomeDir, CharHidden+envconst.AppName) }, - isCreateFile: true, - wantErr: false, + isSetTesting: false, }, { - name: "fail - delete non exists file", - args: args{ - direction: t.TempDir(), - filename: "nonexistent.txt", + name: "success - get test app dir", + want: func() string { + return filepath.Join(os.TempDir(), CharHidden+envconst.AppName) }, - isCreateFile: false, - wantErr: true, + isSetTesting: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if tt.isCreateFile { - require.NoError(t, Create(tt.args.direction, tt.args.filename)) + if tt.isSetTesting { + require.NoError(t, os.Setenv(envname.Testing, "true")) + } else { + require.NoError(t, os.Setenv(envname.Testing, "false")) } - err := Delete(tt.args.direction, tt.args.filename) - require.Equal(t, tt.wantErr, err != nil) + require.Equal(t, GetAppDir(), tt.want()) }) } } -func TestExists(t *testing.T) { - tempDir := t.TempDir() - - type args struct { - direction string - filename string - } +func TestGetDirectionAndFilename(t *testing.T) { tests := []struct { - name string - args args - isCreateFile bool - want bool + name string + fullPath string + wantDir string + wantFile string }{ { - name: "success - is exists", - args: args{ - direction: tempDir, - filename: "test.txt", - }, - isCreateFile: true, - want: true, + name: "simple path", + fullPath: "/home/user/file.txt", + wantDir: "/home/user", + wantFile: "file.txt", }, { - name: "fail - is not exists", - args: args{ - direction: tempDir, - filename: "nonexistent.txt", - }, - isCreateFile: false, - want: false, + name: "nested path", + fullPath: "/home/user/documents/file.txt", + wantDir: "/home/user/documents", + wantFile: "file.txt", }, { - name: "fail - is not exists empty file", - args: args{ - direction: tempDir, - filename: "", - }, - isCreateFile: false, - want: false, + name: "current dir file", + fullPath: "file.txt", + wantDir: ".", + wantFile: "file.txt", }, { - name: "fail - is not exists empty dir", - args: args{ - direction: "", - filename: "text.txt", - }, - isCreateFile: false, - want: false, + name: "empty path", + fullPath: "", + wantDir: ".", + wantFile: ".", }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if tt.isCreateFile { - require.NoError(t, Create(tt.args.direction, tt.args.filename)) - } - - require.Equal(t, Exists(tt.args.direction, tt.args.filename), tt.want) + gotDir, gotFile := GetDirectionAndFilename(tt.fullPath) + require.Equal(t, gotDir, tt.wantDir) + require.Equal(t, gotFile, tt.wantFile) }) } } -func TestGet(t *testing.T) { - tempDir := t.TempDir() - - testFiles := map[string][]byte{ - "test.txt": []byte("test data"), - "empty.txt": []byte(""), - "large.txt": make([]byte, 1024*1024), - "large-repeat.txt": bytes.Repeat([]byte("x"), 1024*1024), - } - - for filename, data := range testFiles { - filePath := filepath.Join(tempDir, filename) - require.NoError(t, os.WriteFile(filePath, data, 0644)) - } - +func TestGetFullPath(t *testing.T) { tests := []struct { name string direction string filename string want string - wantErr bool }{ { - name: "success - read test file", - direction: tempDir, - filename: "test.txt", - want: string(testFiles["test.txt"]), - wantErr: false, - }, - { - name: "success - read empty file", - direction: tempDir, - filename: "empty.txt", - want: string(testFiles["empty.txt"]), - wantErr: false, + name: "simple join", + direction: "/home/user", + filename: "file.txt", + want: "/home/user/file.txt", }, { - name: "success - read large file", - direction: tempDir, - filename: "large.txt", - want: string(testFiles["large.txt"]), - wantErr: false, + name: "empty dir", + direction: "", + filename: "file.txt", + want: "file.txt", }, { - name: "success - read large file", - direction: tempDir, - filename: "large-repeat.txt", - want: string(testFiles["large-repeat.txt"]), - wantErr: false, + name: "empty filename", + direction: "/home/user", + filename: "", + want: "/home/user", }, { - name: "fail - non-existent file", - direction: tempDir, - filename: "nonexistent.txt", + name: "both empty", + direction: "", + filename: "", want: "", - wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := Get(tt.direction, tt.filename) - require.Equal(t, tt.wantErr, err != nil) - require.Equal(t, got, tt.want) - }) - } -} - -func TestWrite(t *testing.T) { - type args struct { - direction string - filename string - data string - } - tests := []struct { - name string - args args - wantErr bool - want string - }{ - { - name: "write to new file", - args: args{ - direction: t.TempDir(), - filename: "test.txt", - data: "Hello, World!", - }, - wantErr: false, - want: "Hello, World!", - }, - { - name: "write to existing file (overwrite)", - args: args{ - direction: t.TempDir(), - filename: "test.txt", - data: "New content", - }, - wantErr: false, - want: "New content", - }, - { - name: "write empty data to new file", - args: args{ - direction: t.TempDir(), - filename: "empty.txt", - data: "", - }, - wantErr: false, - want: "", - }, - { - name: "write to invalid filename", - args: args{ - direction: t.TempDir(), - filename: "", - data: "Invalid", - }, - wantErr: true, - want: "", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := Write(tt.args.direction, tt.args.filename, tt.args.data) - require.Equal(t, tt.wantErr, err != nil) - - got, err := Get(tt.args.direction, tt.args.filename) - require.Equal(t, tt.wantErr, err != nil) - require.Equal(t, got, tt.want) + require.Equal(t, GetFullPath(tt.direction, tt.filename), tt.want) }) } } -func TestGetOpenFile(t *testing.T) { - type args struct { - direction string - filename string - } +func TestGetPrivateKeysDir(t *testing.T) { tests := []struct { - name string - args args - wantErr bool + name string + want func() string }{ { - name: "write to new file", - args: args{ - direction: t.TempDir(), - filename: "test.txt", - }, - wantErr: false, - }, - { - name: "error on invalid directory", - args: args{ - direction: "invalidDir" + t.TempDir(), - filename: "test.txt", + name: "success - get private keys dir", + want: func() string { + return filepath.Join(GetAppDir(), envconst.DirectionPrivateKeys) }, - wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - flags := os.O_WRONLY | os.O_APPEND | os.O_CREATE - got, err := GetOpenFile(tt.args.direction, tt.args.filename, flags) - require.Equal(t, tt.wantErr, err != nil) - - _, err = got.Write([]byte("test")) - require.Equal(t, tt.wantErr, err != nil) - - err = got.Close() - require.Equal(t, tt.wantErr, err != nil) - - fileIsExists := Exists(tt.args.direction, tt.args.filename) - require.Equal(t, fileIsExists, !tt.wantErr) + require.Equal(t, GetPrivateKeysDir(), tt.want()) }) } } diff --git a/internal/store/private_key.go b/internal/store/private_key.go index 804d3c6..2ed1434 100644 --- a/internal/store/private_key.go +++ b/internal/store/private_key.go @@ -1,15 +1,11 @@ package store import ( - "encoding/pem" "errors" - "reflect" - "strings" - "github.com/misha-ssh/kernel/internal/logger" "github.com/misha-ssh/kernel/internal/storage" "github.com/misha-ssh/kernel/pkg/connect" - "golang.org/x/crypto/ssh" + "reflect" ) var ( @@ -17,29 +13,9 @@ var ( ErrWriteToFilePrivateKey = errors.New("err write to file private key") ErrCreateFilePrivateKey = errors.New("err create file private key") - ErrNotValidPrivateKey = errors.New("private key is not valid") ErrGetDataPrivateKey = errors.New("private key get data error") ) -func validatePrivateKey(privateKey string, passphrase string) error { - block, _ := pem.Decode([]byte(privateKey)) - if block == nil { - return ErrNotValidPrivateKey - } - - _, err := ssh.ParseRawPrivateKey([]byte(privateKey)) - if err != nil { - if !strings.Contains(err.Error(), "passphrase") { - logger.Error(err.Error()) - return err - } - - _, err = ssh.ParsePrivateKeyWithPassphrase([]byte(privateKey), []byte(passphrase)) - } - - return err -} - // SavePrivateKey create private key for connection in spec dir func SavePrivateKey(connection *connect.Connect) (string, error) { direction, filename := storage.GetDirectionAndFilename(connection.SshOptions.PrivateKey) @@ -49,12 +25,6 @@ func SavePrivateKey(connection *connect.Connect) (string, error) { return "", ErrGetDataPrivateKey } - err = validatePrivateKey(dataPrivateKey, connection.SshOptions.Passphrase) - if err != nil { - logger.Error(err.Error()) - return "", err - } - filenamePrivateKey := connection.Alias err = storage.Create(DirectionKeys, filenamePrivateKey) @@ -107,12 +77,6 @@ func UpdatePrivateKey(connection *connect.Connect) (string, error) { return "", ErrGetDataPrivateKey } - err = validatePrivateKey(dataPrivateKey, connection.SshOptions.Passphrase) - if err != nil { - logger.Error(err.Error()) - return "", err - } - if !reflect.DeepEqual(existDataPrivateKey, dataPrivateKey) { err = DeletePrivateKey(connection) if err != nil { diff --git a/pkg/connect/connect.go b/pkg/connect/connect.go index e3f465f..bdaefb2 100644 --- a/pkg/connect/connect.go +++ b/pkg/connect/connect.go @@ -1,10 +1,5 @@ package connect -type ConnectionType string - -// TypeSSH type for ssh connection -const TypeSSH ConnectionType = "ssh" - type Connections struct { Connects []Connect `json:"connects"` } @@ -12,28 +7,23 @@ type Connections struct { // Connect represents a single connection configuration type Connect struct { // Alias is a user-defined name for the connection - Alias string `json:"alias"` - Login string `json:"login"` - Address string `json:"address"` - Password string `json:"password"` + Alias string `json:"alias"` + // Login is the username for authentication + Login string `json:"login"` + // Address is the hostname or IP address of the remote server + Address string `json:"address"` + // Password is the password for authentication + Password string `json:"password"` + + // CreatedAt is the timestamp when this connection was created CreatedAt string `json:"created_at"` + // UpdatedAt is the timestamp when this connection was last modified UpdatedAt string `json:"updated_at"` - // Type specifies the connection protocol (e.g., "ssh") - Type ConnectionType `json:"type"` - - // SshOptions contains SSH-specific configuration options - SshOptions *SshOptions `json:"ssh_options,omitempty"` -} - -// SshOptions contains configuration options specific to SSH connections -type SshOptions struct { - // Port specifies the SSH port (default is 22 if not specified) + // Port specifies the SSH port Port int `json:"port"` - // PrivateKey contains the PEM-encoded private key for authentication PrivateKey string `json:"private_key"` - - // Passphrase pass for private key + // Passphrase is the passphrase for decrypting the private key Passphrase string `json:"passphrase"` } diff --git a/pkg/connect/validate.go b/pkg/connect/validate.go index 88d41e1..f6efd03 100644 --- a/pkg/connect/validate.go +++ b/pkg/connect/validate.go @@ -1,8 +1,11 @@ package connect import ( + "encoding/pem" "errors" + "golang.org/x/crypto/ssh" "net" + "os" "regexp" "strings" "time" @@ -16,13 +19,14 @@ var ( func (c *Connect) Validate() error { for _, err := range []error{ - validateAlias(c.Alias), - validatePassword(c.Password, c.SshOptions.PrivateKey), - validateLogin(c.Login), - validateAddress(c.Address), - validateCreatedAt(c.CreatedAt), - validateUpdatedAt(c.UpdatedAt), - validatePort(c.SshOptions.Port), + c.validateAlias(), + c.validatePrivateKey(), + c.validatePassword(), + c.validateLogin(), + c.validateAddress(), + c.validateCreatedAt(), + c.validateUpdatedAt(), + c.validatePort(), } { if err != nil { return err @@ -31,85 +35,112 @@ func (c *Connect) Validate() error { return nil } -func validateAlias(alias string) error { - if strings.TrimSpace(alias) == "" { +func (c *Connect) validateAlias() error { + if strings.TrimSpace(c.Alias) == "" { return errors.New("alias is empty") } - if !aliasPattern.MatchString(alias) { + if !aliasPattern.MatchString(c.Alias) { return errors.New("alias special characters are not allowed") } return nil } -func validateLogin(login string) error { - if strings.TrimSpace(login) == "" { +func (c *Connect) validateLogin() error { + if strings.TrimSpace(c.Login) == "" { return errors.New("login cannot be empty") } - if len(login) > 50 { + if len(c.Login) > 50 { return errors.New("login too long (max 50 characters)") } - if !loginPattern.MatchString(login) { + if !loginPattern.MatchString(c.Login) { return errors.New("login contains invalid characters") } return nil } -func validateAddress(address string) error { - if strings.TrimSpace(address) == "" { +func (c *Connect) validateAddress() error { + if strings.TrimSpace(c.Address) == "" { return errors.New("address cannot be empty") } - if ip := net.ParseIP(address); ip != nil { + if ip := net.ParseIP(c.Address); ip != nil { return nil } - if !addressPattern.MatchString(address) { + if !addressPattern.MatchString(c.Address) { return errors.New("invalid address format") } - if len(address) > 253 { + if len(c.Address) > 253 { return errors.New("address too long") } return nil } -func validatePassword(password string, privateKey string) error { - if strings.TrimSpace(privateKey) != "" { +func (c *Connect) validatePassword() error { + if strings.TrimSpace(c.PrivateKey) != "" { return nil } - if strings.TrimSpace(password) == "" { + if strings.TrimSpace(c.Password) == "" { return errors.New("password cannot be empty") } - if len(password) < 4 { + if len(c.Password) < 4 { return errors.New("password too short (min 4 characters)") } - if len(password) > 100 { + if len(c.Password) > 100 { return errors.New("password too long (max 100 characters)") } return nil } -func validateCreatedAt(date string) error { - return validateDate(date) +func (c *Connect) validatePrivateKey() error { + if strings.TrimSpace(c.Password) != "" { + return nil + } + + data, err := os.ReadFile(c.PrivateKey) + if err != nil { + return errors.New("note found private key") + } + + block, _ := pem.Decode(data) + if block == nil { + return errors.New("private key is not valid") + } + + _, err = ssh.ParseRawPrivateKey(data) + if err != nil { + if !strings.Contains(err.Error(), "passphrase") { + return err + } + + _, err = ssh.ParsePrivateKeyWithPassphrase(data, []byte(c.Passphrase)) + } + + return err +} + +func (c *Connect) validateCreatedAt() error { + return validateDate(c.CreatedAt) } -func validateUpdatedAt(date string) error { - return validateDate(date) +func (c *Connect) validateUpdatedAt() error { + return validateDate(c.UpdatedAt) } func validateDate(date string) error { if strings.TrimSpace(date) == "" { - return errors.New("date cannot be empty") + return nil } parsedTime, err := time.Parse(time.RFC3339, date) @@ -124,8 +155,8 @@ func validateDate(date string) error { return nil } -func validatePort(port int) error { - if port < 1 || port > 65535 { +func (c *Connect) validatePort() error { + if c.Port < 1 || c.Port > 65535 { return errors.New("port must be between 1 and 65535") } diff --git a/pkg/kernel/connect.go b/pkg/kernel/connect.go index 8ed06bd..284b804 100644 --- a/pkg/kernel/connect.go +++ b/pkg/kernel/connect.go @@ -2,6 +2,7 @@ package kernel import ( "errors" + "github.com/misha-ssh/kernel/pkg/ssh" "github.com/misha-ssh/kernel/internal/logger" "github.com/misha-ssh/kernel/internal/setup" @@ -20,7 +21,7 @@ func Connect(connection *connect.Connect) error { switch connection.Type { case connect.TypeSSH: - ssh := &connect.Ssh{ + ssh := &ssh.Ssh{ Connection: connection, } diff --git a/pkg/kernel/create.go b/pkg/kernel/create.go index 91b6dbd..3f5cd81 100644 --- a/pkg/kernel/create.go +++ b/pkg/kernel/create.go @@ -12,7 +12,6 @@ import ( var ( ErrConnectionByAliasExistsAtCreate = errors.New("connection by alias exists") ErrDeletePrivateKeyAtCreate = errors.New("err delete private key") - ErrSavePrivateKeyAtCreate = errors.New("err save private key") ErrGetConnectionAtCreate = errors.New("err get connection") ErrSetConnectionAtCreate = errors.New("err set connection") ) diff --git a/pkg/kernel/download.go b/pkg/kernel/download.go index db553a2..b73868e 100644 --- a/pkg/kernel/download.go +++ b/pkg/kernel/download.go @@ -1,6 +1,7 @@ package kernel import ( + sftp2 "github.com/misha-ssh/kernel/pkg/sftp" "io" "os" @@ -14,7 +15,7 @@ import ( func Download(connection *connect.Connect, downloadRemoteFile string, downloadLocalFile string) error { setup.Init() - sp := connect.Sftp{ + sp := sftp2.Sftp{ Connection: connection, } diff --git a/pkg/kernel/upload.go b/pkg/kernel/upload.go index 8c719b4..ff02f9c 100644 --- a/pkg/kernel/upload.go +++ b/pkg/kernel/upload.go @@ -1,6 +1,7 @@ package kernel import ( + sftp2 "github.com/misha-ssh/kernel/pkg/sftp" "io" "os" @@ -14,7 +15,7 @@ import ( func Upload(connection *connect.Connect, uploadLocalFile string, uploadRemoteFile string) error { setup.Init() - sp := connect.Sftp{ + sp := sftp2.Sftp{ Connection: connection, } diff --git a/pkg/connect/sftp.go b/pkg/sftp/sftp.go similarity index 56% rename from pkg/connect/sftp.go rename to pkg/sftp/sftp.go index e933919..3ff7072 100644 --- a/pkg/connect/sftp.go +++ b/pkg/sftp/sftp.go @@ -1,13 +1,17 @@ -package connect +package sftp -import "github.com/pkg/sftp" +import ( + "github.com/misha-ssh/kernel/pkg/connect" + ssh2 "github.com/misha-ssh/kernel/pkg/ssh" + "github.com/pkg/sftp" +) type Sftp struct { - Connection *Connect + Connection *connect.Connect } func (s Sftp) Client(opts ...sftp.ClientOption) (*sftp.Client, error) { - ssh := &Ssh{ + ssh := &ssh2.Ssh{ Connection: s.Connection, } diff --git a/pkg/ssh/config.go b/pkg/ssh/config.go new file mode 100644 index 0000000..6e80ff7 --- /dev/null +++ b/pkg/ssh/config.go @@ -0,0 +1,47 @@ +package ssh + +import ( + "os" + + "github.com/misha-ssh/kernel/configs/envconst" + "github.com/misha-ssh/kernel/internal/storage" + "github.com/misha-ssh/kernel/pkg/connect" +) + +type Config struct { + LocalStorage *storage.Local +} + +func NewConfig() *Config { + return &Config{ + LocalStorage: &storage.Local{ + Path: storage.GetDirSSH(), + }, + } +} + +func (c *Config) GetConnections() (*connect.Connections, error) { + file, err := c.LocalStorage.GetOpenFile(envconst.FilenameConfigSSH, os.O_RDONLY) + if err != nil { + return nil, err + } + + return parseConnections(file) +} + +func (c *Config) SaveConnection(connection *connect.Connect) error { + file, err := c.LocalStorage.GetOpenFile(envconst.FilenameConfigSSH, os.O_RDWR) + if err != nil { + return err + } + + return addConnection(connection, file) +} + +func (c *Config) UpdateConnection(connection *connect.Connect) (*connect.Connect, error) { + return nil, nil +} + +func (c *Config) DeleteConnection(connection *connect.Connect) error { + return nil +} diff --git a/pkg/ssh/config_parse.go b/pkg/ssh/config_parse.go new file mode 100644 index 0000000..446fee0 --- /dev/null +++ b/pkg/ssh/config_parse.go @@ -0,0 +1,239 @@ +package ssh + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/misha-ssh/kernel/internal/logger" + "github.com/misha-ssh/kernel/pkg/connect" +) + +func parseAlias(connection *connect.Connect, values []string) error { + if strings.ToLower(values[0]) != "host" { + return nil + } + + if len(values) < 2 { + return fmt.Errorf("empty host") + } + + if strings.Contains(values[1], "*") || strings.Contains(values[1], "!") { + return nil + } + + connection.Alias = values[1] + return nil +} + +func parseAddress(connection *connect.Connect, values []string) error { + if strings.ToLower(values[0]) != "hostname" { + return nil + } + + if len(values) < 2 { + return fmt.Errorf("empty hostname") + } + + connection.Address = values[1] + return nil +} + +func parsePort(connection *connect.Connect, values []string) error { + if strings.ToLower(values[0]) != "port" { + return nil + } + + if len(values) < 2 { + return fmt.Errorf("empty port") + } + + port, err := strconv.Atoi(values[1]) + if err != nil { + return fmt.Errorf("invalid port: %q", values[1]) + } + + connection.Port = port + return nil +} + +func parseLogin(connection *connect.Connect, values []string) error { + if strings.ToLower(values[0]) != "user" { + return nil + } + + if len(values) < 2 { + return fmt.Errorf("empty user") + } + + connection.Login = values[1] + return nil +} + +func parsePrivateKey(connection *connect.Connect, values []string) error { + if strings.ToLower(values[0]) != "identityfile" { + return nil + } + + if len(values) < 2 { + return fmt.Errorf("empty private key") + } + + pathKey := values[1] + + if strings.HasPrefix(pathKey, "~/") { + home, err := os.UserHomeDir() + if err != nil { + return err + } + + pathKey = filepath.Join(home, pathKey[2:]) + } + + connection.PrivateKey = pathKey + return nil +} + +func isComment(line string) bool { + return strings.HasPrefix(line, "#") +} + +func isEmptyLine(line string) bool { + return line == "" +} + +func parseLine(connection *connect.Connect, line string) error { + values := strings.Fields(line) + if len(values) == 0 { + return nil + } + + parsers := []func(*connect.Connect, []string) error{ + parseAlias, + parseAddress, + parseLogin, + parsePort, + parsePrivateKey, + } + + for _, parser := range parsers { + if err := parser(connection, values); err != nil { + return err + } + } + + return nil +} + +func saveCurrentConnection(connection *connect.Connect, connections *connect.Connections) { + if connection != nil && !isConnectionEmpty(connection) { + if connection.Port == 0 { + connection.Port = 22 + } + + if err := connection.Validate(); err == nil { + connections.Connects = append(connections.Connects, *connection) + } + } + + *connection = connect.Connect{} +} + +func isConnectionEmpty(connection *connect.Connect) bool { + return connection.Alias == "" && + connection.Port == 0 && + connection.Login == "" && + connection.Address == "" && + connection.PrivateKey == "" +} + +func parseConnections(file *os.File) (*connect.Connections, error) { + defer func() { + err := file.Close() + if err != nil { + logger.Error(err.Error()) + } + }() + + s := bufio.NewScanner(file) + + connections := new(connect.Connections) + current := new(connect.Connect) + + for s.Scan() { + line := strings.TrimSpace(s.Text()) + + switch { + case isComment(line): + continue + case isEmptyLine(line): + saveCurrentConnection(current, connections) + default: + if err := parseLine(current, line); err != nil { + return nil, err + } + } + } + + saveCurrentConnection(current, connections) + + if err := s.Err(); err != nil { + return nil, err + } + + return connections, nil +} + +func prepareConnection(connection *connect.Connect) string { + var configConnection string + + configConnection += fmt.Sprintf("\n\nHost %v", connection.Alias) + configConnection += fmt.Sprintf("\n\tHostName %v", connection.Address) + configConnection += fmt.Sprintf("\n\tUser %v", connection.Login) + configConnection += fmt.Sprintf("\n\tPort %v", strconv.Itoa(connection.Port)) + configConnection += fmt.Sprintf("\n\tIdentityFile %v", connection.PrivateKey) + + return configConnection +} + +func addConnection(connection *connect.Connect, file *os.File) error { + defer func() { + err := file.Close() + if err != nil { + logger.Error(err.Error()) + } + }() + + if err := connection.Validate(); err != nil { + return err + } + + s := bufio.NewScanner(file) + + for s.Scan() { + line := strings.TrimSpace(s.Text()) + + switch { + case isComment(line): + continue + case isEmptyLine(line): + continue + default: + values := strings.Fields(line) + + if len(values) < 2 { + continue + } + + if strings.ToLower(values[0]) == "host" && values[1] == connection.Alias { + return fmt.Errorf("alias already in use") + } + } + } + + _, err := file.WriteString(prepareConnection(connection)) + return err +} diff --git a/pkg/ssh/config_test.go b/pkg/ssh/config_test.go new file mode 100644 index 0000000..c8e6d64 --- /dev/null +++ b/pkg/ssh/config_test.go @@ -0,0 +1,150 @@ +package ssh + +import ( + "testing" + + "github.com/misha-ssh/kernel/internal/storage" + "github.com/misha-ssh/kernel/pkg/connect" + "github.com/misha-ssh/kernel/testutil" + "github.com/stretchr/testify/require" +) + +func TestConfig_GetConnections(t *testing.T) { + tests := []struct { + name string + want *connect.Connections + filename string + wantErr bool + }{ + { + name: "success - get connections", + want: &connect.Connections{ + Connects: []connect.Connect{ + { + Alias: "test", + Address: "localhost", + Login: "user", + Port: 3333, + PrivateKey: "testdata/private_key", + }, + }, + }, + filename: "testdata/config", + wantErr: false, + }, + { + name: "success - get empty connections", + want: &connect.Connections{ + Connects: nil, + }, + filename: "testdata/empty_config", + wantErr: false, + }, + { + name: "err - note exists config", + want: nil, + filename: "", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir := t.TempDir() + + if tt.filename != "" { + err := testutil.CreateSSHConfig(tmpDir, tt.filename) + require.NoError(t, err) + } + + config := &Config{ + LocalStorage: &storage.Local{ + Path: tmpDir, + }, + } + + got, err := config.GetConnections() + if !tt.wantErr { + require.NoError(t, err) + } + + require.Equal(t, tt.want, got) + }) + } +} + +func TestConfig_SaveConnection(t *testing.T) { + tests := []struct { + name string + connection *connect.Connect + filename string + wantErr bool + }{ + { + name: "success - add connections", + connection: &connect.Connect{ + Alias: "new", + Address: "localhost", + Login: "user", + Port: 3333, + PrivateKey: "testdata/private_key", + }, + filename: "testdata/config", + wantErr: false, + }, + { + name: "success - empty file", + connection: &connect.Connect{ + Alias: "test", + Address: "localhost", + Login: "user", + Port: 3333, + PrivateKey: "testdata/private_key", + }, + filename: "testdata/empty_config", + wantErr: false, + }, + { + name: "fail - exists alias", + connection: &connect.Connect{ + Alias: "test", + Address: "localhost", + Login: "user", + Port: 3333, + PrivateKey: "testdata/private_key", + }, + filename: "testdata/config", + wantErr: true, + }, + { + name: "fail - invalid connection", + connection: &connect.Connect{ + Alias: "test", + }, + filename: "", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir := t.TempDir() + + if tt.filename != "" { + err := testutil.CreateSSHConfig(tmpDir, tt.filename) + require.NoError(t, err) + } + + config := &Config{ + LocalStorage: &storage.Local{ + Path: tmpDir, + }, + } + + err := config.SaveConnection(tt.connection) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/pkg/connect/private_key.go b/pkg/ssh/private_key.go similarity index 72% rename from pkg/connect/private_key.go rename to pkg/ssh/private_key.go index ad98de9..42e2a7a 100644 --- a/pkg/connect/private_key.go +++ b/pkg/ssh/private_key.go @@ -1,4 +1,4 @@ -package connect +package ssh import ( "strings" @@ -8,9 +8,10 @@ import ( "golang.org/x/crypto/ssh" ) -func parsePrivateKey(keyName string, passphrase string) (ssh.Signer, error) { - direction, filename := storage.GetDirectionAndFilename(keyName) - data, err := storage.Get(direction, filename) +func (s *SSH) parsePrivateKey() (ssh.Signer, error) { + currentStorage := storage.Get() + + data, err := currentStorage.Get(s.Connection.PrivateKey) if err != nil { logger.Error(err.Error()) return nil, err @@ -25,7 +26,7 @@ func parsePrivateKey(keyName string, passphrase string) (ssh.Signer, error) { return nil, err } - key, err = ssh.ParsePrivateKeyWithPassphrase(dataSshKey, []byte(passphrase)) + key, err = ssh.ParsePrivateKeyWithPassphrase(dataSshKey, []byte(s.Connection.Passphrase)) if err != nil { logger.Error(err.Error()) return nil, err diff --git a/pkg/connect/ssh.go b/pkg/ssh/ssh.go similarity index 81% rename from pkg/connect/ssh.go rename to pkg/ssh/ssh.go index f184c06..93d08f9 100644 --- a/pkg/connect/ssh.go +++ b/pkg/ssh/ssh.go @@ -1,7 +1,8 @@ -package connect +package ssh import ( "fmt" + "github.com/misha-ssh/kernel/pkg/connect" "net" "os" @@ -10,12 +11,18 @@ import ( "golang.org/x/term" ) -type Ssh struct { - Connection *Connect +type SSH struct { + Connection *connect.Connect +} + +func NewSSH(connection *connect.Connect) *SSH { + return &SSH{ + Connection: connection, + } } // Session establishes a new SSH session with the remote server -func (s *Ssh) Session() (*ssh.Session, error) { +func (s *SSH) Session() (*ssh.Session, error) { client, err := s.Client() if err != nil { return nil, err @@ -51,7 +58,7 @@ func (s *Ssh) Session() (*ssh.Session, error) { } // Connect starts an interactive shell session using the established SSH connection -func (s *Ssh) Connect(session *ssh.Session) error { +func (s *SSH) Connect(session *ssh.Session) error { defer func() { if err := session.Close(); err != nil { logger.Error(err.Error()) @@ -87,7 +94,7 @@ func (s *Ssh) Connect(session *ssh.Session) error { } // Client create ssh client from config and Auth -func (s *Ssh) Client() (*ssh.Client, error) { +func (s *SSH) Client() (*ssh.Client, error) { sshAuth, err := s.Auth() if err != nil { return nil, err @@ -102,25 +109,22 @@ func (s *Ssh) Client() (*ssh.Client, error) { hostWithPort := net.JoinHostPort( s.Connection.Address, - fmt.Sprint(s.Connection.SshOptions.Port), + fmt.Sprint(s.Connection.Port), ) return ssh.Dial("tcp", hostWithPort, config) } // Auth automate defines method auth from Connect -func (s *Ssh) Auth() ([]ssh.AuthMethod, error) { +func (s *SSH) Auth() ([]ssh.AuthMethod, error) { var authMethod []ssh.AuthMethod if len(s.Connection.Password) > 0 { authMethod = append(authMethod, ssh.Password(s.Connection.Password)) } - if len(s.Connection.SshOptions.PrivateKey) > 0 { - key, err := parsePrivateKey( - s.Connection.SshOptions.PrivateKey, - s.Connection.SshOptions.Passphrase, - ) + if len(s.Connection.PrivateKey) > 0 { + key, err := s.parsePrivateKey() if err != nil { return nil, err } diff --git a/pkg/connect/terminal.go b/pkg/ssh/terminal.go similarity index 98% rename from pkg/connect/terminal.go rename to pkg/ssh/terminal.go index 5607c2b..91bdcd9 100644 --- a/pkg/connect/terminal.go +++ b/pkg/ssh/terminal.go @@ -1,4 +1,4 @@ -package connect +package ssh import ( "os" diff --git a/pkg/ssh/testdata/config b/pkg/ssh/testdata/config new file mode 100644 index 0000000..3135b85 --- /dev/null +++ b/pkg/ssh/testdata/config @@ -0,0 +1,27 @@ +Host test + HostName localhost + User user + Port 3333 + IdentityFile testdata/private_key + +Host test2 + HostName localhost + User user2 + Port 3333 + IdentityFile testdata/invalid_private_key + +Host tyrell + HostName 192.168.10.20 + +Host martell + HostName 192.168.10.50 + +Host *ell + user oberyn + +Host * !martell + LogLevel INFO + +Host * + User root + Compression yes \ No newline at end of file diff --git a/pkg/ssh/testdata/empty_config b/pkg/ssh/testdata/empty_config new file mode 100644 index 0000000..e69de29 diff --git a/pkg/ssh/testdata/invalid_private_key b/pkg/ssh/testdata/invalid_private_key new file mode 100644 index 0000000..d2421ec --- /dev/null +++ b/pkg/ssh/testdata/invalid_private_key @@ -0,0 +1,2 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +-----END OPENSSH PRIVATE KEY----- \ No newline at end of file diff --git a/pkg/ssh/testdata/private_key b/pkg/ssh/testdata/private_key new file mode 100644 index 0000000..af08f13 --- /dev/null +++ b/pkg/ssh/testdata/private_key @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKAIBAAKCAgEAzFjjQQ3/XlJs8F0lDVMENT8wtgLk5M1987Rzv8PVIgwSrUvv +lwo2RY+DaCEz8C/SuJGXfXeKAStUlrgWs5FvJBfpZThn+XW+Vg20wb23d+KEm/5U +b2vBfK6LaV7ISmUBJxFniR2m8iPi7a12l8Dxslq1D/TlplK8pf/jgQo0hSbkOyjr +YhclXxCduFVzYaV0Cy+4Cxk14+xkT3/gh9JsAA8e1oa+0KxDLBj+fDF927EeYD3m +XlTwf7zQrBtzuMFlfGfwGaURKTQNCc91t4Y2WEFNwoMuRG7+PqxlHnPdEmoV+CV8 +prbPtqx9C/9gbNqSGKnkf5DKBoKMANCKsosR0gfY5FADL5K8Vy2MYvcO1gwMH1dp +LpRELxrzhgyOL8pdDEHFBAbztBpaJ1BZr2Nf730PswROB6i9/SeiX+P4oe18OY7e +CR8CjrhZrkIoc7ltpFHLHgbae+WWaOMHZiONA3CZkfbO+0TMq8GqShZzaMiIveji +4/1jVTAn4g69GvK5gU2kSjC3dTPh9CAIu6oHiKuPnAB3727iLTRHx1URosx73IMa +azx4PQaoPjc4wxyUkpdk/2ReUpNLOvsTHsK7AqPPe/uEl72xuvozDWM2yKh3dNrx +n3WVWAN1aw5ZwlqrS+VsWYa/knOi345sDoqFEIA26JwwolG63mP4oV6GGSkCAwEA +AQKCAgADnzs29NovCC+7YnFEz1ECpxo0TbCUMCLAgjUvg9d4JSXjGbaXUyRjXv/1 +pWoD4rsdz6HTZN4mt2eGTODFIcmqJnzZ8RIhuTEsmg1XRkc1Wife0ncZavvo23in +31jWPbxTnpK62tJR1ipAa3vPxIkcL9CoBd+Yrzx+Bj84cy97YTU4KbljWZTtXpBn +GyeihlHcXWYKF1It1iuwf4whqCyHIz15ELYa4YTGyDIhjiiEj3sB+nLl/uQs5XI+ +7LUkRBRKDFcUg0ketXgaMYnM/RVjQtQPo67bImsB/iEENUpIuGXnijWPiGu92YiV +YPtK7qRaiM9epfi4vRFhddDRiZbBHcm1H83C3ugGqaYytjMg+RKkd0k3U3wDducB +r7ruFaQW33h7v9yIUcESEvXzrV8iArTNU/bKNcEFvUKrgOXRSG2iAO2iEYUxdaYt +IdLLnObLshHaygPct+I82N2ga+PEMkD3IJsjD0xvYhZHHBKGk4zr7vGDvWhJrWmh +8j/53hhQ5qIo5x1lR3wAXYaKneaXN5cWqK08f1wZsOFEo1jmhhWTwi7B1qgU1Tgc +Ie6zPI3ZtMWuxdn16Rtp2t8MsksAkbae2Rl2e0wUdQwAWBdFFvFw2nk15nglMQwl +KKH0E3ljjqCheeeD0pRHSAmhWuiqe+7Y4oSySAP4BVKWSbRdYQKCAQEA7+AoDPCm +GsF924+4/JXlb9a6QjBZdAeVS8QKj+5yE0znKxd6zmOZ6mvrPbyoinUcsRHYowq2 +114UqeOd4qly6toAlUqo7RK6Mk759yzRqCST3utjqGwy7hKcORwD9cgZWaBT3sYo +HP28J1e+uyWQegktOYbpNvcb2JpJl3CWbiCgKbrhOs8uIP5NW/i7VkBtYGOxjmwR +fpiWeuHlk68GYjtDcbjP9IWO+KJ3TA7r3/aJmbgf1gx+WL/fZcB2UPWtCUQRwqsh ++XD2O2tC9z56fYaXPRu+Ab4R7iJMTi7hR5ovJdBwgXmR99e4Lut29/ck+7x4MBJe +bs2P7hohf1DGSQKCAQEA2hVZZCCTSzXQKdNfiisbfCUwNQ3xl/BcOzvN22rOzne/ +lwa95gz9qthDEBnQiOffzqZhvtD0adrFot8tpXMtFTgm1cnrt62OF2+MmMsbs7Lh +e24gDFxT4Gl+5s4z0ALEdIt2xWp/67pmhWDac4swMb0xWErxO7bjC9bLCrIda0uq +FW+WIhxPlbvSudXYLPHqzCouNZMrhOKh1IqZTqseAJzOkC3zO2dmxye4h4h1B1Ix +u/+r5Yn+97meWd5dVEdVnHx4eIawacz1F0XDLxeWUraCc+vkOQRHVeechuYaFiHh +w/Kbd2CsTgrRy6x4teSw5uNTVYnwGFBxPnJ2AC074QKCAQBSDallk8QeDuYQfv9W +V6geM6OPFJ7k09s8CZlbVsNq2rmQwf1eMC/sQnI7shctFZZ085fZXcbhsOr8mkHd +0PzgXSYp61oRjoBmySE0bf5ht/FlJbv3VtutGGycFHs+Te5t/Cv0XnBGSn1cL+Ws +etMLC6yOqxmHlcvOsihOR1MN5NckrypwRYKQAq1PsqvSe0Nu32tTPqBVX7jJ3A/+ +DrbuTzto4UExcaZQYrLQL6J8AAddr+AkBi4KCchPNCDE3OUN8Fzq7EM44m04Mh68 +GIEqAyok2yKJ0gysGstjSyIArjtGgiCaCY3m68GzOxR9Cet6uSObvgzTdjmvxvyC +Yo0RAoIBAQCj8+TuZ6cUpfJHX4e2Ik5ZeMPTPxZgOe29AmrzCEtN4a0B56mgaCfU +5x0T37RtGJWjkGZvxDvb0QNAPTTd68b66uoXU+SIhEwMxmoW/Kto35Sw7MvfPxI3 +5lfnQSKmwU3cqHS0Wiqtl8c3gub4cq3a1vdf/4d4czgiUGr5MYr4fTvzPZ7LKimS +0k/MMj6BG6Z/sz5mPKw9DPzJAyHaiL7XiwuoTUNNZ6FXHD+YdTg2Ns75HW+n86Th +rISl34yerbppGRKg2fGKuPGRe3sPzlXO/TL532AGlXbj2GpO6HK4LOTEIYJLrzwa +t/udeZ6OcM2l50VhS4BbZy6b2gVogJlBAoIBAD3ncarSkt6SW5Jj/XAyTdXuc4dl +s58KtaxIMeaes4KnArEPb3LpqIq03rXCFeCQb29FjbqB6fLXNheyXhWhT21+Je79 +Q4PgAA0QmW6M5XC3kE8gmuwHaN/RO3dhNqHn4Sri5+6rp7BYd8jDm8kNP/KVpCXH +D+BFJ8WcN++2lDN91Sp/1PDujsb9lm5ybTBPZRVOl9bkEMfawg86BudUXysNMAvE +o6xnLn+dfopf/Dzn1XdU9/tkjN2JlJIVDJmoH56kxhpsOY1qcJw0ghmad9Mk6Hd6 +WnP2xHffdHxv2SI/XgeqG/cQRyl/Hlg1/SeLspHN060BeyNAiercfSIVDSQ= +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/testutil/config.go b/testutil/config.go new file mode 100644 index 0000000..144b9fd --- /dev/null +++ b/testutil/config.go @@ -0,0 +1,20 @@ +package testutil + +import ( + "os" + "path/filepath" +) + +func CreateSSHConfig(tmpDir string, file string) error { + sourceData, err := os.ReadFile(file) + if err != nil { + return err + } + + destPath := filepath.Join(tmpDir, "config") + if err = os.WriteFile(destPath, sourceData, 0644); err != nil { + return err + } + + return nil +} diff --git a/testutil/private_key.go b/testutil/private_key.go index 65b7305..79ad709 100644 --- a/testutil/private_key.go +++ b/testutil/private_key.go @@ -9,8 +9,10 @@ import ( "path/filepath" ) -// GeneratePrivateKey generate private key for ssh connect -func GeneratePrivateKey(pass string) ([]byte, error) { +// todo delete method and usage file from testdata with pk + +// generatePrivateKey generate private key for ssh connect +func generatePrivateKey(pass string) ([]byte, error) { privateKey, err := rsa.GenerateKey(rand.Reader, 4096) if err != nil { return nil, err @@ -37,7 +39,7 @@ func GeneratePrivateKey(pass string) ([]byte, error) { // CreatePrivateKey generate and save private key in file func CreatePrivateKey(direction string) (string, error) { - privatePEM, err := GeneratePrivateKey("") + privatePEM, err := generatePrivateKey("") if err != nil { return "", err } @@ -53,7 +55,7 @@ func CreatePrivateKey(direction string) (string, error) { // CreatePrivateKeyWithPass generate and save private key with pass in file func CreatePrivateKeyWithPass(direction string, pass string) (string, error) { - privatePEM, err := GeneratePrivateKey(pass) + privatePEM, err := generatePrivateKey(pass) if err != nil { return "", err }