├── .gitattributes ├── .gitignore ├── docs └── build.md ├── pkg ├── utils │ ├── logger.go │ ├── tools.go │ └── file.go ├── control │ ├── get.go │ ├── control.go │ ├── create.go │ ├── alias.go │ ├── delete.go │ ├── prune.go │ ├── ssh.go │ └── scp.go ├── launcher │ ├── ssh.go │ ├── base.go │ └── scp.go └── config │ └── config.go ├── main.go ├── cmd ├── alias │ ├── alias.go │ ├── list.go │ ├── unset.go │ └── set.go ├── get │ ├── get.go │ ├── ports.go │ ├── keys.go │ ├── users.go │ ├── caches.go │ └── passwords.go ├── delete │ ├── delete.go │ ├── ports.go │ ├── caches.go │ ├── keys.go │ ├── users.go │ └── passwords.go ├── create │ ├── create.go │ ├── ports.go │ ├── keys.go │ ├── users.go │ ├── passwords.go │ └── caches.go ├── version │ └── version.go ├── cmd.go ├── prune │ └── prune.go ├── ssh │ └── ssh.go └── scp │ └── scp.go ├── .goreleaser.yml ├── go.mod ├── .github └── workflows │ └── goreleaser.yml ├── Makefile ├── README_zh.md ├── go.sum ├── LICENSE └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | release/ 3 | .idea 4 | .git 5 | tryssh -------------------------------------------------------------------------------- /docs/build.md: -------------------------------------------------------------------------------- 1 | # Build Project 2 | 3 | ## Build binary file for the current architecture 4 | 5 | ```bash 6 | cd ./tryssh 7 | 8 | make 9 | ``` 10 | 11 | ## Cross-compilation 12 | 13 | ```bash 14 | cd ./tryssh 15 | 16 | make multi 17 | ``` 18 | 19 | ## Clean binary packages 20 | 21 | ```bash 22 | cd ./tryssh 23 | 24 | make clean 25 | ``` -------------------------------------------------------------------------------- /pkg/utils/logger.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | "os" 6 | ) 7 | 8 | var Logger *logrus.Logger 9 | 10 | func init() { 11 | Logger = logrus.New() 12 | Logger.SetFormatter(&logrus.TextFormatter{ 13 | TimestampFormat: "2006-01-02 15:04:05", 14 | FullTimestamp: true, 15 | }) 16 | Logger.Out = os.Stdout 17 | Logger.SetLevel(logrus.InfoLevel) 18 | } 19 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/Driver-C/tryssh/cmd" 5 | "github.com/Driver-C/tryssh/pkg/utils" 6 | ) 7 | 8 | func main() { 9 | defer func() { 10 | if err := recover(); err != nil { 11 | utils.Logger.Errorln(err) 12 | } 13 | }() 14 | 15 | rootCmd := cmd.NewTrysshCommand() 16 | if err := rootCmd.Execute(); err != nil { 17 | utils.Logger.Errorln(err) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /cmd/alias/alias.go: -------------------------------------------------------------------------------- 1 | package alias 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | func NewAliasCommand() *cobra.Command { 8 | cmd := &cobra.Command{ 9 | Use: "alias [flags]", 10 | Short: "Set, unset, and list aliases, aliases can be used to log in to servers", 11 | Long: "Set, unset, and list aliases, aliases can be used to log in to servers", 12 | } 13 | cmd.AddCommand(NewAliasListCommand()) 14 | cmd.AddCommand(NewAliasSetCommand()) 15 | cmd.AddCommand(NewAliasUnsetCommand()) 16 | return cmd 17 | } 18 | -------------------------------------------------------------------------------- /cmd/get/get.go: -------------------------------------------------------------------------------- 1 | package get 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | func NewGetCommand() *cobra.Command { 8 | cmd := &cobra.Command{ 9 | Use: "get [command]", 10 | Short: "Get alternative username, port number, password, and login cache information", 11 | Long: "Get alternative username, port number, password, and login cache information", 12 | } 13 | cmd.AddCommand(NewUsersCommand()) 14 | cmd.AddCommand(NewPortsCommand()) 15 | cmd.AddCommand(NewPasswordsCommand()) 16 | cmd.AddCommand(NewCachesCommand()) 17 | cmd.AddCommand(NewKeysCommand()) 18 | return cmd 19 | } 20 | -------------------------------------------------------------------------------- /cmd/alias/list.go: -------------------------------------------------------------------------------- 1 | package alias 2 | 3 | import ( 4 | "github.com/Driver-C/tryssh/pkg/config" 5 | "github.com/Driver-C/tryssh/pkg/control" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func NewAliasListCommand() *cobra.Command { 10 | cmd := &cobra.Command{ 11 | Use: "list", 12 | Short: "List all alias", 13 | Long: "List all alias", 14 | Aliases: []string{"ls"}, 15 | Run: func(cmd *cobra.Command, args []string) { 16 | configuration := config.LoadConfig() 17 | controller := control.NewAliasController("", configuration, "") 18 | controller.ListAlias() 19 | }, 20 | } 21 | return cmd 22 | } 23 | -------------------------------------------------------------------------------- /cmd/delete/delete.go: -------------------------------------------------------------------------------- 1 | package delete 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | func NewDeleteCommand() *cobra.Command { 8 | cmd := &cobra.Command{ 9 | Use: "delete [command]", 10 | Short: "Delete alternative username, port number, password, and login cache information", 11 | Long: "Delete alternative username, port number, password, and login cache information", 12 | Aliases: []string{"del"}, 13 | } 14 | cmd.AddCommand(NewUsersCommand()) 15 | cmd.AddCommand(NewPortsCommand()) 16 | cmd.AddCommand(NewPasswordsCommand()) 17 | cmd.AddCommand(NewCachesCommand()) 18 | cmd.AddCommand(NewKeysCommand()) 19 | return cmd 20 | } 21 | -------------------------------------------------------------------------------- /cmd/alias/unset.go: -------------------------------------------------------------------------------- 1 | package alias 2 | 3 | import ( 4 | "github.com/Driver-C/tryssh/pkg/config" 5 | "github.com/Driver-C/tryssh/pkg/control" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func NewAliasUnsetCommand() *cobra.Command { 10 | cmd := &cobra.Command{ 11 | Use: "unset ", 12 | Args: cobra.ExactArgs(1), 13 | Short: "Unset the alias", 14 | Long: "Unset the alias", 15 | Run: func(cmd *cobra.Command, args []string) { 16 | aliasContent := args[0] 17 | configuration := config.LoadConfig() 18 | controller := control.NewAliasController("", configuration, aliasContent) 19 | controller.UnsetAlias() 20 | }, 21 | } 22 | return cmd 23 | } 24 | -------------------------------------------------------------------------------- /cmd/create/create.go: -------------------------------------------------------------------------------- 1 | package create 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | func NewCreateCommand() *cobra.Command { 8 | cmd := &cobra.Command{ 9 | Use: "create [command]", 10 | Short: "Create alternative username, port number, password, and login cache information", 11 | Long: "Create alternative username, port number, password, and login cache information", 12 | Aliases: []string{"cre", "crt", "add"}, 13 | } 14 | cmd.AddCommand(NewUsersCommand()) 15 | cmd.AddCommand(NewPortsCommand()) 16 | cmd.AddCommand(NewPasswordsCommand()) 17 | cmd.AddCommand(NewCachesCommand()) 18 | cmd.AddCommand(NewKeysCommand()) 19 | return cmd 20 | } 21 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | builds: 3 | - skip: true 4 | checksum: 5 | name_template: '{{ .ProjectName }}-sha256-checksums.txt' 6 | algorithm: sha256 7 | extra_files: 8 | - glob: ./release/* 9 | release: 10 | # Same as for github 11 | # Note: it can only be one: either github, gitlab or gitea 12 | github: 13 | owner: Driver-C 14 | name: tryssh 15 | 16 | draft: false 17 | 18 | # You can add extra pre-existing files to the release. 19 | # The filename on the release will be the last part of the path (base). If 20 | # another file with the same name exists, the latest one found will be used. 21 | # Defaults to empty. 22 | extra_files: 23 | - glob: ./release/* 24 | -------------------------------------------------------------------------------- /cmd/create/ports.go: -------------------------------------------------------------------------------- 1 | package create 2 | 3 | import ( 4 | "github.com/Driver-C/tryssh/pkg/config" 5 | "github.com/Driver-C/tryssh/pkg/control" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func NewPortsCommand() *cobra.Command { 10 | cmd := &cobra.Command{ 11 | Use: "ports ", 12 | Args: cobra.ExactArgs(1), 13 | Short: "Create an alternative port", 14 | Long: "Create an alternative port", 15 | Aliases: []string{"port", "po"}, 16 | Run: func(cmd *cobra.Command, args []string) { 17 | port := args[0] 18 | configuration := config.LoadConfig() 19 | controller := control.NewCreateController(control.TypePorts, port, configuration) 20 | controller.ExecuteCreate() 21 | }, 22 | } 23 | return cmd 24 | } 25 | -------------------------------------------------------------------------------- /cmd/delete/ports.go: -------------------------------------------------------------------------------- 1 | package delete 2 | 3 | import ( 4 | "github.com/Driver-C/tryssh/pkg/config" 5 | "github.com/Driver-C/tryssh/pkg/control" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func NewPortsCommand() *cobra.Command { 10 | cmd := &cobra.Command{ 11 | Use: "ports ", 12 | Args: cobra.ExactArgs(1), 13 | Short: "Delete an alternative port", 14 | Long: "Delete an alternative port", 15 | Aliases: []string{"port", "po"}, 16 | Run: func(cmd *cobra.Command, args []string) { 17 | port := args[0] 18 | configuration := config.LoadConfig() 19 | controller := control.NewDeleteController(control.TypePorts, port, configuration) 20 | controller.ExecuteDelete() 21 | }, 22 | } 23 | return cmd 24 | } 25 | -------------------------------------------------------------------------------- /cmd/get/ports.go: -------------------------------------------------------------------------------- 1 | package get 2 | 3 | import ( 4 | "github.com/Driver-C/tryssh/pkg/config" 5 | "github.com/Driver-C/tryssh/pkg/control" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func NewPortsCommand() *cobra.Command { 10 | cmd := &cobra.Command{ 11 | Use: "ports ", 12 | Short: "Get alternative ports", 13 | Long: "Get alternative ports", 14 | Aliases: []string{"port", "po"}, 15 | Run: func(cmd *cobra.Command, args []string) { 16 | var port string 17 | if len(args) > 0 { 18 | port = args[0] 19 | } 20 | configuration := config.LoadConfig() 21 | controller := control.NewGetController(control.TypePorts, port, configuration) 22 | controller.ExecuteGet() 23 | }, 24 | } 25 | return cmd 26 | } 27 | -------------------------------------------------------------------------------- /cmd/delete/caches.go: -------------------------------------------------------------------------------- 1 | package delete 2 | 3 | import ( 4 | "github.com/Driver-C/tryssh/pkg/config" 5 | "github.com/Driver-C/tryssh/pkg/control" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func NewCachesCommand() *cobra.Command { 10 | cmd := &cobra.Command{ 11 | Use: "caches ", 12 | Args: cobra.ExactArgs(1), 13 | Short: "Delete an alternative cache", 14 | Long: "Delete an alternative cache", 15 | Aliases: []string{"cache"}, 16 | Run: func(cmd *cobra.Command, args []string) { 17 | ipAddress := args[0] 18 | configuration := config.LoadConfig() 19 | controller := control.NewDeleteController(control.TypeCaches, ipAddress, configuration) 20 | controller.ExecuteDelete() 21 | }, 22 | } 23 | return cmd 24 | } 25 | -------------------------------------------------------------------------------- /cmd/create/keys.go: -------------------------------------------------------------------------------- 1 | package create 2 | 3 | import ( 4 | "github.com/Driver-C/tryssh/pkg/config" 5 | "github.com/Driver-C/tryssh/pkg/control" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func NewKeysCommand() *cobra.Command { 10 | cmd := &cobra.Command{ 11 | Use: "keys ", 12 | Args: cobra.ExactArgs(1), 13 | Short: "Create an alternative key file path", 14 | Long: "Create an alternative key file path", 15 | Aliases: []string{"key"}, 16 | Run: func(cmd *cobra.Command, args []string) { 17 | keyPath := args[0] 18 | configuration := config.LoadConfig() 19 | controller := control.NewCreateController(control.TypeKeys, keyPath, configuration) 20 | controller.ExecuteCreate() 21 | }, 22 | } 23 | return cmd 24 | } 25 | -------------------------------------------------------------------------------- /cmd/create/users.go: -------------------------------------------------------------------------------- 1 | package create 2 | 3 | import ( 4 | "github.com/Driver-C/tryssh/pkg/config" 5 | "github.com/Driver-C/tryssh/pkg/control" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func NewUsersCommand() *cobra.Command { 10 | cmd := &cobra.Command{ 11 | Use: "users ", 12 | Args: cobra.ExactArgs(1), 13 | Short: "Create an alternative username", 14 | Long: "Create an alternative username", 15 | Aliases: []string{"user", "usr"}, 16 | Run: func(cmd *cobra.Command, args []string) { 17 | username := args[0] 18 | configuration := config.LoadConfig() 19 | controller := control.NewCreateController(control.TypeUsers, username, configuration) 20 | controller.ExecuteCreate() 21 | }, 22 | } 23 | return cmd 24 | } 25 | -------------------------------------------------------------------------------- /cmd/delete/keys.go: -------------------------------------------------------------------------------- 1 | package delete 2 | 3 | import ( 4 | "github.com/Driver-C/tryssh/pkg/config" 5 | "github.com/Driver-C/tryssh/pkg/control" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func NewKeysCommand() *cobra.Command { 10 | cmd := &cobra.Command{ 11 | Use: "keys ", 12 | Args: cobra.ExactArgs(1), 13 | Short: "Delete an alternative key file path", 14 | Long: "Delete an alternative key file path", 15 | Aliases: []string{"key"}, 16 | Run: func(cmd *cobra.Command, args []string) { 17 | keyPath := args[0] 18 | configuration := config.LoadConfig() 19 | controller := control.NewDeleteController(control.TypeKeys, keyPath, configuration) 20 | controller.ExecuteDelete() 21 | }, 22 | } 23 | return cmd 24 | } 25 | -------------------------------------------------------------------------------- /cmd/delete/users.go: -------------------------------------------------------------------------------- 1 | package delete 2 | 3 | import ( 4 | "github.com/Driver-C/tryssh/pkg/config" 5 | "github.com/Driver-C/tryssh/pkg/control" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func NewUsersCommand() *cobra.Command { 10 | cmd := &cobra.Command{ 11 | Use: "users ", 12 | Args: cobra.ExactArgs(1), 13 | Short: "Delete an alternative username", 14 | Long: "Delete an alternative username", 15 | Aliases: []string{"user", "usr"}, 16 | Run: func(cmd *cobra.Command, args []string) { 17 | username := args[0] 18 | configuration := config.LoadConfig() 19 | controller := control.NewDeleteController(control.TypeUsers, username, configuration) 20 | controller.ExecuteDelete() 21 | }, 22 | } 23 | return cmd 24 | } 25 | -------------------------------------------------------------------------------- /cmd/get/keys.go: -------------------------------------------------------------------------------- 1 | package get 2 | 3 | import ( 4 | "github.com/Driver-C/tryssh/pkg/config" 5 | "github.com/Driver-C/tryssh/pkg/control" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func NewKeysCommand() *cobra.Command { 10 | cmd := &cobra.Command{ 11 | Use: "keys ", 12 | Short: "Get alternative key file path", 13 | Long: "Get alternative key file path", 14 | Aliases: []string{"key"}, 15 | Run: func(cmd *cobra.Command, args []string) { 16 | var keyPath string 17 | if len(args) > 0 { 18 | keyPath = args[0] 19 | } 20 | configuration := config.LoadConfig() 21 | controller := control.NewGetController(control.TypeKeys, keyPath, configuration) 22 | controller.ExecuteGet() 23 | }, 24 | } 25 | return cmd 26 | } 27 | -------------------------------------------------------------------------------- /cmd/get/users.go: -------------------------------------------------------------------------------- 1 | package get 2 | 3 | import ( 4 | "github.com/Driver-C/tryssh/pkg/config" 5 | "github.com/Driver-C/tryssh/pkg/control" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func NewUsersCommand() *cobra.Command { 10 | cmd := &cobra.Command{ 11 | Use: "users ", 12 | Short: "Get alternative usernames", 13 | Long: "Get alternative usernames", 14 | Aliases: []string{"user", "usr"}, 15 | Run: func(cmd *cobra.Command, args []string) { 16 | var username string 17 | if len(args) > 0 { 18 | username = args[0] 19 | } 20 | configuration := config.LoadConfig() 21 | controller := control.NewGetController(control.TypeUsers, username, configuration) 22 | controller.ExecuteGet() 23 | }, 24 | } 25 | return cmd 26 | } 27 | -------------------------------------------------------------------------------- /cmd/create/passwords.go: -------------------------------------------------------------------------------- 1 | package create 2 | 3 | import ( 4 | "github.com/Driver-C/tryssh/pkg/config" 5 | "github.com/Driver-C/tryssh/pkg/control" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func NewPasswordsCommand() *cobra.Command { 10 | cmd := &cobra.Command{ 11 | Use: "passwords ", 12 | Args: cobra.ExactArgs(1), 13 | Short: "Create an alternative password", 14 | Long: "Create an alternative password", 15 | Aliases: []string{"password", "pass", "pwd"}, 16 | Run: func(cmd *cobra.Command, args []string) { 17 | password := args[0] 18 | configuration := config.LoadConfig() 19 | controller := control.NewCreateController(control.TypePasswords, password, configuration) 20 | controller.ExecuteCreate() 21 | }, 22 | } 23 | return cmd 24 | } 25 | -------------------------------------------------------------------------------- /cmd/delete/passwords.go: -------------------------------------------------------------------------------- 1 | package delete 2 | 3 | import ( 4 | "github.com/Driver-C/tryssh/pkg/config" 5 | "github.com/Driver-C/tryssh/pkg/control" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func NewPasswordsCommand() *cobra.Command { 10 | cmd := &cobra.Command{ 11 | Use: "passwords ", 12 | Args: cobra.ExactArgs(1), 13 | Short: "Delete an alternative password", 14 | Long: "Delete an alternative password", 15 | Aliases: []string{"password", "pass", "pwd"}, 16 | Run: func(cmd *cobra.Command, args []string) { 17 | password := args[0] 18 | configuration := config.LoadConfig() 19 | controller := control.NewDeleteController(control.TypePasswords, password, configuration) 20 | controller.ExecuteDelete() 21 | }, 22 | } 23 | return cmd 24 | } 25 | -------------------------------------------------------------------------------- /cmd/get/caches.go: -------------------------------------------------------------------------------- 1 | package get 2 | 3 | import ( 4 | "github.com/Driver-C/tryssh/pkg/config" 5 | "github.com/Driver-C/tryssh/pkg/control" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func NewCachesCommand() *cobra.Command { 10 | cmd := &cobra.Command{ 11 | Use: "caches ", 12 | Short: "Get alternative caches by ipAddress", 13 | Long: "Get alternative caches by ipAddress", 14 | Aliases: []string{"cache"}, 15 | Run: func(cmd *cobra.Command, args []string) { 16 | var ipAddress string 17 | if len(args) > 0 { 18 | ipAddress = args[0] 19 | } 20 | configuration := config.LoadConfig() 21 | controller := control.NewGetController(control.TypeCaches, ipAddress, configuration) 22 | controller.ExecuteGet() 23 | }, 24 | } 25 | return cmd 26 | } 27 | -------------------------------------------------------------------------------- /cmd/get/passwords.go: -------------------------------------------------------------------------------- 1 | package get 2 | 3 | import ( 4 | "github.com/Driver-C/tryssh/pkg/config" 5 | "github.com/Driver-C/tryssh/pkg/control" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func NewPasswordsCommand() *cobra.Command { 10 | cmd := &cobra.Command{ 11 | Use: "passwords ", 12 | Short: "Get alternative passwords", 13 | Long: "Get alternative passwords", 14 | Aliases: []string{"password", "pass", "pwd"}, 15 | Run: func(cmd *cobra.Command, args []string) { 16 | var password string 17 | if len(args) > 0 { 18 | password = args[0] 19 | } 20 | configuration := config.LoadConfig() 21 | controller := control.NewGetController(control.TypePasswords, password, configuration) 22 | controller.ExecuteGet() 23 | }, 24 | } 25 | return cmd 26 | } 27 | -------------------------------------------------------------------------------- /pkg/utils/tools.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | func InterfaceSlice(slice interface{}) []interface{} { 8 | s := reflect.ValueOf(slice) 9 | if s.Kind() != reflect.Slice { 10 | panic("InterfaceSlice() given a non-slice type") 11 | } 12 | 13 | // Keep the distinction between nil and empty slice input 14 | if s.IsNil() { 15 | return nil 16 | } 17 | 18 | ret := make([]interface{}, s.Len()) 19 | 20 | for i := 0; i < s.Len(); i++ { 21 | ret[i] = s.Index(i).Interface() 22 | } 23 | 24 | return ret 25 | } 26 | 27 | func RemoveDuplicate(s []string) []string { 28 | result := make([]string, 0, len(s)) 29 | temp := map[string]bool{} 30 | 31 | for _, v := range s { 32 | if !temp[v] { 33 | temp[v] = true 34 | result = append(result, v) 35 | } 36 | } 37 | return result 38 | } 39 | -------------------------------------------------------------------------------- /cmd/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "fmt" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | var ( 9 | Version string 10 | BuildGoVersion string 11 | BuildTime string 12 | ) 13 | 14 | func NewVersionCommand() *cobra.Command { 15 | cmd := &cobra.Command{ 16 | Use: "version", 17 | Short: "Print the client version information for the current context", 18 | Long: "Print the client version information for the current context", 19 | Run: func(cmd *cobra.Command, args []string) { 20 | var versionContent string 21 | if Version != "" { 22 | versionContent += fmt.Sprintf("Version: %s\n", Version) 23 | } 24 | if BuildGoVersion != "" { 25 | versionContent += fmt.Sprintf("GoVersion: %s\n", BuildGoVersion) 26 | } 27 | if BuildTime != "" { 28 | versionContent += fmt.Sprintf("BuildTime: %s\n", BuildTime) 29 | } 30 | fmt.Printf(versionContent) 31 | }, 32 | } 33 | return cmd 34 | } 35 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Driver-C/tryssh 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24 6 | 7 | require ( 8 | github.com/cheggaaa/pb/v3 v3.1.7 9 | github.com/pkg/sftp v1.13.9 10 | github.com/schwarmco/go-cartesian-product v0.0.0-20230921023625-e02d1c150053 11 | github.com/sirupsen/logrus v1.9.3 12 | github.com/spf13/cobra v1.9.1 13 | golang.org/x/crypto v0.38.0 14 | gopkg.in/yaml.v3 v3.0.1 15 | ) 16 | 17 | require ( 18 | github.com/VividCortex/ewma v1.2.0 // indirect 19 | github.com/fatih/color v1.18.0 // indirect 20 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 21 | github.com/kr/fs v0.1.0 // indirect 22 | github.com/mattn/go-colorable v0.1.14 // indirect 23 | github.com/mattn/go-isatty v0.0.20 // indirect 24 | github.com/mattn/go-runewidth v0.0.16 // indirect 25 | github.com/rivo/uniseg v0.4.7 // indirect 26 | github.com/spf13/pflag v1.0.6 // indirect 27 | golang.org/x/sys v0.33.0 // indirect 28 | golang.org/x/term v0.32.0 // indirect 29 | ) 30 | -------------------------------------------------------------------------------- /cmd/alias/set.go: -------------------------------------------------------------------------------- 1 | package alias 2 | 3 | import ( 4 | "github.com/Driver-C/tryssh/pkg/config" 5 | "github.com/Driver-C/tryssh/pkg/control" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func NewAliasSetCommand() *cobra.Command { 10 | cmd := &cobra.Command{ 11 | Use: "set [flags]", 12 | Args: cobra.ExactArgs(1), 13 | Short: "Set an alias for the specified server address", 14 | Long: "Set an alias for the specified server address", 15 | Run: func(cmd *cobra.Command, args []string) { 16 | aliasContent := args[0] 17 | targetAddress, _ := cmd.Flags().GetString("target") 18 | configuration := config.LoadConfig() 19 | controller := control.NewAliasController(targetAddress, configuration, aliasContent) 20 | controller.SetAlias() 21 | }, 22 | } 23 | cmd.Flags().StringP( 24 | "target", "t", "", "Set the alias for the target server address") 25 | err := cmd.MarkFlagRequired("target") 26 | if err != nil { 27 | return nil 28 | } 29 | return cmd 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/goreleaser.yml: -------------------------------------------------------------------------------- 1 | name: Build and Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | goreleaser: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | 17 | - name: Set up Go 18 | uses: actions/setup-go@v5 19 | with: 20 | go-version: '1.24' 21 | 22 | - name: Make All 23 | run: make multi VERSION="${{ github.ref_name }}" 24 | 25 | - name: Publishing 26 | run: | 27 | GOPROXY=proxy.golang.org go list -m github.com/Driver-C/tryssh@${{ github.ref_name }} 28 | GOPROXY=proxy.golang.org go list -m github.com/Driver-C/tryssh@latest 29 | 30 | - name: Run GoReleaser 31 | uses: goreleaser/goreleaser-action@v6 32 | with: 33 | version: latest 34 | args: release --clean 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | -------------------------------------------------------------------------------- /cmd/cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/Driver-C/tryssh/cmd/alias" 5 | "github.com/Driver-C/tryssh/cmd/create" 6 | "github.com/Driver-C/tryssh/cmd/delete" 7 | "github.com/Driver-C/tryssh/cmd/get" 8 | "github.com/Driver-C/tryssh/cmd/prune" 9 | "github.com/Driver-C/tryssh/cmd/scp" 10 | "github.com/Driver-C/tryssh/cmd/ssh" 11 | "github.com/Driver-C/tryssh/cmd/version" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | func NewTrysshCommand() *cobra.Command { 16 | rootCmd := &cobra.Command{ 17 | Use: "tryssh [command]", 18 | Short: "A command line ssh terminal tool.", 19 | Long: "A command line ssh terminal tool.", 20 | } 21 | rootCmd.AddCommand(version.NewVersionCommand()) 22 | rootCmd.AddCommand(ssh.NewSshCommand()) 23 | rootCmd.AddCommand(scp.NewScpCommand()) 24 | rootCmd.AddCommand(alias.NewAliasCommand()) 25 | rootCmd.AddCommand(create.NewCreateCommand()) 26 | rootCmd.AddCommand(delete.NewDeleteCommand()) 27 | rootCmd.AddCommand(get.NewGetCommand()) 28 | rootCmd.AddCommand(prune.NewPruneCommand()) 29 | rootCmd.CompletionOptions.DisableDefaultCmd = true 30 | return rootCmd 31 | } 32 | -------------------------------------------------------------------------------- /cmd/prune/prune.go: -------------------------------------------------------------------------------- 1 | package prune 2 | 3 | import ( 4 | "github.com/Driver-C/tryssh/pkg/config" 5 | "github.com/Driver-C/tryssh/pkg/control" 6 | "github.com/spf13/cobra" 7 | "time" 8 | ) 9 | 10 | const ( 11 | concurrency = 8 12 | sshTimeout = 2 * time.Second 13 | ) 14 | 15 | func NewPruneCommand() *cobra.Command { 16 | cmd := &cobra.Command{ 17 | Use: "prune", 18 | Short: "Check if all current caches are available and clear the ones that are not available", 19 | Long: "Check if all current caches are available and clear the ones that are not available", 20 | Run: func(cmd *cobra.Command, args []string) { 21 | auto, _ := cmd.Flags().GetBool("auto") 22 | concurrencyOpt, _ := cmd.Flags().GetInt("concurrency") 23 | timeout, _ := cmd.Flags().GetDuration("timeout") 24 | configuration := config.LoadConfig() 25 | controller := control.NewPruneController(configuration, auto, timeout, concurrencyOpt) 26 | controller.PruneCaches() 27 | }, 28 | } 29 | cmd.Flags().BoolP( 30 | "auto", "a", false, "Automatically perform concurrent cache optimization without"+ 31 | " asking for confirmation to delete") 32 | cmd.Flags().IntP( 33 | "concurrency", "c", concurrency, "Number of multiple requests to perform at a time") 34 | cmd.Flags().DurationP("timeout", "t", sshTimeout, 35 | "SSH timeout when attempting to log in. It can be \"1s\" or \"1m\" or other duration") 36 | return cmd 37 | } 38 | -------------------------------------------------------------------------------- /cmd/ssh/ssh.go: -------------------------------------------------------------------------------- 1 | package ssh 2 | 3 | import ( 4 | "github.com/Driver-C/tryssh/pkg/config" 5 | "github.com/Driver-C/tryssh/pkg/control" 6 | "github.com/spf13/cobra" 7 | "time" 8 | ) 9 | 10 | const ( 11 | concurrency = 8 12 | sshTimeout = 1 * time.Second 13 | ) 14 | 15 | func NewSshCommand() *cobra.Command { 16 | cmd := &cobra.Command{ 17 | Use: "ssh ", 18 | Args: cobra.ExactArgs(1), 19 | Short: "Connect to the server through SSH protocol", 20 | Long: "Connect to the server through SSH protocol", 21 | Run: func(cmd *cobra.Command, args []string) { 22 | user, _ := cmd.Flags().GetString("user") 23 | concurrencyOpt, _ := cmd.Flags().GetInt("concurrency") 24 | timeout, _ := cmd.Flags().GetDuration("timeout") 25 | targetIp := args[0] 26 | configuration := config.LoadConfig() 27 | controller := control.NewSshController(targetIp, configuration) 28 | controller.TryLogin(user, concurrencyOpt, timeout) 29 | }, 30 | } 31 | cmd.Flags().StringP( 32 | "user", "u", "", "Specify a username to attempt to login to the server,\n"+ 33 | "if the specified username does not exist, try logging in using that username") 34 | cmd.Flags().IntP( 35 | "concurrency", "c", concurrency, "Number of multiple requests to perform at a time") 36 | cmd.Flags().DurationP("timeout", "t", sshTimeout, 37 | "SSH timeout when attempting to log in. It can be \"1s\" or \"1m\" or other duration") 38 | return cmd 39 | } 40 | -------------------------------------------------------------------------------- /pkg/control/get.go: -------------------------------------------------------------------------------- 1 | package control 2 | 3 | import ( 4 | "fmt" 5 | "github.com/Driver-C/tryssh/pkg/config" 6 | ) 7 | 8 | type GetController struct { 9 | getType string 10 | getContent string 11 | configuration *config.MainConfig 12 | } 13 | 14 | func (gc GetController) ExecuteGet() { 15 | switch gc.getType { 16 | case TypeUsers: 17 | fmt.Println("INDEX USER") 18 | gc.searchAndPrint(gc.configuration.Main.Users) 19 | case TypePorts: 20 | fmt.Println("INDEX PORT") 21 | gc.searchAndPrint(gc.configuration.Main.Ports) 22 | case TypePasswords: 23 | fmt.Println("INDEX PASSWORD") 24 | gc.searchAndPrint(gc.configuration.Main.Passwords) 25 | case TypeKeys: 26 | fmt.Println("INDEX KEY") 27 | gc.searchAndPrint(gc.configuration.Main.Keys) 28 | case TypeCaches: 29 | // gc.getContent is ipAddress 30 | fmt.Println("INDEX CACHE") 31 | if gc.getContent != "" { 32 | for index, server := range gc.configuration.ServerLists { 33 | if server.Ip == gc.getContent { 34 | fmt.Printf("%d %s\n", index, server) 35 | break 36 | } 37 | } 38 | } else { 39 | for index, server := range gc.configuration.ServerLists { 40 | fmt.Printf("%d %s\n", index, server) 41 | } 42 | } 43 | } 44 | } 45 | 46 | func (gc GetController) searchAndPrint(contents []string) { 47 | if gc.getContent != "" { 48 | for index, content := range contents { 49 | if content == gc.getContent { 50 | fmt.Printf("%d %s\n", index, content) 51 | break 52 | } 53 | } 54 | } else { 55 | for index, content := range contents { 56 | fmt.Printf("%d %s\n", index, content) 57 | } 58 | } 59 | } 60 | 61 | func NewGetController(getType string, getContent string, 62 | configuration *config.MainConfig) *GetController { 63 | return &GetController{ 64 | getType: getType, 65 | getContent: getContent, 66 | configuration: configuration, 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BIN_NAME := "tryssh" 2 | CMD_PACKAGE := github.com/Driver-C/tryssh/cmd/version 3 | GO_VERSION := $(shell go version | awk '{print $$3}') 4 | BUILD_TIME := $(shell date -u '+%Y-%m-%d %H:%M:%S') 5 | LDFLAGS := 6 | 7 | GIT_TAG = $(shell git describe --tags --abbrev=0 --exact-match 2>/dev/null) 8 | 9 | OS_ARCH_LIST=darwin:amd64 darwin:arm64 freebsd:amd64 linux:amd64 linux:arm linux:arm64 windows:amd64 windows:arm64 10 | 11 | ifdef VERSION 12 | BINARY_VERSION = $(VERSION) 13 | endif 14 | BINARY_VERSION ?= $(GIT_TAG) 15 | 16 | ifeq ($(BINARY_VERSION),) 17 | # If cannot find any information that can be used as a version number, change it to debug 18 | BINARY_VERSION := "debug" 19 | endif 20 | 21 | LDFLAGS += -X '$(CMD_PACKAGE).Version=$(BINARY_VERSION)' 22 | LDFLAGS += -X '$(CMD_PACKAGE).BuildGoVersion=$(GO_VERSION)' 23 | LDFLAGS += -X '$(CMD_PACKAGE).BuildTime=$(BUILD_TIME) UTC' 24 | 25 | .PHONY: default 26 | default: build 27 | 28 | .PHONY: build 29 | build: tidy 30 | @go build -v -trimpath -ldflags "$(LDFLAGS)" ./ 31 | 32 | .PHONY: tidy 33 | tidy: clean 34 | @go mod tidy 35 | 36 | .PHONY: clean 37 | clean: 38 | @go clean 39 | @rm -f ./$(BIN_NAME) 40 | @rm -rf ./release 41 | 42 | .PHONY: multi 43 | multi: tidy 44 | @$(foreach n, $(OS_ARCH_LIST),\ 45 | os=$(shell echo "$(n)" | cut -d : -f 1);\ 46 | arch=$(shell echo "$(n)" | cut -d : -f 2);\ 47 | target_suffix=$(BINARY_VERSION)-$${os}-$${arch};\ 48 | bin_name="$(BIN_NAME)";\ 49 | if [ $${os} = "windows" ]; then bin_name="$(BIN_NAME).exe"; fi;\ 50 | echo "[==> Build $${os}-$${arch} start... <==]";\ 51 | mkdir -p ./release/$${os}-$${arch};\ 52 | cp ./LICENSE ./release/$${os}-$${arch}/;\ 53 | env CGO_ENABLED=0 GOOS=$${os} GOARCH=$${arch} go build -v -trimpath -ldflags "$(LDFLAGS)" \ 54 | -o ./release/$${os}-$${arch}/$${bin_name};\ 55 | cd ./release;\ 56 | zip -rq $(BIN_NAME)-$${target_suffix}.zip $${os}-$${arch};\ 57 | rm -rf $${os}-$${arch};\ 58 | cd ..;\ 59 | echo "[==> Build $${os}-$${arch} done <==]";\ 60 | ) 61 | -------------------------------------------------------------------------------- /pkg/utils/file.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "gopkg.in/yaml.v3" 5 | "io/fs" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | const ( 11 | configFileMode = 0644 12 | ) 13 | 14 | func FileYamlMarshalAndWrite(path string, conf interface{}) bool { 15 | // Create a directory if it does not exist 16 | dirPath := filepath.Dir(path) 17 | if _, err := os.Stat(dirPath); err != nil { 18 | if os.IsNotExist(err) { 19 | if err := os.MkdirAll(dirPath, 0755); err != nil { 20 | Logger.Fatalln("Directory creation failed: ", err) 21 | } 22 | } else { 23 | Logger.Fatalln("An error occurred while searching for the directory: ", dirPath) 24 | } 25 | } 26 | 27 | confData, err := yaml.Marshal(conf) 28 | if err != nil { 29 | Logger.Fatalln("Configuration file marshal failed: ", err) 30 | } else { 31 | err := os.WriteFile(path, confData, configFileMode) 32 | if err != nil { 33 | Logger.Fatalln("Configuration file writing failed: ", err) 34 | } 35 | } 36 | return true 37 | } 38 | 39 | func ReadFile(filePath string) ([]byte, bool) { 40 | content, err := os.ReadFile(filePath) 41 | if err != nil { 42 | Logger.Errorln("Error reading file: ", err) 43 | return nil, false 44 | } 45 | return content, true 46 | } 47 | 48 | func CheckFileIsExist(filename string) bool { 49 | if _, err := os.Stat(filename); os.IsNotExist(err) { 50 | return false 51 | } 52 | return true 53 | } 54 | 55 | func CreateFile(filePath string, perm fs.FileMode) bool { 56 | file, err := os.Create(filePath) 57 | if err != nil { 58 | Logger.Errorln("Create file error: ", err) 59 | return false 60 | } 61 | if err := file.Chmod(perm); err != nil { 62 | Logger.Errorln("Chmod error: ", err) 63 | return false 64 | } 65 | 66 | defer func(file *os.File) { 67 | err := file.Close() 68 | if err != nil { 69 | Logger.Fatalln("Failed to close file after creating it: ", err) 70 | } 71 | }(file) 72 | return true 73 | } 74 | 75 | func UpdateFile(filePath string, fileContent []byte, perm fs.FileMode) bool { 76 | if err := os.WriteFile(filePath, fileContent, perm); err != nil { 77 | Logger.Errorln("File writing failed: ", err) 78 | return false 79 | } 80 | return true 81 | } 82 | -------------------------------------------------------------------------------- /pkg/control/control.go: -------------------------------------------------------------------------------- 1 | package control 2 | 3 | import ( 4 | "context" 5 | "github.com/Driver-C/tryssh/pkg/launcher" 6 | "github.com/cheggaaa/pb/v3" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | const ( 12 | TypeUsers = "users" 13 | TypePorts = "ports" 14 | TypePasswords = "passwords" 15 | TypeCaches = "caches" 16 | TypeKeys = "keys" 17 | sshClientTimeoutWhenLogin = 5 * time.Second 18 | ) 19 | 20 | func ConcurrencyTryToConnect(concurrency int, connectors []launcher.Connector) []launcher.Connector { 21 | hitConnectors := make([]launcher.Connector, 0) 22 | mutex := new(sync.Mutex) 23 | bar := pb.StartNew(len(connectors)) 24 | bar.Set("prefix", "Attempting:") 25 | // If the number of connectors is less than the set concurrency, change the concurrency to the number of connectors 26 | if concurrency > len(connectors) { 27 | concurrency = len(connectors) 28 | } 29 | connectorsChan := make(chan launcher.Connector) 30 | ctx, cancelFunc := context.WithCancel(context.Background()) 31 | // Producer 32 | go func(ctx context.Context, connectorsChan chan<- launcher.Connector, connectors []launcher.Connector) { 33 | for _, connector := range connectors { 34 | select { 35 | case <-ctx.Done(): 36 | break 37 | default: 38 | connectorsChan <- connector 39 | } 40 | } 41 | close(connectorsChan) 42 | }(ctx, connectorsChan, connectors) 43 | // Consumer 44 | var wg sync.WaitGroup 45 | for i := 0; i < concurrency; i++ { 46 | wg.Add(1) 47 | go func(ctx context.Context, cancelFunc context.CancelFunc, 48 | connectorsChan <-chan launcher.Connector, cwg *sync.WaitGroup, mutex *sync.Mutex) { 49 | defer cwg.Done() 50 | for { 51 | select { 52 | case <-ctx.Done(): 53 | return 54 | case connector, ok := <-connectorsChan: 55 | if !ok { 56 | return 57 | } 58 | if err := connector.TryToConnect(); err == nil { 59 | mutex.Lock() 60 | hitConnectors = append(hitConnectors, connector) 61 | mutex.Unlock() 62 | bar.Finish() 63 | cancelFunc() 64 | } 65 | bar.Increment() 66 | } 67 | } 68 | }(ctx, cancelFunc, connectorsChan, &wg, mutex) 69 | } 70 | wg.Wait() 71 | bar.Finish() 72 | cancelFunc() 73 | return hitConnectors 74 | } 75 | -------------------------------------------------------------------------------- /cmd/create/caches.go: -------------------------------------------------------------------------------- 1 | package create 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/Driver-C/tryssh/pkg/config" 6 | "github.com/Driver-C/tryssh/pkg/control" 7 | "github.com/Driver-C/tryssh/pkg/utils" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func NewCachesCommand() *cobra.Command { 12 | cmd := &cobra.Command{ 13 | Use: "caches ", 14 | Short: "Create an alternative cache", 15 | Long: "Create an alternative cache", 16 | Aliases: []string{"cache"}, 17 | Run: func(cmd *cobra.Command, args []string) { 18 | newIp, _ := cmd.Flags().GetString("ip") 19 | newUser, _ := cmd.Flags().GetString("user") 20 | newPort, _ := cmd.Flags().GetString("port") 21 | newPassword, _ := cmd.Flags().GetString("pwd") 22 | newAlias, _ := cmd.Flags().GetString("alias") 23 | newCacheContent := control.CacheContent{ 24 | Ip: newIp, 25 | User: newUser, 26 | Port: newPort, 27 | Password: newPassword, 28 | Alias: newAlias, 29 | } 30 | contentJson, err := json.Marshal(newCacheContent) 31 | if err != nil { 32 | utils.Logger.Errorln("Cache content JSON marshal failed.") 33 | return 34 | } 35 | configuration := config.LoadConfig() 36 | controller := control.NewCreateController(control.TypeCaches, string(contentJson), configuration) 37 | controller.ExecuteCreate() 38 | }, 39 | } 40 | cmd.Flags().StringP("ip", "i", "", "The ipaddress of the cache to be added") 41 | cmd.Flags().StringP("user", "u", "", "The username of the cache to be added") 42 | cmd.Flags().StringP("port", "P", "", "The port of the cache to be added") 43 | cmd.Flags().StringP("pwd", "p", "", "The password of the cache to be added") 44 | cmd.Flags().StringP("alias", "a", "", "The alias of the cache to be added") 45 | 46 | if err := cmd.MarkFlagRequired("ip"); err != nil { 47 | utils.Logger.Errorln("Flag: ip must be set.") 48 | return nil 49 | } 50 | if err := cmd.MarkFlagRequired("user"); err != nil { 51 | utils.Logger.Errorln("Flag: user must be set.") 52 | return nil 53 | } 54 | if err := cmd.MarkFlagRequired("port"); err != nil { 55 | utils.Logger.Errorln("Flag: port must be set.") 56 | return nil 57 | } 58 | if err := cmd.MarkFlagRequired("pwd"); err != nil { 59 | utils.Logger.Errorln("Flag: password must be set.") 60 | return nil 61 | } 62 | return cmd 63 | } 64 | -------------------------------------------------------------------------------- /cmd/scp/scp.go: -------------------------------------------------------------------------------- 1 | package scp 2 | 3 | import ( 4 | "github.com/Driver-C/tryssh/pkg/config" 5 | "github.com/Driver-C/tryssh/pkg/control" 6 | "github.com/spf13/cobra" 7 | "time" 8 | ) 9 | 10 | const ( 11 | concurrency = 8 12 | sshTimeout = 1 * time.Second 13 | ) 14 | 15 | var scpExample = `# Download test.txt file from 192.168.1.1 and place it under ./ 16 | tryssh scp 192.168.1.1:/root/test.txt ./ 17 | # Upload test.txt file to 192.168.1.1 and place it under /root/ 18 | tryssh scp ./test.txt 192.168.1.1:/root/ 19 | # Download test.txt file from 192.168.1.1 and rename it to test2.txt and place it under ./ 20 | tryssh scp 192.168.1.1:/root/test.txt ./test2.txt 21 | 22 | # Download testDir directory from 192.168.1.1 and place it under ~/Downloads/ 23 | tryssh scp -r 192.168.1.1:/root/testDir ~/Downloads/ 24 | # Upload testDir directory to 192.168.1.1 and rename it to testDir2 and place it under /root/ 25 | tryssh scp -r ~/Downloads/testDir 192.168.1.1:/root/testDir2` 26 | 27 | func NewScpCommand() *cobra.Command { 28 | cmd := &cobra.Command{ 29 | Use: "scp ", 30 | Args: cobra.ExactArgs(2), 31 | Short: "Upload/Download file to/from the server through SSH protocol", 32 | Long: "Upload/Download file to/from the server through SSH protocol", 33 | Example: scpExample, 34 | Run: func(cmd *cobra.Command, args []string) { 35 | source := args[0] 36 | destination := args[1] 37 | user, _ := cmd.Flags().GetString("user") 38 | concurrencyOpt, _ := cmd.Flags().GetInt("concurrency") 39 | timeout, _ := cmd.Flags().GetDuration("timeout") 40 | recursive, _ := cmd.Flags().GetBool("recursive") 41 | configuration := config.LoadConfig() 42 | controller := control.NewScpController(source, destination, configuration) 43 | controller.TryCopy(user, concurrencyOpt, recursive, timeout) 44 | }, 45 | } 46 | cmd.Flags().StringP( 47 | "user", "u", "", "Specify a username to attempt to login to the server,\n"+ 48 | "if the specified username does not exist, try logging in using that username") 49 | cmd.Flags().IntP( 50 | "concurrency", "c", concurrency, "Number of multiple requests to perform at a time") 51 | cmd.Flags().BoolP("recursive", "r", false, "Recursively copy entire directories") 52 | cmd.Flags().DurationP("timeout", "t", sshTimeout, 53 | "SSH timeout when attempting to log in. It can be \"1s\" or \"1m\" or other duration") 54 | return cmd 55 | } 56 | -------------------------------------------------------------------------------- /pkg/control/create.go: -------------------------------------------------------------------------------- 1 | package control 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/Driver-C/tryssh/pkg/config" 6 | "github.com/Driver-C/tryssh/pkg/utils" 7 | ) 8 | 9 | type CacheContent struct { 10 | Ip string `json:"ip"` 11 | Port string `json:"port"` 12 | User string `json:"user"` 13 | Password string `json:"password"` 14 | Alias string `json:"alias"` 15 | } 16 | 17 | type CreateController struct { 18 | createType string 19 | createContent string 20 | configuration *config.MainConfig 21 | } 22 | 23 | func (cc CreateController) ExecuteCreate() { 24 | switch cc.createType { 25 | case TypeUsers: 26 | cc.configuration.Main.Users = utils.RemoveDuplicate( 27 | append(cc.configuration.Main.Users, cc.createContent)) 28 | cc.updateConfig() 29 | case TypePorts: 30 | cc.configuration.Main.Ports = utils.RemoveDuplicate( 31 | append(cc.configuration.Main.Ports, cc.createContent)) 32 | cc.updateConfig() 33 | case TypePasswords: 34 | cc.configuration.Main.Passwords = utils.RemoveDuplicate( 35 | append(cc.configuration.Main.Passwords, cc.createContent)) 36 | cc.updateConfig() 37 | case TypeKeys: 38 | cc.configuration.Main.Keys = utils.RemoveDuplicate( 39 | append(cc.configuration.Main.Keys, cc.createContent)) 40 | cc.updateConfig() 41 | case TypeCaches: 42 | cc.createCaches() 43 | cc.updateConfig() 44 | } 45 | } 46 | 47 | func (cc CreateController) updateConfig() { 48 | if config.UpdateConfig(cc.configuration) { 49 | utils.Logger.Infof("Create %s: %s completed.\n", cc.createType, cc.createContent) 50 | } else { 51 | utils.Logger.Errorf("Create %s: %s failed.\n", cc.createType, cc.createContent) 52 | } 53 | } 54 | 55 | func (cc CreateController) createCaches() { 56 | var newCache CacheContent 57 | if err := json.Unmarshal([]byte(cc.createContent), &newCache); err == nil { 58 | cc.configuration.ServerLists = append(cc.configuration.ServerLists, 59 | config.ServerListConfig{ 60 | Ip: newCache.Ip, 61 | Port: newCache.Port, 62 | User: newCache.User, 63 | Password: newCache.Password, 64 | Alias: newCache.Alias, 65 | }, 66 | ) 67 | } else { 68 | utils.Logger.Errorln("Cache's JSON unmarshal failed.") 69 | } 70 | } 71 | 72 | func NewCreateController(createType string, createContent string, 73 | configuration *config.MainConfig) *CreateController { 74 | return &CreateController{ 75 | createType: createType, 76 | createContent: createContent, 77 | configuration: configuration, 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /pkg/launcher/ssh.go: -------------------------------------------------------------------------------- 1 | package launcher 2 | 3 | import ( 4 | "github.com/Driver-C/tryssh/pkg/utils" 5 | "golang.org/x/crypto/ssh" 6 | "golang.org/x/crypto/ssh/terminal" 7 | "os" 8 | "time" 9 | ) 10 | 11 | type SshLauncher struct { 12 | SshConnector 13 | } 14 | 15 | func (h *SshLauncher) Launch() bool { 16 | return h.dialServer() 17 | } 18 | 19 | func NewSshLaunchersByCombinations(combinations chan []interface{}, 20 | sshTimeout time.Duration) (launchers []*SshLauncher) { 21 | for com := range combinations { 22 | launchers = append(launchers, &SshLauncher{SshConnector{ 23 | Ip: com[0].(string), 24 | Port: com[1].(string), 25 | User: com[2].(string), 26 | Password: com[3].(string), 27 | Key: com[4].(string), 28 | SshTimeout: sshTimeout, 29 | }}) 30 | } 31 | return 32 | } 33 | 34 | func (h *SshLauncher) dialServer() (res bool) { 35 | res = false 36 | sshClient, err := h.CreateConnection() 37 | if err == nil { 38 | utils.Logger.Infoln("[ LOGIN SUCCESSFUL ]\n") 39 | utils.Logger.Infoln("User:", sshClient.User()) 40 | utils.Logger.Infoln("Port:", h.Port) 41 | res = true 42 | h.createTerminal(sshClient) 43 | } else { 44 | return 45 | } 46 | defer h.CloseConnection(sshClient) 47 | return 48 | } 49 | 50 | func (h *SshLauncher) createTerminal(conn *ssh.Client) { 51 | session, err := conn.NewSession() 52 | if err != nil { 53 | utils.Logger.Fatalln(err.Error()) 54 | } 55 | defer func(conn *ssh.Client) { 56 | if err := session.Close(); err != nil { 57 | if err.Error() != "EOF" { 58 | utils.Logger.Fatalln(err.Error()) 59 | } 60 | } 61 | }(conn) 62 | 63 | modes := ssh.TerminalModes{ 64 | ssh.ECHO: 1, 65 | ssh.TTY_OP_ISPEED: 14400, 66 | ssh.TTY_OP_OSPEED: 14400, 67 | ssh.VSTATUS: 1, 68 | } 69 | fd := int(os.Stdin.Fd()) 70 | oldState, err := terminal.MakeRaw(fd) 71 | if err != nil { 72 | utils.Logger.Fatalln(err.Error()) 73 | } 74 | defer func(fd int, oldState *terminal.State) { 75 | if err := terminal.Restore(fd, oldState); err != nil { 76 | utils.Logger.Fatalln(err.Error()) 77 | } 78 | }(fd, oldState) 79 | 80 | termWidth, termHeight, err := terminal.GetSize(fd) 81 | session.Stdin = os.Stdin 82 | session.Stdout = os.Stdout 83 | session.Stderr = os.Stderr 84 | 85 | err = session.RequestPty(TerminalTerm, termHeight, termWidth, modes) 86 | if err != nil { 87 | utils.Logger.Fatalln(err.Error()) 88 | } 89 | 90 | err = session.Shell() 91 | if err != nil { 92 | utils.Logger.Fatalln(err.Error()) 93 | } 94 | 95 | err = session.Wait() 96 | if err != nil { 97 | utils.Logger.Warnln(err.Error()) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /pkg/control/alias.go: -------------------------------------------------------------------------------- 1 | package control 2 | 3 | import ( 4 | "fmt" 5 | "github.com/Driver-C/tryssh/pkg/config" 6 | "github.com/Driver-C/tryssh/pkg/utils" 7 | ) 8 | 9 | type AliasController struct { 10 | targetIp string 11 | configuration *config.MainConfig 12 | alias string 13 | } 14 | 15 | func (ac *AliasController) SetAlias() { 16 | var beSetCount int 17 | for index, server := range ac.configuration.ServerLists { 18 | if server.Ip == ac.targetIp { 19 | aliasServerList := ac.getServerListFromAlias() 20 | if len(aliasServerList) != 0 { 21 | ac.ListAlias() 22 | utils.Logger.Fatalf( 23 | "The alias \"%s\" has already been set, try another alias or delete it and set again.\n", 24 | ac.alias) 25 | } 26 | ac.configuration.ServerLists[index].Alias = ac.alias 27 | utils.Logger.Infof( 28 | "The server %s@%s:%s's alias \"%s\" will be set.\n", 29 | server.User, ac.targetIp, server.Port, ac.alias) 30 | beSetCount++ 31 | } 32 | } 33 | if config.UpdateConfig(ac.configuration) { 34 | utils.Logger.Infof("%d cache information has been changed.\n", beSetCount) 35 | } else { 36 | utils.Logger.Fatalln("Main config update failed.") 37 | } 38 | } 39 | 40 | func (ac *AliasController) ListAlias() { 41 | var aliasCount int 42 | for _, server := range ac.configuration.ServerLists { 43 | if ac.alias == "" { 44 | if server.Alias != "" { 45 | fmt.Printf("Alias: %s Server: %s\n", server.Alias, server.Ip) 46 | aliasCount++ 47 | } 48 | } else { 49 | if server.Alias == ac.alias { 50 | fmt.Printf("Alias: %s Server: %s\n", server.Alias, server.Ip) 51 | aliasCount++ 52 | } 53 | } 54 | } 55 | if aliasCount == 0 { 56 | utils.Logger.Infoln("No aliases were found that have been set.") 57 | } 58 | } 59 | 60 | func (ac *AliasController) UnsetAlias() { 61 | var beUnsetCount int 62 | for index, server := range ac.configuration.ServerLists { 63 | if server.Alias == ac.alias { 64 | ac.configuration.ServerLists[index].Alias = "" 65 | utils.Logger.Infof( 66 | "The server %s@%s:%s's alias \"%s\" will be unset.\n", 67 | server.User, ac.targetIp, server.Port, ac.alias) 68 | beUnsetCount++ 69 | } 70 | } 71 | if config.UpdateConfig(ac.configuration) { 72 | utils.Logger.Infof("%d cache information has been changed.\n", beUnsetCount) 73 | } else { 74 | utils.Logger.Fatalln("Main config update failed.") 75 | } 76 | } 77 | 78 | func (ac *AliasController) getServerListFromAlias() []config.ServerListConfig { 79 | var aliasServerList []config.ServerListConfig 80 | for _, server := range ac.configuration.ServerLists { 81 | if server.Alias == ac.alias && ac.alias != "" { 82 | aliasServerList = append(aliasServerList, server) 83 | } 84 | } 85 | return aliasServerList 86 | } 87 | 88 | func NewAliasController(targetIp string, configuration *config.MainConfig, alias string) *AliasController { 89 | return &AliasController{ 90 | targetIp: targetIp, 91 | configuration: configuration, 92 | alias: alias, 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /pkg/control/delete.go: -------------------------------------------------------------------------------- 1 | package control 2 | 3 | import ( 4 | "github.com/Driver-C/tryssh/pkg/config" 5 | "github.com/Driver-C/tryssh/pkg/utils" 6 | ) 7 | 8 | type DeleteController struct { 9 | deleteType string 10 | deleteContent string 11 | configuration *config.MainConfig 12 | } 13 | 14 | func (dc DeleteController) ExecuteDelete() { 15 | switch dc.deleteType { 16 | case TypeUsers: 17 | contents := dc.configuration.Main.Users 18 | if newContents := dc.searchAndDelete(contents); newContents != nil { 19 | dc.configuration.Main.Users = newContents 20 | dc.updateConfig() 21 | } else { 22 | utils.Logger.Warnf("No matching username: %s\n", dc.deleteContent) 23 | } 24 | case TypePorts: 25 | contents := dc.configuration.Main.Ports 26 | if newContents := dc.searchAndDelete(contents); newContents != nil { 27 | dc.configuration.Main.Ports = newContents 28 | dc.updateConfig() 29 | } else { 30 | utils.Logger.Warnf("No matching port: %s\n", dc.deleteContent) 31 | } 32 | case TypePasswords: 33 | contents := dc.configuration.Main.Passwords 34 | if newContents := dc.searchAndDelete(contents); newContents != nil { 35 | dc.configuration.Main.Passwords = newContents 36 | dc.updateConfig() 37 | } else { 38 | utils.Logger.Warnf("No matching password: %s\n", dc.deleteContent) 39 | } 40 | case TypeKeys: 41 | contents := dc.configuration.Main.Keys 42 | if newContents := dc.searchAndDelete(contents); newContents != nil { 43 | dc.configuration.Main.Keys = newContents 44 | dc.updateConfig() 45 | } else { 46 | utils.Logger.Warnf("No matching key: %s\n", dc.deleteContent) 47 | } 48 | case TypeCaches: 49 | // dc.deleteContent is ipAddress 50 | var deleteCount int 51 | if dc.deleteContent != "" { 52 | for index, server := range dc.configuration.ServerLists { 53 | if server.Ip == dc.deleteContent { 54 | dc.configuration.ServerLists = append(dc.configuration.ServerLists[:index], 55 | dc.configuration.ServerLists[index+1:]...) 56 | dc.updateConfig() 57 | deleteCount++ 58 | } 59 | } 60 | if deleteCount == 0 { 61 | utils.Logger.Warnf("No matching cache: %s\n", dc.deleteContent) 62 | } 63 | } else { 64 | utils.Logger.Errorln("IP address cannot be empty characters") 65 | } 66 | } 67 | } 68 | 69 | func (dc DeleteController) searchAndDelete(contents []string) []string { 70 | for index, content := range contents { 71 | if dc.deleteContent == content { 72 | contents = append(contents[:index], contents[index+1:]...) 73 | return contents 74 | } 75 | } 76 | return nil 77 | } 78 | 79 | func (dc DeleteController) updateConfig() { 80 | if config.UpdateConfig(dc.configuration) { 81 | utils.Logger.Infof("Delete %s: %s completed.\n", dc.deleteType, dc.deleteContent) 82 | } else { 83 | utils.Logger.Errorf("Delete %s: %s failed.\n", dc.deleteType, dc.deleteContent) 84 | } 85 | } 86 | 87 | func NewDeleteController(deleteType string, deleteContent string, 88 | configuration *config.MainConfig) *DeleteController { 89 | return &DeleteController{ 90 | deleteType: deleteType, 91 | deleteContent: deleteContent, 92 | configuration: configuration, 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /pkg/control/prune.go: -------------------------------------------------------------------------------- 1 | package control 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "github.com/Driver-C/tryssh/pkg/config" 7 | "github.com/Driver-C/tryssh/pkg/launcher" 8 | "github.com/Driver-C/tryssh/pkg/utils" 9 | "os" 10 | "strings" 11 | "sync" 12 | "time" 13 | ) 14 | 15 | type PruneController struct { 16 | configuration *config.MainConfig 17 | auto bool 18 | sshTimeout time.Duration 19 | concurrency int 20 | } 21 | 22 | func (pc *PruneController) PruneCaches() { 23 | newServerList := make([]config.ServerListConfig, 0) 24 | if pc.auto { 25 | newServerList = pc.concurrencyDeleteCache() 26 | } else { 27 | for _, server := range pc.configuration.ServerLists { 28 | lan := &launcher.SshLauncher{SshConnector: *launcher.GetSshConnectorFromConfig(&server)} 29 | // Set timeout 30 | lan.SshTimeout = pc.sshTimeout 31 | // Determine if connection is possible 32 | if err := lan.TryToConnect(); err != nil { 33 | if !pc.interactiveDeleteCache(server) { 34 | newServerList = append(newServerList, server) 35 | } 36 | } else { 37 | utils.Logger.Infof("Cache %v is still available.", server) 38 | newServerList = append(newServerList, server) 39 | } 40 | } 41 | } 42 | pc.configuration.ServerLists = newServerList 43 | if config.UpdateConfig(pc.configuration) { 44 | utils.Logger.Infoln("Update config successful.") 45 | } else { 46 | utils.Logger.Errorln("Update config failed.") 47 | } 48 | } 49 | 50 | func (pc *PruneController) interactiveDeleteCache(server config.ServerListConfig) bool { 51 | reader := bufio.NewReader(os.Stdin) 52 | for { 53 | fmt.Printf("Are you sure you want to delete this cache? "+ 54 | "Please enter \"yes\" to confirm deletion, or \"no\" to cancel. %s\n"+ 55 | "(yes/no): ", server) 56 | stdin, _ := reader.ReadString('\n') 57 | // Delete space 58 | stdin = strings.TrimSpace(stdin) 59 | switch stdin { 60 | case "yes": 61 | utils.Logger.Infof("The cache %v has been marked for deletion.", server) 62 | return true 63 | case "no": 64 | utils.Logger.Infof("Cache %v skipped.", server) 65 | return false 66 | default: 67 | utils.Logger.Errorln("Input error:", stdin) 68 | } 69 | } 70 | } 71 | 72 | func (pc *PruneController) concurrencyDeleteCache() []config.ServerListConfig { 73 | newServerList := make([]config.ServerListConfig, 0) 74 | serversChan := make(chan *config.ServerListConfig) 75 | var mutex sync.Mutex 76 | var wg sync.WaitGroup 77 | 78 | go func(serversChan chan<- *config.ServerListConfig) { 79 | for _, server := range pc.configuration.ServerLists { 80 | newServer := server 81 | serversChan <- &newServer 82 | } 83 | close(serversChan) 84 | }(serversChan) 85 | 86 | for i := 0; i < pc.concurrency; i++ { 87 | wg.Add(1) 88 | go func(serversChan <-chan *config.ServerListConfig, wg *sync.WaitGroup) { 89 | defer wg.Done() 90 | for { 91 | serverP, ok := <-serversChan 92 | if !ok { 93 | break 94 | } 95 | lan := &launcher.SshLauncher{SshConnector: *launcher.GetSshConnectorFromConfig(serverP)} 96 | lan.SshTimeout = pc.sshTimeout 97 | if err := lan.TryToConnect(); err == nil { 98 | utils.Logger.Infof("Cache %v is still available.", *serverP) 99 | mutex.Lock() 100 | newServerList = append(newServerList, *serverP) 101 | mutex.Unlock() 102 | } else { 103 | utils.Logger.Infof("The cache %v has been marked for deletion.", *serverP) 104 | } 105 | } 106 | }(serversChan, &wg) 107 | } 108 | wg.Wait() 109 | return newServerList 110 | } 111 | 112 | func NewPruneController(configuration *config.MainConfig, auto bool, timeout time.Duration, 113 | concurrency int) *PruneController { 114 | return &PruneController{ 115 | configuration: configuration, 116 | auto: auto, 117 | sshTimeout: timeout, 118 | concurrency: concurrency, 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /pkg/control/ssh.go: -------------------------------------------------------------------------------- 1 | package control 2 | 3 | import ( 4 | "github.com/Driver-C/tryssh/pkg/config" 5 | "github.com/Driver-C/tryssh/pkg/launcher" 6 | "github.com/Driver-C/tryssh/pkg/utils" 7 | "time" 8 | ) 9 | 10 | type SshController struct { 11 | targetIp string 12 | configuration *config.MainConfig 13 | cacheIsFound bool 14 | cacheIndex int 15 | concurrency int 16 | sshTimeout time.Duration 17 | } 18 | 19 | // TryLogin Functional entrance 20 | func (sc *SshController) TryLogin(user string, concurrency int, sshTimeout time.Duration) { 21 | // Set timeout 22 | sc.sshTimeout = sshTimeout 23 | // Set concurrency 24 | sc.concurrency = concurrency 25 | // Obtain the real address based on the alias 26 | sc.searchAliasExistsOrNot() 27 | var targetServer *config.ServerListConfig 28 | targetServer, sc.cacheIndex, sc.cacheIsFound = config.SelectServerCache(user, sc.targetIp, sc.configuration) 29 | if user != "" { 30 | utils.Logger.Infof("Specify the username \"%s\" to attempt to login to the server.\n", user) 31 | } 32 | if sc.cacheIsFound { 33 | utils.Logger.Infof("The cache for %s is found, which will be used to try.\n", sc.targetIp) 34 | sc.tryLoginWithCache(user, targetServer) 35 | } else { 36 | utils.Logger.Warnf("The cache for %s could not be found. Start trying to login.\n\n", sc.targetIp) 37 | sc.tryLoginWithoutCache(user) 38 | } 39 | } 40 | 41 | func (sc *SshController) tryLoginWithCache(user string, targetServer *config.ServerListConfig) { 42 | lan := &launcher.SshLauncher{SshConnector: *launcher.GetSshConnectorFromConfig(targetServer)} 43 | // Set default timeout time 44 | lan.SshTimeout = sshClientTimeoutWhenLogin 45 | if !lan.Launch() { 46 | utils.Logger.Errorf("Failed to log in with cached information. Start trying to login again.\n\n") 47 | sc.tryLoginWithoutCache(user) 48 | } 49 | } 50 | 51 | func (sc *SshController) tryLoginWithoutCache(user string) { 52 | combinations := config.GenerateCombination(sc.targetIp, user, sc.configuration) 53 | launchers := launcher.NewSshLaunchersByCombinations(combinations, sc.sshTimeout) 54 | connectors := make([]launcher.Connector, len(launchers)) 55 | for i, l := range launchers { 56 | connectors[i] = l 57 | } 58 | hitLaunchers := ConcurrencyTryToConnect(sc.concurrency, connectors) 59 | if len(hitLaunchers) > 0 { 60 | utils.Logger.Infoln("Login succeeded. The cache will be added.\n") 61 | hitLauncher := hitLaunchers[0].(*launcher.SshLauncher) 62 | // The new server cache information 63 | newServerCache := launcher.GetConfigFromSshConnector(&hitLauncher.SshConnector) 64 | // Determine if the login attempt was successful after the old cache login failed. 65 | // If so, delete the old cache information that cannot be logged in after the login attempt is successful 66 | if sc.cacheIsFound { 67 | // Sync outdated cache's alias 68 | newServerCache.Alias = sc.configuration.ServerLists[sc.cacheIndex].Alias 69 | 70 | utils.Logger.Infoln("The old cache will be deleted.\n") 71 | sc.configuration.ServerLists = append( 72 | sc.configuration.ServerLists[:sc.cacheIndex], sc.configuration.ServerLists[sc.cacheIndex+1:]...) 73 | } 74 | sc.configuration.ServerLists = append(sc.configuration.ServerLists, *newServerCache) 75 | if config.UpdateConfig(sc.configuration) { 76 | utils.Logger.Infoln("Cache added.\n\n") 77 | // If the timeout time is less than sshClientTimeoutWhenLogin during login, 78 | // change to sshClientTimeoutWhenLogin 79 | if hitLauncher.SshTimeout < sshClientTimeoutWhenLogin { 80 | hitLauncher.SshTimeout = sshClientTimeoutWhenLogin 81 | } 82 | if !hitLauncher.Launch() { 83 | utils.Logger.Errorf("Login failed.\n") 84 | } 85 | } else { 86 | utils.Logger.Errorf("Cache added failed.\n\n") 87 | } 88 | } else { 89 | utils.Logger.Errorf("There is no password combination that can log in.\n") 90 | } 91 | } 92 | 93 | func (sc *SshController) searchAliasExistsOrNot() { 94 | for _, server := range sc.configuration.ServerLists { 95 | if server.Alias == sc.targetIp { 96 | sc.targetIp = server.Ip 97 | } 98 | } 99 | } 100 | 101 | func NewSshController(targetIp string, configuration *config.MainConfig) *SshController { 102 | return &SshController{ 103 | targetIp: targetIp, 104 | configuration: configuration, 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /pkg/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/Driver-C/tryssh/pkg/utils" 5 | "github.com/schwarmco/go-cartesian-product" 6 | "gopkg.in/yaml.v3" 7 | "os" 8 | "os/user" 9 | "path/filepath" 10 | ) 11 | 12 | const ( 13 | configFileName = "tryssh.db" 14 | configDirName = ".tryssh" 15 | knownHostsFileName = "known_hosts" 16 | ) 17 | 18 | var ( 19 | configPath string 20 | KnownHostsPath string 21 | ) 22 | 23 | func init() { 24 | if usr, err := user.Current(); err != nil { 25 | utils.Logger.Warnf("Unable to obtain current user information: %s, "+ 26 | "Will use the current directory as the configuration file directory.", err) 27 | configPath = filepath.Join("./", configDirName, configFileName) 28 | KnownHostsPath = filepath.Join("./", configDirName, knownHostsFileName) 29 | } else { 30 | configPath = filepath.Join(usr.HomeDir, configDirName, configFileName) 31 | KnownHostsPath = filepath.Join(usr.HomeDir, configDirName, knownHostsFileName) 32 | } 33 | } 34 | 35 | // MainConfig Main config 36 | type MainConfig struct { 37 | Main struct { 38 | Ports []string `yaml:"ports,flow"` 39 | Users []string `yaml:"users,flow"` 40 | Passwords []string `yaml:"passwords,flow"` 41 | Keys []string `yaml:"keys,flow"` 42 | } `yaml:"main"` 43 | ServerLists []ServerListConfig `yaml:"serverList"` 44 | } 45 | 46 | // ServerListConfig Server information cache list 47 | type ServerListConfig struct { 48 | Ip string `yaml:"ip"` 49 | Port string `yaml:"port"` 50 | User string `yaml:"user"` 51 | Password string `yaml:"password"` 52 | Key string `yaml:"key"` 53 | Alias string `yaml:"alias"` 54 | } 55 | 56 | // generateConfig Generate initial configuration file (force overwrite) 57 | func generateConfig() { 58 | utils.Logger.Infoln("Generating configuration file.\n") 59 | _ = utils.FileYamlMarshalAndWrite(configPath, &MainConfig{}) 60 | utils.Logger.Infoln("Generating configuration file successful.\n") 61 | utils.Logger.Warnln("Main setting is empty. " + 62 | "You need to create some users, ports and passwords before running again.\n") 63 | } 64 | 65 | func LoadConfig() (c *MainConfig) { 66 | c = new(MainConfig) 67 | 68 | if utils.CheckFileIsExist(configPath) { 69 | conf, err := os.ReadFile(configPath) 70 | if err != nil { 71 | utils.Logger.Fatalln("Configuration file load failed: ", err) 72 | } 73 | unmarshalErr := yaml.Unmarshal(conf, c) 74 | if unmarshalErr != nil { 75 | utils.Logger.Fatalln("Configuration file parsing failed: ", unmarshalErr) 76 | } else { 77 | if len(c.Main.Ports) == 0 || len(c.Main.Users) == 0 || len(c.Main.Passwords) == 0 { 78 | utils.Logger.Warnln("Main setting is empty. " + 79 | "You need to create some users, ports and passwords before running again.\n") 80 | } 81 | } 82 | } else { 83 | utils.Logger.Infoln("Configuration file cannot be found, it will be generated automatically.\n") 84 | generateConfig() 85 | } 86 | 87 | // known_hosts 88 | if !utils.CheckFileIsExist(KnownHostsPath) { 89 | // Default permission is 0600 90 | if !utils.CreateFile(KnownHostsPath, 0600) { 91 | utils.Logger.Fatalln("The known_hosts file creation failed") 92 | } 93 | } 94 | return 95 | } 96 | 97 | // SelectServerCache Search cache from server list 98 | func SelectServerCache(user string, ip string, conf *MainConfig) (*ServerListConfig, int, bool) { 99 | for index, server := range conf.ServerLists { 100 | if server.Ip == ip { 101 | if user != "" { 102 | if server.User == user { 103 | return &server, index, true 104 | } 105 | } else { 106 | return &server, index, true 107 | } 108 | } 109 | } 110 | return nil, 0, false 111 | } 112 | 113 | func UpdateConfig(conf *MainConfig) (writeRes bool) { 114 | writeRes = utils.FileYamlMarshalAndWrite(configPath, conf) 115 | return 116 | } 117 | 118 | // GenerateCombination Generate objects for all port, user, and password combinations 119 | func GenerateCombination(ip string, user string, conf *MainConfig) (combinations chan []interface{}) { 120 | ips := []interface{}{ip} 121 | users := []interface{}{user} 122 | ports := utils.InterfaceSlice(conf.Main.Ports) 123 | if user == "" { 124 | users = utils.InterfaceSlice(conf.Main.Users) 125 | } 126 | passwords := utils.InterfaceSlice(conf.Main.Passwords) 127 | keys := utils.InterfaceSlice(conf.Main.Keys) 128 | // Generate combinations with immutable parameter order 129 | combinations = cartesian.Iter(ips, ports, users, passwords, keys) 130 | return 131 | } 132 | -------------------------------------------------------------------------------- /pkg/control/scp.go: -------------------------------------------------------------------------------- 1 | package control 2 | 3 | import ( 4 | "github.com/Driver-C/tryssh/pkg/config" 5 | "github.com/Driver-C/tryssh/pkg/launcher" 6 | "github.com/Driver-C/tryssh/pkg/utils" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | type ScpController struct { 12 | source string 13 | destination string 14 | configuration *config.MainConfig 15 | cacheIsFound bool 16 | cacheIndex int 17 | destIp string 18 | concurrency int 19 | sshTimeout time.Duration 20 | recursive bool 21 | } 22 | 23 | // TryCopy Functional entrance 24 | func (cc *ScpController) TryCopy(user string, concurrency int, recursive bool, sshTimeout time.Duration) { 25 | // Set timeout 26 | cc.sshTimeout = sshTimeout 27 | // Set concurrency 28 | cc.concurrency = concurrency 29 | // Set recursive or not 30 | cc.recursive = recursive 31 | if strings.Contains(cc.source, ":") { 32 | cc.destIp = strings.Split(cc.source, ":")[0] 33 | remotePath := strings.Split(cc.source, ":")[1] 34 | // Obtain the real address based on the alias 35 | cc.searchAliasExistsOrNot() 36 | // Reassemble remote server address and file path 37 | cc.source = strings.Join([]string{cc.destIp, remotePath}, ":") 38 | } else if strings.Contains(cc.destination, ":") { 39 | cc.destIp = strings.Split(cc.destination, ":")[0] 40 | remotePath := strings.Split(cc.destination, ":")[1] 41 | // Obtain the real address based on the alias 42 | cc.searchAliasExistsOrNot() 43 | // Reassemble remote server address and file path 44 | cc.destination = strings.Join([]string{cc.destIp, remotePath}, ":") 45 | } else { 46 | return 47 | } 48 | // Obtain the real address based on the alias 49 | cc.searchAliasExistsOrNot() 50 | // Reassemble remote server address and file path 51 | var targetServer *config.ServerListConfig 52 | targetServer, cc.cacheIndex, cc.cacheIsFound = config.SelectServerCache(user, cc.destIp, cc.configuration) 53 | 54 | if cc.cacheIsFound { 55 | utils.Logger.Infof("The cache for %s is found, which will be used to try.\n", cc.destIp) 56 | cc.tryCopyWithCache(user, targetServer) 57 | } else { 58 | utils.Logger.Warnf("The cache for %s could not be found. Start trying to login.\n\n", cc.destIp) 59 | cc.tryCopyWithoutCache(user) 60 | } 61 | } 62 | 63 | func (cc *ScpController) tryCopyWithCache(user string, targetServer *config.ServerListConfig) { 64 | lan := &launcher.ScpLauncher{ 65 | SshConnector: *launcher.GetSshConnectorFromConfig(targetServer), 66 | Src: cc.source, 67 | Dest: cc.destination, 68 | Recursive: cc.recursive, 69 | } 70 | // Set default timeout time 71 | lan.SshTimeout = sshClientTimeoutWhenLogin 72 | if !lan.Launch() { 73 | utils.Logger.Errorf("Failed to log in with cached information. Start trying to login again.\n\n") 74 | cc.tryCopyWithoutCache(user) 75 | } 76 | } 77 | 78 | func (cc *ScpController) tryCopyWithoutCache(user string) { 79 | combinations := config.GenerateCombination(cc.destIp, user, cc.configuration) 80 | launchers := launcher.NewScpLaunchersByCombinations(combinations, cc.source, cc.destination, 81 | cc.recursive, cc.sshTimeout) 82 | connectors := make([]launcher.Connector, len(launchers)) 83 | for i, l := range launchers { 84 | connectors[i] = l 85 | } 86 | hitLaunchers := ConcurrencyTryToConnect(cc.concurrency, connectors) 87 | if len(hitLaunchers) > 0 { 88 | utils.Logger.Infoln("Login succeeded. The cache will be added.\n") 89 | hitLauncher := hitLaunchers[0].(*launcher.ScpLauncher) 90 | // The new server cache information 91 | newServerCache := launcher.GetConfigFromSshConnector(&hitLauncher.SshConnector) 92 | // Determine if the login attempt was successful after the old cache login failed. 93 | // If so, delete the old cache information that cannot be logged in after the login attempt is successful 94 | if cc.cacheIsFound { 95 | // Sync outdated cache's alias 96 | newServerCache.Alias = cc.configuration.ServerLists[cc.cacheIndex].Alias 97 | 98 | utils.Logger.Infoln("The old cache will be deleted.\n") 99 | cc.configuration.ServerLists = append( 100 | cc.configuration.ServerLists[:cc.cacheIndex], cc.configuration.ServerLists[cc.cacheIndex+1:]...) 101 | } 102 | cc.configuration.ServerLists = append(cc.configuration.ServerLists, *newServerCache) 103 | if config.UpdateConfig(cc.configuration) { 104 | utils.Logger.Infoln("Cache added.\n\n") 105 | // If the timeout time is less than sshClientTimeoutWhenLogin during login, 106 | // change to sshClientTimeoutWhenLogin 107 | if hitLauncher.SshTimeout < sshClientTimeoutWhenLogin { 108 | hitLauncher.SshTimeout = sshClientTimeoutWhenLogin 109 | } 110 | if !hitLauncher.Launch() { 111 | utils.Logger.Errorf("Login failed.\n") 112 | } 113 | } else { 114 | utils.Logger.Errorf("Cache added failed.\n\n") 115 | } 116 | } else { 117 | utils.Logger.Errorf("There is no password combination that can log in.\n") 118 | } 119 | } 120 | 121 | func (cc *ScpController) searchAliasExistsOrNot() { 122 | for _, server := range cc.configuration.ServerLists { 123 | if server.Alias == cc.destIp { 124 | cc.destIp = server.Ip 125 | } 126 | } 127 | } 128 | 129 | func NewScpController(source string, destination string, configuration *config.MainConfig) *ScpController { 130 | return &ScpController{ 131 | source: source, 132 | destination: destination, 133 | configuration: configuration, 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /pkg/launcher/base.go: -------------------------------------------------------------------------------- 1 | package launcher 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "github.com/Driver-C/tryssh/pkg/config" 7 | "github.com/Driver-C/tryssh/pkg/utils" 8 | "golang.org/x/crypto/ssh" 9 | "net" 10 | "strings" 11 | "sync" 12 | "time" 13 | ) 14 | 15 | const ( 16 | sshProtocol string = "tcp" 17 | TerminalTerm = "xterm" 18 | SSHKeyKeyword = "SSH-KEY" 19 | ) 20 | 21 | var ( 22 | keysMap = sync.Map{} 23 | hostKeyMutex = new(sync.Mutex) 24 | ) 25 | 26 | type Connector interface { 27 | Launch() bool 28 | CreateConnection() (sshClient *ssh.Client, err error) 29 | CloseConnection(sshClient *ssh.Client) 30 | TryToConnect() (err error) 31 | } 32 | 33 | type SshConnector struct { 34 | Ip string 35 | Port string 36 | User string 37 | Password string 38 | Key string 39 | SshTimeout time.Duration 40 | } 41 | 42 | func (sc *SshConnector) Launch() bool { 43 | return false 44 | } 45 | 46 | func (sc *SshConnector) LoadConfig() (config *ssh.ClientConfig) { 47 | var authMethods []ssh.AuthMethod 48 | var privateKey []byte 49 | if sc.Key != "" { 50 | if _, ok := keysMap.Load(sc.Key); !ok { 51 | if pk, status := utils.ReadFile(sc.Key); status { 52 | keysMap.Store(sc.Key, pk) 53 | privateKey = pk 54 | } 55 | } else { 56 | pk, _ := keysMap.Load(sc.Key) 57 | privateKey = pk.([]byte) 58 | } 59 | signer, err := ssh.ParsePrivateKey(privateKey) 60 | if err == nil { 61 | authMethods = append(authMethods, ssh.PublicKeys(signer)) 62 | } else { 63 | utils.Logger.Errorln("Failed to parse private key: %v", err) 64 | } 65 | } 66 | authMethods = append(authMethods, ssh.Password(sc.Password)) 67 | config = &ssh.ClientConfig{ 68 | User: sc.User, 69 | Auth: authMethods, 70 | HostKeyCallback: trustedHostKeyCallback(searchKeyFromAddress(sc.Ip), sc.Ip, hostKeyMutex), 71 | Timeout: sc.SshTimeout, 72 | } 73 | return 74 | } 75 | 76 | func (sc *SshConnector) CreateConnection() (sshClient *ssh.Client, err error) { 77 | addr := sc.Ip + ":" + sc.Port 78 | conf := sc.LoadConfig() 79 | 80 | sshClient, err = ssh.Dial(sshProtocol, addr, conf) 81 | if err != nil { 82 | if strings.Contains(err.Error(), SSHKeyKeyword) { 83 | // If it's a public key verification issue, just exit 84 | utils.Logger.Fatalf("Unable to connect: %s Cause: %s\n", addr, err.Error()) 85 | } 86 | } 87 | return 88 | } 89 | 90 | func (sc *SshConnector) CloseConnection(sshClient *ssh.Client) { 91 | err := sshClient.Close() 92 | if err != nil { 93 | utils.Logger.Errorln("Unable to close connection: ", err.Error()) 94 | } 95 | } 96 | 97 | func (sc *SshConnector) TryToConnect() (err error) { 98 | sshClient, err := sc.CreateConnection() 99 | if err != nil { 100 | return 101 | } 102 | defer sc.CloseConnection(sshClient) 103 | return 104 | } 105 | 106 | // GetSshConnectorFromConfig Get SshConnector by ServerListConfig 107 | func GetSshConnectorFromConfig(conf *config.ServerListConfig) *SshConnector { 108 | return &SshConnector{ 109 | Ip: conf.Ip, 110 | Port: conf.Port, 111 | User: conf.User, 112 | Password: conf.Password, 113 | Key: conf.Key, 114 | } 115 | } 116 | 117 | // GetConfigFromSshConnector Get ServerListConfig by SshConnector 118 | func GetConfigFromSshConnector(tgt *SshConnector) *config.ServerListConfig { 119 | return &config.ServerListConfig{ 120 | Ip: tgt.Ip, 121 | Port: tgt.Port, 122 | User: tgt.User, 123 | Password: tgt.Password, 124 | Key: tgt.Key, 125 | } 126 | } 127 | 128 | func searchKeyFromAddress(address string) string { 129 | knownHostsContent, status := utils.ReadFile(config.KnownHostsPath) 130 | if !status { 131 | utils.Logger.Fatalln("Read known_hosts failed") 132 | } 133 | knownHostsLines := strings.Split(string(knownHostsContent), "\n") 134 | for _, line := range knownHostsLines { 135 | if strings.Split(line, " ")[0] == address { 136 | return strings.Join(strings.Split(line, " ")[1:], " ") 137 | } 138 | } 139 | return "" 140 | } 141 | 142 | func keyString(k ssh.PublicKey) string { 143 | return k.Type() + " " + base64.StdEncoding.EncodeToString(k.Marshal()) 144 | } 145 | 146 | func trustedHostKeyCallback(trustedKey string, address string, hostKeyMutex *sync.Mutex) ssh.HostKeyCallback { 147 | if trustedKey == "" { 148 | return func(_ string, _ net.Addr, k ssh.PublicKey) error { 149 | hostKeyMutex.Lock() 150 | defer hostKeyMutex.Unlock() 151 | // Re search for key to avoid duplicate operations 152 | if searchKeyFromAddress(address) != "" { 153 | return nil 154 | } 155 | newHostKeyInfo := address + " " + keyString(k) + "\n" 156 | if knownHostsContent, status := utils.ReadFile(config.KnownHostsPath); status { 157 | knownHostsContent = append(knownHostsContent, []byte(newHostKeyInfo)...) 158 | if utils.UpdateFile(config.KnownHostsPath, knownHostsContent, 0600) { 159 | utils.Logger.Infoln("First login, automatically add key to known_hosts") 160 | return nil 161 | } else { 162 | return fmt.Errorf("update known_hosts failed") 163 | } 164 | } else { 165 | return fmt.Errorf("read known_hosts failed") 166 | } 167 | } 168 | } 169 | 170 | return func(_ string, _ net.Addr, k ssh.PublicKey) error { 171 | ks := keyString(k) 172 | if trustedKey != ks { 173 | return fmt.Errorf("\n*[%s]* ssh-key verification: expected %q but got %q\n"+ 174 | "*[%s]* Server [%s] may have been impersonated. "+ 175 | "If you can confirm that the public key change of server [%s] "+ 176 | "is normal, please delete the entry for server [%s] in ~/.tryssh/known_hosts "+ 177 | "and try logging in again.", 178 | SSHKeyKeyword, trustedKey, ks, SSHKeyKeyword, address, address, address) 179 | } 180 | return nil 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /pkg/launcher/scp.go: -------------------------------------------------------------------------------- 1 | package launcher 2 | 3 | import ( 4 | "github.com/Driver-C/tryssh/pkg/utils" 5 | "github.com/cheggaaa/pb/v3" 6 | "github.com/pkg/sftp" 7 | "io" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | type ScpLauncher struct { 15 | SshConnector 16 | Src string 17 | Dest string 18 | Recursive bool 19 | } 20 | 21 | func (c *ScpLauncher) Launch() bool { 22 | sftpClient := c.createScpClient() 23 | if sftpClient == nil { 24 | return false 25 | } 26 | defer c.closeScpClient(sftpClient) 27 | 28 | // Replace ~ to the real home directory 29 | c.replaceHomeDirSymbol(sftpClient) 30 | 31 | switch { 32 | case strings.Contains(c.Src, c.Ip) && !c.Recursive: 33 | return c.download(c.Dest, strings.Split(c.Src, ":")[1], sftpClient) 34 | case strings.Contains(c.Src, c.Ip) && c.Recursive: 35 | return c.downloadDir(c.Dest, strings.Split(c.Src, ":")[1], sftpClient) 36 | case strings.Contains(c.Dest, c.Ip) && !c.Recursive: 37 | return c.upload(c.Src, strings.Split(c.Dest, ":")[1], sftpClient) 38 | case strings.Contains(c.Dest, c.Ip) && c.Recursive: 39 | return c.uploadDir(c.Src, strings.Split(c.Dest, ":")[1], sftpClient) 40 | } 41 | 42 | return false 43 | } 44 | 45 | func (c *ScpLauncher) replaceHomeDirSymbol(sftpClient *sftp.Client) { 46 | remoteHomeDir, err := sftpClient.Getwd() 47 | if err != nil { 48 | utils.Logger.Fatalf("Failed to get home directory: %v", err) 49 | } 50 | homeDirSymbol := "~" 51 | c.Src = strings.Replace(c.Src, homeDirSymbol, remoteHomeDir, -1) 52 | c.Dest = strings.Replace(c.Dest, homeDirSymbol, remoteHomeDir, -1) 53 | } 54 | 55 | func NewScpLaunchersByCombinations(combinations chan []interface{}, src string, dest string, 56 | recursive bool, sshTimeout time.Duration) (launchers []*ScpLauncher) { 57 | for com := range combinations { 58 | launchers = append(launchers, &ScpLauncher{ 59 | SshConnector: SshConnector{ 60 | Ip: com[0].(string), 61 | Port: com[1].(string), 62 | User: com[2].(string), 63 | Password: com[3].(string), 64 | Key: com[4].(string), 65 | SshTimeout: sshTimeout, 66 | }, 67 | Src: src, 68 | Dest: dest, 69 | Recursive: recursive, 70 | }) 71 | } 72 | return 73 | } 74 | 75 | func (c *ScpLauncher) createScpClient() (sftpClient *sftp.Client) { 76 | sshClient, errSsh := c.CreateConnection() 77 | if errSsh != nil { 78 | return 79 | } 80 | sftpClient, errScp := sftp.NewClient(sshClient, sftp.UseConcurrentWrites(true), 81 | sftp.UseConcurrentReads(true)) 82 | if errScp != nil { 83 | utils.Logger.Fatalln(errScp.Error()) 84 | } 85 | return 86 | } 87 | 88 | func (c *ScpLauncher) closeScpClient(sftpClient *sftp.Client) { 89 | err := sftpClient.Close() 90 | if err != nil { 91 | utils.Logger.Errorln(err.Error()) 92 | } 93 | } 94 | 95 | func (c *ScpLauncher) upload(local, remote string, sftpClient *sftp.Client) bool { 96 | localPathSegments := strings.Split(local, "/") 97 | localFileName := localPathSegments[len(localPathSegments)-1] 98 | // Openssh scp options rule imitation 99 | var remoteFileName string 100 | if strings.HasSuffix(remote, "/") { 101 | remoteFileName = localFileName 102 | } 103 | prefix := local + " " 104 | 105 | localFile, err := os.Open(local) 106 | if err != nil { 107 | utils.Logger.Fatalln(err.Error()) 108 | } 109 | defer func(localFile *os.File) { 110 | err := localFile.Close() 111 | if err != nil { 112 | utils.Logger.Errorln(err.Error()) 113 | } 114 | }(localFile) 115 | 116 | remoteFile, err := sftpClient.Create(sftp.Join(remote, remoteFileName)) 117 | if err != nil { 118 | utils.Logger.Fatalln(err.Error()) 119 | } 120 | defer func(remoteFile *sftp.File) { 121 | err := remoteFile.Close() 122 | if err != nil { 123 | utils.Logger.Errorln(err.Error()) 124 | } 125 | }(remoteFile) 126 | 127 | localFileInfo, err := localFile.Stat() 128 | if err != nil { 129 | utils.Logger.Errorln("Get local file stat failed: ", err) 130 | return false 131 | } 132 | localFileSize := localFileInfo.Size() 133 | localFilePerm := localFileInfo.Mode().Perm() 134 | // Sync file permission 135 | if err := remoteFile.Chmod(localFilePerm); err != nil { 136 | utils.Logger.Errorln("Sync file permission failed: ", err) 137 | return false 138 | } 139 | progressBar := pb.New64(localFileSize) 140 | progressBar.Set("prefix", prefix) 141 | barReader := progressBar.NewProxyReader(localFile) 142 | localReader := io.LimitReader(barReader, localFileSize) 143 | progressBar.Start() 144 | // Reader must be io.Reader, bytes.Reader or satisfy one of the following interfaces: 145 | // Len() int, Size() int64, Stat() (os.FileInfo, error). 146 | // Or the concurrent upload can not work. 147 | _, err = io.Copy(remoteFile, localReader) 148 | if err != nil { 149 | utils.Logger.Fatalln(err.Error()) 150 | } 151 | progressBar.Finish() 152 | return true 153 | } 154 | 155 | func (c *ScpLauncher) uploadDir(local, remote string, sftpClient *sftp.Client) bool { 156 | // Openssh scp options rule imitation 157 | if strings.HasSuffix(remote, "/") { 158 | remote = filepath.Join(remote, filepath.Base(local)) 159 | } 160 | 161 | // Create remote root directory 162 | if err := sftpClient.MkdirAll(remote); err != nil { 163 | utils.Logger.Errorln("Unable to create remote directory: ", err) 164 | return false 165 | } 166 | entries, err := os.ReadDir(local) 167 | if err != nil { 168 | utils.Logger.Errorln(err.Error()) 169 | return false 170 | } 171 | for _, entry := range entries { 172 | localPath := filepath.Join(local, entry.Name()) 173 | remotePath := filepath.Join(remote, entry.Name()) 174 | if entry.IsDir() { 175 | // Create remote directory 176 | if err := sftpClient.MkdirAll(remotePath); err != nil { 177 | utils.Logger.Errorln("Unable to create remote directory: ", err) 178 | return false 179 | } 180 | c.uploadDir(localPath, remotePath, sftpClient) 181 | } else { 182 | c.upload(localPath, remotePath, sftpClient) 183 | } 184 | } 185 | return true 186 | } 187 | 188 | func (c *ScpLauncher) download(local, remote string, sftpClient *sftp.Client) bool { 189 | remotePath := strings.Split(remote, "/") 190 | remoteFileName := remotePath[len(remotePath)-1] 191 | // Openssh scp options rule imitation 192 | var localFileName string 193 | if strings.HasSuffix(local, "/") { 194 | localFileName = remoteFileName 195 | } 196 | prefix := remote + " " 197 | 198 | remoteFile, err := sftpClient.Open(remote) 199 | if err != nil { 200 | utils.Logger.Fatalln(err.Error()) 201 | } 202 | defer func(remoteFile *sftp.File) { 203 | err := remoteFile.Close() 204 | if err != nil { 205 | utils.Logger.Errorln(err.Error()) 206 | } 207 | }(remoteFile) 208 | 209 | localFile, err := os.Create(sftp.Join(local, localFileName)) 210 | if err != nil { 211 | utils.Logger.Fatalln(err.Error()) 212 | } 213 | defer func(localFile *os.File) { 214 | err := localFile.Close() 215 | if err != nil { 216 | utils.Logger.Errorln(err.Error()) 217 | } 218 | }(localFile) 219 | 220 | remoteFileInfo, err := remoteFile.Stat() 221 | if err != nil { 222 | utils.Logger.Errorln("Get remote file stat failed: ", err) 223 | return false 224 | } 225 | remoteFilePerm := remoteFileInfo.Mode().Perm() 226 | // Sync file permission 227 | if err := localFile.Chmod(remoteFilePerm); err != nil { 228 | utils.Logger.Errorln("Sync file permission failed: ", err) 229 | return false 230 | } 231 | remoteFileSize := remoteFileInfo.Size() 232 | progressBar := pb.New64(remoteFileSize) 233 | progressBar.Set("prefix", prefix) 234 | barWriter := progressBar.NewProxyWriter(localFile) 235 | progressBar.Start() 236 | _, err = io.Copy(barWriter, remoteFile) 237 | if err != nil { 238 | utils.Logger.Fatalln(err.Error()) 239 | } 240 | progressBar.Finish() 241 | return true 242 | } 243 | 244 | func (c *ScpLauncher) downloadDir(local, remote string, sftpClient *sftp.Client) bool { 245 | // Openssh scp options rule imitation 246 | if strings.HasSuffix(local, "/") { 247 | local = filepath.Join(local, filepath.Base(remote)) 248 | } 249 | 250 | // Create local root directory 251 | if err := os.MkdirAll(local, 0755); err != nil { 252 | utils.Logger.Errorln("Unable to create local directory: ", err) 253 | return false 254 | } 255 | entries, err := sftpClient.ReadDir(remote) 256 | if err != nil { 257 | utils.Logger.Errorln(err.Error()) 258 | return false 259 | } 260 | for _, entry := range entries { 261 | localPath := filepath.Join(local, entry.Name()) 262 | remotePath := filepath.Join(remote, entry.Name()) 263 | if entry.IsDir() { 264 | // Create local directory 265 | if err := os.MkdirAll(localPath, 0755); err != nil { 266 | utils.Logger.Errorln("Unable to create local directory: ", err) 267 | return false 268 | } 269 | c.downloadDir(localPath, remotePath, sftpClient) 270 | } else { 271 | c.download(localPath, remotePath, sftpClient) 272 | } 273 | } 274 | return true 275 | } 276 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 | # tryssh 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/Driver-C/tryssh)](https://goreportcard.com/report/github.com/Driver-C/tryssh) 4 | 5 | [English](README.md) | 简体中文 6 | 7 | `tryssh`是一个具有密码猜测功能的命令行SSH终端工具。 8 | 9 | 它可以使用SSH协议交互登录服务器,或者将本地文件上传到服务器或将远程文件下载到本地。 10 | 11 | 当然,它也可以管理用于尝试登陆服务器的用户名、端口号、密码以及已经成功登陆服务器的缓存信息。 12 | 13 | > 注意!不要将`tryssh`用于生产场景! 14 | 15 | ## 我为什么需要 tryssh ? 16 | 17 | * 我只喜欢使用命令行工具,不想使用图形化工具 18 | * 我有很多登陆信息相似的服务器,但是我不想每次登陆都输入登陆信息 19 | * 我经常跨操作系统使用SSH终端,但是没有找到让我在多种操作系统上使用习惯不变的工具 20 | * `tryssh` 我没用过,看着还不错,想试试 21 | 22 | ## 当前开发状态 23 | 24 | 目前`tryssh`处于功能完善阶段,基本功能已有,但是在功能的细节上做得不好还需要改进,比如安全。 25 | 26 | 目前仅有 *Driver-C* 一人参与开发,而且需要利用业务时间来完成,所以开发进度不会很快。 27 | 28 | 如果遇到任何使用问题,任何建议请提交`issue`,会尽快回复。 29 | 30 | 目前项目仅保留`master`分支用于发布稳定版本,`tag`也从master分支创建。 31 | 32 | ## 待做清单 33 | 34 | 排名不区分优先级,以下内容在完成后删除对应条目 35 | 36 | 1. 传输文件支持通配符 37 | 2. 完成单元测试代码 38 | 3. 安全相关功能,配置文件加密、隐藏明文显示的敏感信息、密码输入应改为交互式等 39 | 40 | ## 快速开始 41 | 42 | ```bash 43 | # 创建一个名为 testuser 的备选用户 44 | tryssh create users testuser 45 | 46 | # 创建备选端口号 22 47 | tryssh create ports 22 48 | 49 | # 创建一个备选密码 50 | tryssh create passwords 123456 51 | 52 | # 用以上创建的信息尝试登陆 192.168.1.1 53 | tryssh ssh 192.168.1.1 54 | ``` 55 | 56 | ## 怎么查看其他帮助 57 | 58 | 在`tryssh`的帮助信息中已经写好了所有子命令的使用帮助,可以通过下列命令查看 59 | 60 | ```bash 61 | tryssh -h 62 | 63 | # 查看子命令 ssh 的帮助 64 | tryssh ssh -h 65 | ``` 66 | 67 | ## 功能详解 68 | 69 | ``` 70 | $ tryssh -h 71 | command line ssh terminal tool. 72 | 73 | Usage: 74 | tryssh [command] 75 | 76 | Available Commands: 77 | alias Set, unset, and list aliases, aliases can be used to log in to servers 78 | create Create alternative username, port number, password, and login cache information 79 | delete Delete alternative username, port number, password, and login cache information 80 | get Get alternative username, port number, password, and login cache information 81 | help Help about any command 82 | prune Check if all current caches are available and clear the ones that are not available 83 | scp Upload/Download file to/from the server through SSH protocol 84 | ssh Connect to the server through SSH protocol 85 | version Print the client version information for the current context 86 | 87 | Flags: 88 | -h, --help help for tryssh 89 | 90 | Use "tryssh [command] --help" for more information about a command. 91 | ``` 92 | 93 | ### create 命令 94 | 95 | tryssh 的`create`命令用于创建用来猜密码登陆的各类配置,比如用户名、端口号和密码,也可以直接创建已知用户名、端口号和密码的缓存。 96 | 97 | #### create 帮助信息 98 | ``` 99 | $ tryssh create -h 100 | Create alternative username, port number, password, and login cache information 101 | 102 | Usage: 103 | tryssh create [command] 104 | 105 | Available Commands: 106 | caches Create an alternative cache 107 | keys Create a alternative key file path 108 | passwords Create an alternative password 109 | ports Create an alternative port 110 | users Create an alternative username 111 | 112 | Flags: 113 | -h, --help help for create 114 | 115 | Use "tryssh create [command] --help" for more information about a command. 116 | ``` 117 | 118 | #### create 使用举例 119 | 120 | ``` 121 | # 创建一个名为 testuser 的备选用户 122 | tryssh create users testuser 123 | 124 | # 创建备选端口号 22 125 | tryssh create ports 22 126 | 127 | # 创建一个备选密码 128 | tryssh create passwords 123456 129 | ``` 130 | 131 | ### delete 命令 132 | 133 | tryssh 的`delete`命令用于删除用来猜密码登陆的各类配置,比如用户名、端口号和密码,也可以直接删除缓存。 134 | 135 | #### delete 帮助信息 136 | 137 | ``` 138 | $ tryssh delete -h 139 | Delete alternative username, port number, password, and login cache information 140 | 141 | Usage: 142 | tryssh delete [command] 143 | 144 | Available Commands: 145 | caches Delete an alternative cache 146 | passwords Delete an alternative password 147 | ports Delete an alternative port 148 | users Delete an alternative username 149 | 150 | Flags: 151 | -h, --help help for delete 152 | 153 | Use "tryssh delete [command] --help" for more information about a command. 154 | ``` 155 | 156 | #### delete 使用举例 157 | 158 | ``` 159 | # 删除一个名为 testuser 的备选用户 160 | tryssh delete users testuser 161 | 162 | # 删除备选端口号 22 163 | tryssh delete ports 22 164 | 165 | # 删除一个备选密码 166 | tryssh delete passwords 123456 167 | 168 | # 删除服务器192.168.1.1的登陆缓存 169 | tryssh delete caches 192.168.1.1 170 | ``` 171 | 172 | ### get 命令 173 | 174 | tryssh 的`get`命令用于查看用来猜密码登陆的各类配置,比如用户名、端口号、密码以及登陆缓存。 175 | 176 | #### get 帮助信息 177 | 178 | ``` 179 | $ tryssh get -h 180 | Get alternative username, port number, password, and login cache information 181 | 182 | Usage: 183 | tryssh get [command] 184 | 185 | Available Commands: 186 | caches Get alternative caches by ipAddress 187 | keys Delete a alternative key file path 188 | passwords Get alternative passwords 189 | ports Get alternative ports 190 | users Get alternative usernames 191 | 192 | Flags: 193 | -h, --help help for get 194 | 195 | Use "tryssh get [command] --help" for more information about a command. 196 | ``` 197 | 198 | #### get 使用举例 199 | 200 | ``` 201 | # 查看用于猜密码的候选用户 202 | tryssh get users 203 | 204 | # 查看用于猜密码的候选端口号 205 | tryssh get ports 206 | 207 | # 查看当前已有的登陆缓存 208 | tryssh get caches 209 | ``` 210 | 211 | ### prune 命令 212 | 213 | tryssh的`prune`命令用于测试当前已有缓存是否依然可用,如果不可用可以选择执行删除缓存,也可以不询问直接删除缓存。 214 | 215 | #### prune 帮助信息 216 | 217 | ``` 218 | $ tryssh prune -h 219 | Check if all current caches are available and clear the ones that are not available 220 | 221 | Usage: 222 | tryssh prune [flags] 223 | 224 | Flags: 225 | -a, --auto Automatically perform concurrent cache optimization without asking for confirmation to delete 226 | -c, --concurrency int Number of multiple requests to perform at a time (default 8) 227 | -h, --help help for prune 228 | -t, --timeout duration SSH timeout when attempting to log in. It can be "1s" or "1m" or other duration (default 2s) 229 | ``` 230 | 231 | #### prune 使用举例 232 | 233 | ``` 234 | # 交互式进行缓存可用性测试 235 | tryssh prune 236 | 237 | # 非交互进行缓存可用性测试 238 | tryssh prune -a 239 | 240 | # 非交互进行缓存可用性测试,同时设置并发数为10(默认为8),连接超时时间为5秒(默认为2秒) 241 | tryssh prune -c 10 -t 5s -a 242 | ``` 243 | 244 | > 交互式模式下设置并发数是无效的 245 | 246 | ### alias 命令 247 | 248 | tryssh 的`alias`命令是用于给 *已有* 的缓存设置别名用的,方便在登陆或者传输文件时直接使用别名来操作 249 | 250 | #### alias 帮助信息 251 | 252 | ``` 253 | $ tryssh alias -h 254 | Set, unset, and list aliases, aliases can be used to log in to servers 255 | 256 | Usage: 257 | tryssh alias [command] 258 | 259 | Available Commands: 260 | list List all alias 261 | set Set an alias for the specified server address 262 | unset Unset the alias 263 | 264 | Flags: 265 | -h, --help help for alias 266 | 267 | Use "tryssh alias [command] --help" for more information about a command. 268 | ``` 269 | 270 | #### alias 使用举例 271 | 272 | ``` 273 | # 查看当前所有别名 274 | tryssh alias list 275 | 276 | # 给192.168.1.1服务器设置一个名为"host1"的别名 277 | tryssh alias set host1 -t 192.168.1.1 278 | 279 | # 取消名为"host1"的别名 280 | tryssh alias unset host1 281 | ``` 282 | 283 | ### ssh 命令 284 | 285 | tryssh 的`ssh`命令用于猜密码登陆服务器,在成功获取正确登陆信息后会缓存这些信息以便下次直接使用缓存登陆,不用重新猜密码。 286 | 287 | #### ssh 帮助信息 288 | 289 | ``` 290 | chenjingyu@MacBook ~ % tryssh ssh -h 291 | Connect to the server through SSH protocol 292 | 293 | Usage: 294 | tryssh ssh [flags] 295 | 296 | Flags: 297 | -c, --concurrency int Number of multiple requests to perform at a time (default 8) 298 | -h, --help help for ssh 299 | -t, --timeout duration SSH timeout when attempting to log in. It can be "1s" or "1m" or other duration (default 1s) 300 | -u, --user string Specify a username to attempt to login to the server, 301 | if the specified username does not exist, try logging in using that username 302 | ``` 303 | 304 | #### ssh 使用举例 305 | 306 | ``` 307 | # 登陆192.168.1.1服务器,如果没有缓存则尝试猜密码登陆 308 | tryssh ssh 192.168.1.1 309 | 310 | # 登陆别名为host1的服务器 311 | tryssh ssh host1 312 | 313 | # 登陆192.168.1.1服务器,如果没有缓存则尝试猜密码登陆,同时设置并发数为20,超时时间为500毫秒,指定登陆的用户为root 314 | tryssh ssh 192.168.1.1 -c 20 -t 500ms -u root 315 | ``` 316 | 317 | ### scp 命令 318 | 319 | tryssh 的`scp`命令用于上传或者下载文件或者目录,`scp`命令支持使用别名 320 | 321 | #### scp 帮助信息 322 | 323 | ``` 324 | chenjingyu@MacBook ~ % tryssh scp -h 325 | Upload/Download file to/from the server through SSH protocol 326 | 327 | Usage: 328 | tryssh scp [flags] 329 | 330 | Examples: 331 | # Download test.txt file from 192.168.1.1 and place it under ./ 332 | tryssh scp 192.168.1.1:/root/test.txt ./ 333 | # Upload test.txt file to 192.168.1.1 and place it under /root/ 334 | tryssh scp ./test.txt 192.168.1.1:/root/ 335 | # Download test.txt file from 192.168.1.1 and rename it to test2.txt and place it under ./ 336 | tryssh scp 192.168.1.1:/root/test.txt ./test2.txt 337 | 338 | # Download testDir directory from 192.168.1.1 and place it under ~/Downloads/ 339 | tryssh scp -r 192.168.1.1:/root/testDir ~/Downloads/ 340 | # Upload testDir directory to 192.168.1.1 and rename it to testDir2 and place it under /root/ 341 | tryssh scp -r ~/Downloads/testDir 192.168.1.1:/root/testDir2 342 | 343 | Flags: 344 | -c, --concurrency int Number of multiple requests to perform at a time (default 8) 345 | -h, --help help for scp 346 | -r, --recursive Recursively copy entire directories 347 | -t, --timeout duration SSH timeout when attempting to log in. It can be "1s" or "1m" or other duration (default 1s) 348 | -u, --user string Specify a username to attempt to login to the server, 349 | if the specified username does not exist, try logging in using that username 350 | ``` 351 | 352 | #### scp 使用举例 353 | 354 | > scp的使用例子在帮助信息里已经有阐述,下面只是做翻译 355 | 356 | ``` 357 | # 从192.168.1.1服务器上下载test.txt文件放到本地的./目录 358 | tryssh scp 192.168.1.1:/root/test.txt ./ 359 | 360 | # 从本地上传test.txt文件到192.168.1.1的/root/目录下 361 | tryssh scp ./test.txt 192.168.1.1:/root/ 362 | 363 | # 从192.168.1.1服务器上下载test.txt文件到本地./目录下并改名为test2.txt 364 | tryssh scp 192.168.1.1:/root/test.txt ./test2.txt 365 | 366 | # 从192.168.1.1服务器上下载testDir目录到本地的~/Downloads/下 367 | tryssh scp -r 192.168.1.1:/root/testDir ~/Downloads/ 368 | 369 | # 上传本地的testDir目录到192.168.1.1服务器/root/下并改名为testDir2 370 | tryssh scp -r ~/Downloads/testDir 192.168.1.1:/root/testDir2 371 | ``` 372 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= 2 | github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= 3 | github.com/cheggaaa/pb/v3 v3.1.7 h1:2FsIW307kt7A/rz/ZI2lvPO+v3wKazzE4K/0LtTWsOI= 4 | github.com/cheggaaa/pb/v3 v3.1.7/go.mod h1:/Ji89zfVPeC/u5j8ukD0MBPHt2bzTYp74lQ7KlgFWTQ= 5 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 10 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 11 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 12 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 13 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 14 | github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= 15 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 16 | github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= 17 | github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 18 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 19 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 20 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 21 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 22 | github.com/pkg/sftp v1.13.9 h1:4NGkvGudBL7GteO3m6qnaQ4pC0Kvf0onSVc9gR3EWBw= 23 | github.com/pkg/sftp v1.13.9/go.mod h1:OBN7bVXdstkFFN/gdnHPUb5TE8eb8G1Rp9wCItqjkkA= 24 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 25 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 26 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 27 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 28 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 29 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 30 | github.com/schwarmco/go-cartesian-product v0.0.0-20230921023625-e02d1c150053 h1:h7EwPM2KjupG0zVAG+EYxbR2cHnbiP1d4DTAZ+G09LY= 31 | github.com/schwarmco/go-cartesian-product v0.0.0-20230921023625-e02d1c150053/go.mod h1:/TRiIlxvQQAtfnBXEqqbnYBYPmE6XT5iZxSx+hJ9zGw= 32 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 33 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 34 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 35 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 36 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 37 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 38 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 39 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 40 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 41 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 42 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 43 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 44 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 45 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 46 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 47 | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 48 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 49 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 50 | golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 51 | golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= 52 | golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= 53 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 54 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 55 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 56 | golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 57 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 58 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 59 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 60 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 61 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 62 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 63 | golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= 64 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 65 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 66 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 67 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 68 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 69 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 70 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 71 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 72 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 73 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 74 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 75 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 76 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 77 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 78 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 79 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 80 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 81 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 82 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 83 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 84 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 85 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 86 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 87 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 88 | golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= 89 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 90 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 91 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 92 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 93 | golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= 94 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 95 | golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= 96 | golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= 97 | golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= 98 | golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= 99 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 100 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 101 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 102 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 103 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 104 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 105 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 106 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 107 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 108 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 109 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 110 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 111 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 112 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 113 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 114 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 115 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 116 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 117 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 118 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 119 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 120 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tryssh 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/Driver-C/tryssh)](https://goreportcard.com/report/github.com/Driver-C/tryssh) 4 | 5 | English | [简体中文](README_zh.md) 6 | 7 | `tryssh` is a command line SSH terminal tool with password guessing function. 8 | 9 | It can use the SSH protocol to interactively log in to the server, or upload local files to the server or download remote files to the local location. 10 | 11 | Of course, it can also manage the usernames, port numbers, passwords, and cached information of successfully logged-in servers for login attempts. 12 | 13 | > Attention! Do not use `tryssh` in a production environment! 14 | 15 | ## Why do I need "tryssh"? 16 | 17 | * I prefer command-line tools and do not want to use graphical tools 18 | * I have many servers with similar login information, but I don't want to input login details every time I log in 19 | * I frequently use SSH terminal across multiple operating systems, but I haven't found a tool that allows me to maintain the same workflow across different OSes 20 | * I haven't used `tryssh` before, but it looks good, and I want to give it a try 21 | 22 | ## Current development status 23 | 24 | Currently, `tryssh` is in the stage of feature completion. The core functionalities are already implemented, but there is room for improvement in terms of details, particularly in areas such as security. 25 | 26 | Currently, only one person *Driver-C* is involved in the development, and the progress is limited by the need to allocate time from other work responsibilities. Therefore, the development progress is not expected to be fast. 27 | 28 | If you encounter any usage issues or have any suggestions, please submit an `issue`. We will respond as soon as possible. 29 | 30 | Currently, the project only maintains the `master` branch for releasing stable versions, and `tags` are created from the `master` branch as well. 31 | 32 | ## TODO list 33 | 34 | Rankings do not differentiate priority levels. Delete the corresponding entry after completion of the following content. 35 | 36 | 1. File transfer supports wildcards 37 | 2. Completing unit test code 38 | 3. Security-related features, such as encrypting configuration files, hiding sensitive information from plain text display, and switching to interactive password input 39 | 40 | ## Quick Start 41 | 42 | ```bash 43 | # Create an alternative user named "testuser" 44 | tryssh create users testuser 45 | 46 | # Create an alternative port number 22 47 | tryssh create ports 22 48 | 49 | # Create an alternative password 50 | tryssh create passwords 123456 51 | 52 | # Attempt to log in to 192.168.1.1 using the information created above 53 | tryssh ssh 192.168.1.1 54 | ``` 55 | 56 | ## How to view other help documentation? 57 | 58 | All usage help for the subcommands has been documented in the `tryssh` help information. You can view it by using the following command: 59 | 60 | ```bash 61 | tryssh -h 62 | 63 | # View the help documentation for the subcommand "ssh" 64 | tryssh ssh -h 65 | ``` 66 | 67 | ## Detailed Function Explanation 68 | 69 | ``` 70 | $ tryssh -h 71 | command line ssh terminal tool. 72 | 73 | Usage: 74 | tryssh [command] 75 | 76 | Available Commands: 77 | alias Set, unset, and list aliases, aliases can be used to log in to servers 78 | create Create alternative username, port number, password, and login cache information 79 | delete Delete alternative username, port number, password, and login cache information 80 | get Get alternative username, port number, password, and login cache information 81 | help Help about any command 82 | prune Check if all current caches are available and clear the ones that are not available 83 | scp Upload/Download file to/from the server through SSH protocol 84 | ssh Connect to the server through SSH protocol 85 | version Print the client version information for the current context 86 | 87 | Flags: 88 | -h, --help help for tryssh 89 | 90 | Use "tryssh [command] --help" for more information about a command. 91 | ``` 92 | 93 | ### Command: create 94 | 95 | The `"create"` command of `tryssh` is used to create various configurations for password guessing login, such as usernames, port numbers, and passwords. It can also directly create caches with known usernames, port numbers, and passwords. 96 | 97 | #### Help information 98 | 99 | ``` 100 | $ tryssh create -h 101 | Create alternative username, port number, password, and login cache information 102 | 103 | Usage: 104 | tryssh create [command] 105 | 106 | Available Commands: 107 | caches Create an alternative cache 108 | keys Create a alternative key file path 109 | passwords Create an alternative password 110 | ports Create an alternative port 111 | users Create an alternative username 112 | 113 | Flags: 114 | -h, --help help for create 115 | 116 | Use "tryssh create [command] --help" for more information about a command. 117 | ``` 118 | 119 | #### Example 120 | 121 | ``` 122 | # Create an alternative user named testuser 123 | tryssh create users testuser 124 | 125 | # Create an alternative port: 22 126 | tryssh create ports 22 127 | 128 | # Create an alternative passwords: 123456 129 | tryssh create passwords 123456 130 | ``` 131 | 132 | ### Command: delete 133 | 134 | The `"delete"` command of `tryssh` is used to delete various configurations for password guessing login, such as usernames, port numbers, and passwords. It can also directly delete caches. 135 | 136 | #### Help information 137 | 138 | ``` 139 | $ tryssh delete -h 140 | Delete alternative username, port number, password, and login cache information 141 | 142 | Usage: 143 | tryssh delete [command] 144 | 145 | Available Commands: 146 | caches Delete an alternative cache 147 | keys Delete a alternative key file path 148 | passwords Delete an alternative password 149 | ports Delete an alternative port 150 | users Delete an alternative username 151 | 152 | Flags: 153 | -h, --help help for delete 154 | 155 | Use "tryssh delete [command] --help" for more information about a command. 156 | ``` 157 | 158 | #### Example 159 | 160 | ``` 161 | # Delete an alternative user named testuser 162 | tryssh delete users testuser 163 | 164 | # Delete an alternative port: 22 165 | tryssh delete ports 22 166 | 167 | # Delete an alternative passwords: 123456 168 | tryssh delete passwords 123456 169 | 170 | # Delete the cache information about 192.168.1.1 171 | tryssh delete caches 192.168.1.1 172 | ``` 173 | 174 | ### Command: get 175 | 176 | The `"get"` command of `tryssh` is used to view various configurations for password guessing login, such as usernames, port numbers, passwords, and login caches. 177 | 178 | #### Help information 179 | 180 | ``` 181 | $ tryssh get -h 182 | Get alternative username, port number, password, and login cache information 183 | 184 | Usage: 185 | tryssh get [command] 186 | 187 | Available Commands: 188 | caches Get alternative caches by ipAddress 189 | passwords Get alternative passwords 190 | ports Get alternative ports 191 | users Get alternative usernames 192 | 193 | Flags: 194 | -h, --help help for get 195 | 196 | Use "tryssh get [command] --help" for more information about a command. 197 | ``` 198 | 199 | #### Example 200 | 201 | ``` 202 | # View candidate users for password guessing 203 | tryssh get users 204 | 205 | # View candidate ports for password guessing 206 | tryssh get ports 207 | 208 | # View the currently existing login caches 209 | tryssh get caches 210 | ``` 211 | 212 | ### Command: prune 213 | 214 | The `"prune"` command of `tryssh` is used to test whether the existing caches are still usable. If they are not, you can choose to delete the cache, or directly delete it without confirmation. 215 | 216 | #### Help information 217 | 218 | ``` 219 | $ tryssh prune -h 220 | Check if all current caches are available and clear the ones that are not available 221 | 222 | Usage: 223 | tryssh prune [flags] 224 | 225 | Flags: 226 | -a, --auto Automatically perform concurrent cache optimization without asking for confirmation to delete 227 | -c, --concurrency int Number of multiple requests to perform at a time (default 8) 228 | -h, --help help for prune 229 | -t, --timeout duration SSH timeout when attempting to log in. It can be "1s" or "1m" or other duration (default 2s) 230 | ``` 231 | 232 | #### Example 233 | 234 | ``` 235 | # Interactively conduct cache availability testing 236 | tryssh prune 237 | 238 | # Conduct non-interactive cache availability testing 239 | tryssh prune -a 240 | 241 | # Conduct non-interactive cache availability testing, while setting the concurrency to 10 (default is 8) and the connection timeout to 5 seconds (default is 2 seconds). 242 | tryssh prune -c 10 -t 5s -a 243 | ``` 244 | 245 | > The setting for concurrency is invalid in interactive mode. 246 | 247 | ### Command: alias 248 | 249 | The `"alias"` command of `tryssh` is used to assign aliases to existing caches, making it convenient to use these aliases directly for login or file transfer operations. 250 | 251 | #### Help information 252 | 253 | ``` 254 | $ tryssh alias -h 255 | Set, unset, and list aliases, aliases can be used to log in to servers 256 | 257 | Usage: 258 | tryssh alias [command] 259 | 260 | Available Commands: 261 | list List all alias 262 | set Set an alias for the specified server address 263 | unset Unset the alias 264 | 265 | Flags: 266 | -h, --help help for alias 267 | 268 | Use "tryssh alias [command] --help" for more information about a command. 269 | ``` 270 | 271 | #### Example 272 | 273 | ``` 274 | # View all current aliases 275 | tryssh alias list 276 | 277 | # Set an alias named 'host1' for the server with the IP address 192.168.1.1 278 | tryssh alias set host1 -t 192.168.1.1 279 | 280 | # Remove the alias named 'host1' 281 | tryssh alias unset host1 282 | ``` 283 | 284 | ### Command: ssh 285 | 286 | The `"ssh"` command of `tryssh` is used for password guessing login to a server. Upon successfully obtaining the correct login information, it will cache these details for direct login in future attempts, without the need to guess the password again. 287 | 288 | #### Help information 289 | 290 | ``` 291 | chenjingyu@MacBook ~ % tryssh ssh -h 292 | Connect to the server through SSH protocol 293 | 294 | Usage: 295 | tryssh ssh [flags] 296 | 297 | Flags: 298 | -c, --concurrency int Number of multiple requests to perform at a time (default 8) 299 | -h, --help help for ssh 300 | -t, --timeout duration SSH timeout when attempting to log in. It can be "1s" or "1m" or other duration (default 1s) 301 | -u, --user string Specify a username to attempt to login to the server, 302 | if the specified username does not exist, try logging in using that username 303 | ``` 304 | 305 | #### Example 306 | 307 | ``` 308 | # Login to the server at 192.168.1.1. If there is no cache available, attempt to guess the password for login 309 | tryssh ssh 192.168.1.1 310 | 311 | # Login to the server with the alias 'host1' 312 | tryssh ssh host1 313 | 314 | # Login to the server at 192.168.1.1. If there is no cache available, attempt to guess the password for login. Set the concurrency to 20, timeout to 500 milliseconds, and specify the user as 'root'. 315 | tryssh ssh 192.168.1.1 -c 20 -t 500ms -u root 316 | ``` 317 | 318 | ### Command: scp 319 | 320 | The `"scp"` command of `tryssh` is used to upload or download files or directories. The scp command supports the use of aliases. 321 | 322 | #### Help information 323 | 324 | ``` 325 | chenjingyu@MacBook ~ % tryssh scp -h 326 | Upload/Download file to/from the server through SSH protocol 327 | 328 | Usage: 329 | tryssh scp [flags] 330 | 331 | Examples: 332 | # Download test.txt file from 192.168.1.1 and place it under ./ 333 | tryssh scp 192.168.1.1:/root/test.txt ./ 334 | # Upload test.txt file to 192.168.1.1 and place it under /root/ 335 | tryssh scp ./test.txt 192.168.1.1:/root/ 336 | # Download test.txt file from 192.168.1.1 and rename it to test2.txt and place it under ./ 337 | tryssh scp 192.168.1.1:/root/test.txt ./test2.txt 338 | 339 | # Download testDir directory from 192.168.1.1 and place it under ~/Downloads/ 340 | tryssh scp -r 192.168.1.1:/root/testDir ~/Downloads/ 341 | # Upload testDir directory to 192.168.1.1 and rename it to testDir2 and place it under /root/ 342 | tryssh scp -r ~/Downloads/testDir 192.168.1.1:/root/testDir2 343 | 344 | Flags: 345 | -c, --concurrency int Number of multiple requests to perform at a time (default 8) 346 | -h, --help help for scp 347 | -r, --recursive Recursively copy entire directories 348 | -t, --timeout duration SSH timeout when attempting to log in. It can be "1s" or "1m" or other duration (default 1s) 349 | -u, --user string Specify a username to attempt to login to the server, 350 | if the specified username does not exist, try logging in using that username 351 | ``` 352 | 353 | #### Example 354 | 355 | > Same as the information in the help section. 356 | 357 | ``` 358 | # Download test.txt file from 192.168.1.1 and place it under ./ 359 | tryssh scp 192.168.1.1:/root/test.txt ./ 360 | 361 | # Upload test.txt file to 192.168.1.1 and place it under /root/ 362 | tryssh scp ./test.txt 192.168.1.1:/root/ 363 | 364 | # Download test.txt file from 192.168.1.1 and rename it to test2.txt and place it under ./ 365 | tryssh scp 192.168.1.1:/root/test.txt ./test2.txt 366 | 367 | # Download testDir directory from 192.168.1.1 and place it under ~/Downloads/ 368 | tryssh scp -r 192.168.1.1:/root/testDir ~/Downloads/ 369 | 370 | # Upload testDir directory to 192.168.1.1 and rename it to testDir2 and place it under /root/ 371 | tryssh scp -r ~/Downloads/testDir 192.168.1.1:/root/testDir2 372 | ``` 373 | --------------------------------------------------------------------------------