From 65626536e37b403f00bd5bb83c204236de0fa827 Mon Sep 17 00:00:00 2001 From: Miguel Mota Date: Tue, 6 Apr 2021 01:11:34 -0700 Subject: [PATCH] Add SSH server user config type flag --- cmd/commands/server.go | 4 +++ docs/content/faq.md | 14 +++++++++ docs/content/install.md | 6 ++++ docs/content/ssh.md | 47 ++++++++++++++++++++++++++---- pkg/ssh/server.go | 64 ++++++++++++++++++++++++++++++++++------- 5 files changed, 119 insertions(+), 16 deletions(-) diff --git a/cmd/commands/server.go b/cmd/commands/server.go index fbd8ce3..ecd281d 100644 --- a/cmd/commands/server.go +++ b/cmd/commands/server.go @@ -4,6 +4,7 @@ package cmd import ( "fmt" + "strings" "time" cssh "github.com/miguelmota/cointop/pkg/ssh" @@ -19,6 +20,7 @@ func ServerCmd() *cobra.Command { var maxSessions uint = 0 var executableBinary string = "cointop" var hostKeyFile string = cssh.DefaultHostKeyFile + var userConfigType string = cssh.UserConfigTypePublicKey serverCmd := &cobra.Command{ Use: "server", @@ -33,6 +35,7 @@ func ServerCmd() *cobra.Command { MaxSessions: maxSessions, ExecutableBinary: executableBinary, HostKeyFile: hostKeyFile, + UserConfigType: userConfigType, }) fmt.Printf("Running SSH server on port %v\n", port) @@ -47,6 +50,7 @@ func ServerCmd() *cobra.Command { serverCmd.Flags().UintVarP(&maxSessions, "max-sessions", "", maxSessions, "Max number of sessions allowed. Default is 0 for unlimited.") serverCmd.Flags().StringVarP(&executableBinary, "binary", "b", executableBinary, "Executable binary path") serverCmd.Flags().StringVarP(&hostKeyFile, "host-key-file", "k", hostKeyFile, "Host key file") + serverCmd.Flags().StringVarP(&userConfigType, "user-config-type", "", userConfigType, fmt.Sprintf("User config type. Options are: %s", strings.Join(cssh.UserConfigTypes, ","))) return serverCmd } diff --git a/docs/content/faq.md b/docs/content/faq.md index d706e6e..7f6608d 100644 --- a/docs/content/faq.md +++ b/docs/content/faq.md @@ -400,6 +400,20 @@ draft: false The public SSH instance is for demo use or short-term usage and will disconnect users after an idle timeout to allow other users to try it out. +## How do I fix the error `SSH key is required to start server` when trying to run SSH server? + + Generate the SSH key if the host machine doesn't have one: + + ```bash + $ ssh-keygen + ``` + + Make sure the host key flag points to the location of the SSH key: + + ```bash + cointop server -k ~/.ssh/id_rsa [...] + ``` + ## Why doesn't the version number work when I install with `go get`? The version number is read from the git tag during the build process but this requires the `GO111MODULE` environment variable to be set in order for Go to read the build information: diff --git a/docs/content/install.md b/docs/content/install.md index b73823b..bb6845e 100644 --- a/docs/content/install.md +++ b/docs/content/install.md @@ -189,6 +189,12 @@ cointop is available on [Docker Hub](https://hub.docker.com/r/cointop/cointop). docker run -it cointop/cointop ``` +Note: the config is under `/root/.config/cointop` in container, so attach a volume to make it persistent in host: + +```bash +docker run -v ~/.cache/cointop:/root/.config/cointop -it cointop/cointop +``` + ## Binaries You can find pre-built binaries on the [releases](https://github.com/miguelmota/cointop/releases) page. diff --git a/docs/content/ssh.md b/docs/content/ssh.md index f837ca4..108a4a2 100644 --- a/docs/content/ssh.md +++ b/docs/content/ssh.md @@ -5,32 +5,69 @@ draft: false --- # SSH Server +The SSH server requires that the host has SSH keys, so generate SSH keys if not already: + +```bash +$ ssh-keygen +``` + +Check keys were generated: + +```bash +$ ls ~/.ssh +id_rsa id_rsa.pub +``` + Run SSH server: ```bash cointop server -p 2222 ``` +If the host SSH keys live elsewhere, specify the location: + +```bash +cointop server -p 2222 -k ~/.ssh/some-dir/id_rsa +``` + SSH into server to see cointop: ```bash ssh localhost -p 2222 ``` -SSH demo: +The cointop SSH server will use the client's public SSH key as the identifier for persistent config by default. You may change it to use the username instead: ```bash -ssh cointop.sh +cointop server -p 2222 --user-config-type=username ``` -Passing arguments to SSH server: +SSH'ing into server with same username will use the same respective config now: ```bash -ssh cointop.sh -t cointop --colorscheme synthwave +ssh alice@localhost -p 2222 ``` -Using docker to run SSH server: +Pass arguments to cointop on SSH server by using SSH `-t` flag followed by cointop command and arguments. For example: + +```bash +ssh localhost -p 2222 -t cointop --colorscheme synthwave +``` + +## Using docker to run SSH server: ```bash docker run -p 2222:22 -v ~/.ssh:/keys --entrypoint cointop -it cointop/cointop server -k /keys/id_rsa ``` + +cointop server writes the client config to `/tmp/cointop_config` within the container, so to make it persistent in host attach a volume. The following example will to write the cached config to `~/.cache/cointop` on the host: + +```bash +docker run -p 2222:22 -v ~/.ssh:/keys -v ~/.cache/cointop:/tmp/cointop_config --entrypoint cointop -it cointop/cointop server -k /keys/id_rsa +``` + +## SSH demo + +```bash +ssh cointop.sh +``` diff --git a/pkg/ssh/server.go b/pkg/ssh/server.go index 42f06a6..fa06699 100644 --- a/pkg/ssh/server.go +++ b/pkg/ssh/server.go @@ -11,6 +11,7 @@ import ( "io/ioutil" "os" "os/exec" + "strings" "syscall" "time" "unsafe" @@ -24,6 +25,17 @@ import ( // DefaultHostKeyFile is default SSH key path var DefaultHostKeyFile = "~/.ssh/id_rsa" +// UserConfigTypePublicKey is constant +var UserConfigTypePublicKey = "public-key" + +// UserConfigTypeUsername is constant +var UserConfigTypeUsername = "username" + +// UserConfigTypeNone is constant +var UserConfigTypeNone = "none" + +var UserConfigTypes = []string{UserConfigTypePublicKey, UserConfigTypeUsername, UserConfigTypeNone} + // Config is config struct type Config struct { Port uint @@ -33,6 +45,7 @@ type Config struct { ExecutableBinary string HostKeyFile string MaxSessions uint + UserConfigType string } // Server is server struct @@ -46,6 +59,7 @@ type Server struct { hostKeyFile string maxSessions uint sessionCount uint + userConfigType string } // NewServer returns a new server instance @@ -54,7 +68,11 @@ func NewServer(config *Config) *Server { if config.HostKeyFile != "" { hostKeyFile = config.HostKeyFile } - + userConfigType := config.UserConfigType + if userConfigType == "" { + userConfigType = UserConfigTypePublicKey + } + validateUserConfigType(userConfigType) hostKeyFile = pathutil.NormalizePath(hostKeyFile) return &Server{ port: config.Port, @@ -64,6 +82,7 @@ func NewServer(config *Config) *Server { executableBinary: config.ExecutableBinary, hostKeyFile: hostKeyFile, maxSessions: config.MaxSessions, + userConfigType: userConfigType, } } @@ -95,18 +114,32 @@ func (s *Server) ListenAndServe() error { } configDir := "" - pubKey := sshSession.PublicKey() - if pubKey != nil { - pubBytes := pubKey.Marshal() - if len(pubBytes) > 0 { - hash := sha256.Sum256(pubBytes) - configDir = fmt.Sprintf("/tmp/cointop_config/%x", hash) - err := os.MkdirAll(configDir, 0700) - if err != nil { - fmt.Println(err) - return + configDirKey := "" + + switch s.userConfigType { + case UserConfigTypePublicKey: + pubKey := sshSession.PublicKey() + if pubKey != nil { + pubBytes := pubKey.Marshal() + if len(pubBytes) > 0 { + hash := sha256.Sum256(pubBytes) + configDirKey = fmt.Sprintf("%x", hash) } } + case UserConfigTypeUsername: + user := sshSession.User() + if user != "" { + configDirKey = user + } + } + + if configDirKey != "" { + configDir = fmt.Sprintf("/tmp/cointop_config/%s", configDirKey) + err := os.MkdirAll(configDir, 0700) + if err != nil { + fmt.Println(err) + return + } } if configDir == "" { @@ -209,3 +242,12 @@ func setWinsize(f *os.File, w, h int) { func createTempDir() (string, error) { return ioutil.TempDir("", "") } + +func validateUserConfigType(userConfigType string) { + for _, validType := range UserConfigTypes { + if validType == userConfigType { + return + } + } + panic(fmt.Errorf("invalid user config type. Acceptable values are: %s", strings.Join(UserConfigTypes, ","))) +}