diff --git a/README.md b/README.md index 3f3604d7..ef4b7f84 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,15 @@ TUI list select ssh/scp client. ## Description -command to read a prepared list in advance and connect ssh/scp the selected host. List file is set in yaml format.When selecting a host, you can filter by keywords. Can execute commands concurrently to multiple hosts. Supported multiple ssh proxy, and supported http/socks5 proxy. +command to read a prepared list in advance and connect ssh/scp the selected host. List file is set in yaml format. When selecting a host, you can filter by keywords. Can execute commands concurrently to multiple hosts. Supported multiple ssh proxy, and supported http/socks5 proxy. + +## Features + +* List selection type ssh client. +* Pure Go. +* Commands can be executed by ssh connection in parallel. +* Supported multiple proxy. +* Can use bashrc of local machine at ssh connection destination. ## Demo @@ -23,7 +31,7 @@ lscp is need the following command in remote server. ## Install -compile gofile(tested go1.8.3). +compile gofile(tested go1.11.5). go get github.com/blacknon/lssh cd $GOPATH/src/github.com/blacknon/lssh @@ -40,9 +48,10 @@ brew install(Mac OS X) # generate .lssh.conf(use ~/.ssh/config.not support proxy) lssh --generate > ~/.lssh.conf -## Usage +## Config -Please edit "~/.lssh.conf". The connection information at servers,can be divided into external files. log dir "\" => date(YYYYMMDD) ,"\" => Servername. +Please edit "~/.lssh.conf". +For details see [wiki](https://github.com/blacknon/lssh/wiki/Config). example: @@ -53,7 +62,7 @@ example: dirpath = "/path/to/logdir" # server common settings - [common] + [common] port = "22" user = "test" @@ -132,7 +141,10 @@ example: addr = "example.com" port = "54321" -After run command. + +## Usage + +run command. lssh @@ -140,61 +152,65 @@ After run command. option(lssh) NAME: - lssh - TUI list select and parallel ssh client command. + lssh - TUI list select and parallel ssh client command. USAGE: - lssh [options] [commands...] - + lssh [options] [commands...] + OPTIONS: - --host value, -H value connect servernames - --list, -l print server list from config - --file value, -f value config file path (default: "$HOME/.lssh.conf") - --term, -t run specified command at terminal - --parallel, -p run command parallel node(tail -F etc...) - --generate (beta) generate .lssh.conf from .ssh/config.(not support ProxyCommand) - --help, -h print this help - --version, -v print the version - + --host value, -H value connect servernames + --list, -l print server list from config + --file value, -f value config file path (default: "/home/blacknon/.lssh.conf") + --portforward-local value port forwarding local port(ex. 127.0.0.1:8080) + --portforward-remote value port forwarding remote port(ex. 127.0.0.1:80) + --term, -t run specified command at terminal + --parallel, -p run command parallel node(tail -F etc...) + --generate (beta) generate .lssh.conf from .ssh/config.(not support ProxyCommand) + --help, -h print this help + --version, -v print the version + COPYRIGHT: - blacknon(blacknon@orebibou.com) - + blacknon(blacknon@orebibou.com) + VERSION: - 0.5.1 - + 0.5.2 + USAGE: - # connect ssh - lssh - - # parallel run command in select server over ssh - lssh -p command... - + # connect ssh + lssh + + # parallel run command in select server over ssh + lssh -p command... + -option(lscp) + option(lscp) + NAME: lscp - TUI list select and parallel scp client command. USAGE: lscp [options] (local|remote):from_path... (local|remote):to_path - + OPTIONS: --host value, -H value connect servernames --list, -l print server list from config - --file value, -f value config file path (default: "$HOME/.lssh.conf") + --file value, -f value config file path (default: "/home/blacknon/.lssh.conf") --permission, -p copy file permission --help, -h print this help --version, -v print the version - + COPYRIGHT: blacknon(blacknon@orebibou.com) - + VERSION: - 0.5.1 - + 0.5.2 + USAGE: # local to remote scp lscp /path/to/local... /path/to/remote - + # remote to local scp lscp remote:/path/to/remote... /path/to/local + If you specify a command as an argument, you can select multiple hosts. Select host 'Tab', select all displayed hosts 'Ctrl + A'. @@ -226,6 +242,10 @@ You can scp like copy files using stdin/stdout.It also supports multiple nodes(p sample lssh.conf +

+ +

+ [server.iTerm2_sample] addr = "192.168.100.103" key = "/path/to/private_key" @@ -234,6 +254,14 @@ sample lssh.conf post_cmd = 'printf "\033]50;SetProfile=Default\a"' # local theme post_cmd = "(option) exec command after ssh disconnected." + [server.GnomeTerminal_sample] + addr = "192.168.100.103" + key = "/path/to/private_key" + note = "Before/After run local command" + pre_cmd = 'printf "\033]50;SetProfile=dq\a"' # ssh theme + post_cmd = 'printf "\033]50;SetProfile=Default\a"' # local theme + post_cmd = "(option) exec command after ssh disconnected." + ### [lssh] use local bashrc file. diff --git a/args/lscp_args.go b/args/lscp_args.go index d7d4ebc6..d090b8b6 100644 --- a/args/lscp_args.go +++ b/args/lscp_args.go @@ -52,7 +52,7 @@ USAGE: app.Name = "lscp" app.Usage = "TUI list select and parallel scp client command." app.Copyright = "blacknon(blacknon@orebibou.com)" - app.Version = "0.5.1" + app.Version = "0.5.2" app.Flags = []cli.Flag{ cli.StringSliceFlag{Name: "host,H", Usage: "connect servernames"}, diff --git a/args/lssh_args.go b/args/lssh_args.go index dcea7f92..51d5a9ad 100644 --- a/args/lssh_args.go +++ b/args/lssh_args.go @@ -39,8 +39,8 @@ VERSION: {{.Version}} {{end}} USAGE: - # connect ssh - {{.Name}} + # connect ssh + {{.Name}} # parallel run command in select server over ssh {{.Name}} -p command... @@ -51,13 +51,15 @@ USAGE: app.Name = "lssh" app.Usage = "TUI list select and parallel ssh client command." app.Copyright = "blacknon(blacknon@orebibou.com)" - app.Version = "0.5.1" + app.Version = "0.5.2" // Set options app.Flags = []cli.Flag{ cli.StringSliceFlag{Name: "host,H", Usage: "connect servernames"}, cli.BoolFlag{Name: "list,l", Usage: "print server list from config"}, cli.StringFlag{Name: "file,f", Value: defConf, Usage: "config file path"}, + cli.StringFlag{Name: "portforward-local", Usage: "port forwarding local port(ex. 127.0.0.1:8080)"}, + cli.StringFlag{Name: "portforward-remote", Usage: "port forwarding remote port(ex. 127.0.0.1:80)"}, cli.BoolFlag{Name: "term,t", Usage: "run specified command at terminal"}, cli.BoolFlag{Name: "parallel,p", Usage: "run command parallel node(tail -F etc...)"}, cli.BoolFlag{Name: "generate", Usage: "(beta) generate .lssh.conf from .ssh/config.(not support ProxyCommand)"}, @@ -136,6 +138,9 @@ USAGE: r.IsParallel = c.Bool("parallel") r.ExecCmd = c.Args() + r.PortForwardLocal = c.String("portforward-local") + r.PortForwardRemote = c.String("portforward-remote") + r.Start() return nil } diff --git a/common/common.go b/common/common.go index b8a57497..6d9d2926 100644 --- a/common/common.go +++ b/common/common.go @@ -100,6 +100,7 @@ func GetFilesBase64(list []string) (result string, err error) { } data = append(data, file_data...) + data = append(data, '\n') } result = base64.StdEncoding.EncodeToString(data) diff --git a/conf/conf.go b/conf/conf.go index 6b17f885..3acb009a 100644 --- a/conf/conf.go +++ b/conf/conf.go @@ -1,21 +1,25 @@ package conf import ( + "crypto/md5" + "encoding/hex" "fmt" "os" "os/user" "strings" + "time" "github.com/BurntSushi/toml" "github.com/blacknon/lssh/common" ) type Config struct { - Log LogConfig - Include map[string]IncludeConfig - Common ServerConfig - Server map[string]ServerConfig - Proxy map[string]ProxyConfig + Log LogConfig + Include map[string]IncludeConfig + Includes IncludesConfig + Common ServerConfig + Server map[string]ServerConfig + Proxy map[string]ProxyConfig } type LogConfig struct { @@ -24,25 +28,33 @@ type LogConfig struct { Dir string `toml:"dirpath"` } +type IncludesConfig struct { + Path []string `toml:"path"` +} + type IncludeConfig struct { Path string `toml:"path"` } type ServerConfig struct { - Addr string `toml:"addr"` - Port string `toml:"port"` - User string `toml:"user"` - Pass string `toml:"pass"` - Key string `toml:"key"` - KeyPass string `toml:"keypass"` - PreCmd string `toml:"pre_cmd"` - PostCmd string `toml:"post_cmd"` - ProxyType string `toml:"proxy_type"` - Proxy string `toml:"proxy"` - LocalRcUse string `toml:"local_rc"` // yes|no - LocalRcPath []string `toml:"local_rc_file"` - LocalRcDecodeCmd string `toml:"local_rc_decode_cmd"` - Note string `toml:"note"` + Addr string `toml:"addr"` + Port string `toml:"port"` + User string `toml:"user"` + Pass string `toml:"pass"` + Key string `toml:"key"` + KeyPass string `toml:"keypass"` + PreCmd string `toml:"pre_cmd"` + PostCmd string `toml:"post_cmd"` + ProxyType string `toml:"proxy_type"` + Proxy string `toml:"proxy"` + LocalRcUse string `toml:"local_rc"` // yes|no + LocalRcPath []string `toml:"local_rc_file"` + LocalRcDecodeCmd string `toml:"local_rc_decode_cmd"` + PortForwardLocal string `toml:"port_forward_local"` // port forward (local). "host:port" + PortForwardRemote string `toml:"port_forward_remote"` // port forward (remote). "host:port" + SSHAgentUse bool `toml:"ssh_agent"` + SSHAgentKeyPath []string `toml:"ssh_agent_key"` // "keypath::passphase" + Note string `toml:"note"` } type ProxyConfig struct { @@ -54,6 +66,9 @@ type ProxyConfig struct { } func ReadConf(confPath string) (checkConf Config) { + // user path + usr, _ := user.Current() + if !common.IsExist(confPath) { fmt.Printf("Config file(%s) Not Found.\nPlease create file.\n\n", confPath) fmt.Printf("sample: %s\n", "https://raw.githubusercontent.com/blacknon/lssh/master/example/config.tml") @@ -72,13 +87,32 @@ func ReadConf(confPath string) (checkConf Config) { checkConf.Server[key] = setValue } + // for append includes to include.path + if checkConf.Includes.Path != nil { + if checkConf.Include == nil { + checkConf.Include = map[string]IncludeConfig{} + } + + for _, includePath := range checkConf.Includes.Path { + unixTime := time.Now().Unix() + keyString := strings.Join([]string{string(unixTime), includePath}, "_") + + // key to md5 + hasher := md5.New() + hasher.Write([]byte(keyString)) + key := string(hex.EncodeToString(hasher.Sum(nil))) + + // append checkConf.Include[key] + checkConf.Include[key] = IncludeConfig{strings.Replace(includePath, "~", usr.HomeDir, 1)} + } + } + // Read include files if checkConf.Include != nil { for _, v := range checkConf.Include { var includeConf Config // user path - usr, _ := user.Current() path := strings.Replace(v.Path, "~", usr.HomeDir, 1) // Read include config file diff --git a/example/config.tml b/example/config.tml index 7b9cb382..3277b998 100644 --- a/example/config.tml +++ b/example/config.tml @@ -1,5 +1,6 @@ [log] enable = true +timestamp = true dirpath = "/path/to/logdir" [server.PasswordAuth_ServerName] diff --git a/example/lssh.gif b/example/lssh.gif index 49822e75..0146b1a2 100644 Binary files a/example/lssh.gif and b/example/lssh.gif differ diff --git a/example/lssh_iterm2.gif b/example/lssh_iterm2.gif new file mode 100644 index 00000000..7b9759e6 Binary files /dev/null and b/example/lssh_iterm2.gif differ diff --git a/list/event.go b/list/event.go index 6fd3a181..530871a3 100644 --- a/list/event.go +++ b/list/event.go @@ -33,8 +33,7 @@ func (l *ListInfo) keyEvent() (lineData []string) { for { switch ev := termbox.PollEvent(); ev.Type { - - // Get Key Event + // Type Key case termbox.EventKey: switch ev.Key { // ESC or Ctrl + C Key (Exit) @@ -138,6 +137,20 @@ func (l *ListInfo) keyEvent() (lineData []string) { l.draw() } } + + // Type Mouse + case termbox.EventMouse: + if ev.Key == termbox.MouseLeft { + // mouse select line is (ev.MouseY - headLine) line. + mouseSelectLine := ev.MouseY - headLine + + if mouseSelectLine <= len(l.ViewText)-headLine { + l.CursorLine = mouseSelectLine + } + l.draw() + } + + // Other default: l.draw() } diff --git a/list/list.go b/list/list.go index 8cfa0386..5b36f264 100644 --- a/list/list.go +++ b/list/list.go @@ -130,6 +130,10 @@ func (l *ListInfo) View() { panic(err) } defer termbox.Close() + + // enable termbox mouse input + termbox.SetInputMode(termbox.InputMouse) + l.getText() l.keyEvent() } diff --git a/ssh/connect.go b/ssh/connect.go index 8c921da8..1b52d832 100644 --- a/ssh/connect.go +++ b/ssh/connect.go @@ -23,11 +23,14 @@ import ( type Connect struct { Server string Conf conf.Config + sshClient *ssh.Client IsTerm bool IsParallel bool IsLocalRc bool LocalRcData string LocalRcDecodeCmd string + ForwardLocal string + ForwardRemote string } type Proxy struct { @@ -38,13 +41,14 @@ type Proxy struct { // @brief: create ssh session func (c *Connect) CreateSession() (session *ssh.Session, err error) { // New connect - conn, err := c.createSshClient() + err = c.createSshClient() if err != nil { return session, err } // New session - session, err = conn.NewSession() + session, err = c.sshClient.NewSession() + if err != nil { return session, err } @@ -54,38 +58,41 @@ func (c *Connect) CreateSession() (session *ssh.Session, err error) { // @brief: create ssh client // @note: -// support multiple proxy connect -func (c *Connect) createSshClient() (client *ssh.Client, err error) { +// support multiple proxy connect. +func (c *Connect) createSshClient() (err error) { // New ClientConfig serverConf := c.Conf.Server[c.Server] sshConf, err := c.createSshClientConfig(c.Server) if err != nil { - return client, err + return err } // not use proxy if serverConf.Proxy == "" { - client, err = ssh.Dial("tcp", net.JoinHostPort(serverConf.Addr, serverConf.Port), sshConf) + client, err := ssh.Dial("tcp", net.JoinHostPort(serverConf.Addr, serverConf.Port), sshConf) if err != nil { - return client, err + return err } + + // set sshClient + c.sshClient = client } else { - client, err = c.createSshClientOverProxy(serverConf, sshConf) + err := c.createSshClientOverProxy(serverConf, sshConf) if err != nil { - return client, err + return err } } - return client, err + return err } // @brief: // Create ssh client via proxy -func (c *Connect) createSshClientOverProxy(serverConf conf.ServerConfig, sshConf *ssh.ClientConfig) (client *ssh.Client, err error) { +func (c *Connect) createSshClientOverProxy(serverConf conf.ServerConfig, sshConf *ssh.ClientConfig) (err error) { // get proxy slice proxyList, proxyType, err := GetProxyList(c.Server, c.Conf) if err != nil { - return client, err + return err } // var @@ -106,22 +113,25 @@ func (c *Connect) createSshClientOverProxy(serverConf conf.ServerConfig, sshConf proxyConf := c.Conf.Server[proxy] proxySshConf, err := c.createSshClientConfig(proxy) if err != nil { - return client, err + return err } proxyClient, err = createSshClientViaProxy(proxyConf, proxySshConf, proxyClient, proxyDialer) } if err != nil { - return client, err + return err } } - client, err = createSshClientViaProxy(serverConf, sshConf, proxyClient, proxyDialer) + client, err := createSshClientViaProxy(serverConf, sshConf, proxyClient, proxyDialer) if err != nil { - return client, err + return err } + // set c.sshClient + c.sshClient = client + return } @@ -295,7 +305,8 @@ func (c *Connect) ConTerm(session *ssh.Session) (err error) { ssh.TTY_OP_OSPEED: 14400, } - err = session.RequestPty("xterm", height, width, modes) + term := os.Getenv("TERM") + err = session.RequestPty(term, height, width, modes) if err != nil { return } @@ -362,7 +373,8 @@ func (c *Connect) setIsTerm(preSession *ssh.Session) (session *ssh.Session, err return session, err } - if err = preSession.RequestPty("xterm", hight, width, modes); err != nil { + term := os.Getenv("TERM") + if err = preSession.RequestPty(term, hight, width, modes); err != nil { preSession.Close() return session, err } diff --git a/ssh/connect_port_forward.go b/ssh/connect_port_forward.go new file mode 100644 index 00000000..7420a2f7 --- /dev/null +++ b/ssh/connect_port_forward.go @@ -0,0 +1,57 @@ +package ssh + +import ( + "fmt" + "io" + "net" + "os" +) + +// @TODO: +// ・sshClinetをStructのフィールドにする(引数にする必要もないだろうし) +// ・forward単体でポートフォワードが終わるようにする + +// @brief: +func (c *Connect) forward(localConn net.Conn) { + // Create ssh connect + sshConn, err := c.sshClient.Dial("tcp", c.ForwardRemote) + + // Copy localConn.Reader to sshConn.Writer + go func() { + _, err = io.Copy(sshConn, localConn) + if err != nil { + fmt.Println("Port forward local to remote failed: %v\n", err) + } + }() + + // Copy sshConn.Reader to localConn.Writer + go func() { + _, err = io.Copy(localConn, sshConn) + if err != nil { + fmt.Println("Port forward remote to local failed: %v\n", err) + } + }() +} + +// @brief: +func (c *Connect) PortForwarder() { + // Open local port. + localListener, err := net.Listen("tcp", c.ForwardLocal) + + if err != nil { + // error local port open. + fmt.Fprintf(os.Stdout, "local port listen failed: %v\n", err) + } else { + // start port forwarding. + go func() { + for { + // Setup localConn (type net.Conn) + localConn, err := localListener.Accept() + if err != nil { + fmt.Println("listen.Accept failed: %v\n", err) + } + go c.forward(localConn) + } + }() + } +} diff --git a/ssh/connect_ssh_agent.go b/ssh/connect_ssh_agent.go new file mode 100644 index 00000000..01feefa6 --- /dev/null +++ b/ssh/connect_ssh_agent.go @@ -0,0 +1,66 @@ +package ssh + +import ( + "fmt" + "io/ioutil" + "os" + "os/user" + "strings" + + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/agent" +) + +func (c *Connect) CreateSshAgentKeyring() (keyring agent.Agent) { + // declare keyring + keyring = agent.NewKeyring() + + // user path + usr, _ := user.Current() + + conf := c.Conf.Server[c.Server] + sshKeys := conf.SSHAgentKeyPath + + for _, keyPathData := range sshKeys { + // parse ssh key strings + // * keyPathArray[0] ... KeyPath + // * keyPathArray[1] ... KeyPassPhase + keyPathArray := strings.SplitN(keyPathData, "::", 2) + + // key path to fullpath + keyPath := strings.Replace(keyPathArray[0], "~", usr.HomeDir, 1) + + // read key file + keyData, err := ioutil.ReadFile(keyPath) + if err != nil { + fmt.Fprintf(os.Stderr, "failed read key file: %v\n", err) + continue + } + + // parse key data + var key interface{} + if len(keyPathArray) > 1 { + key, err = ssh.ParseRawPrivateKeyWithPassphrase(keyData, []byte(keyPathArray[1])) + } else { + key, err = ssh.ParseRawPrivateKey(keyData) + } + + if err != nil { + fmt.Fprintf(os.Stderr, "failed parse key file: %v, %v\n", keyPath, err) + continue + } + + // add key to keyring + err = keyring.Add(agent.AddedKey{ + PrivateKey: key, + ConfirmBeforeUse: true, + LifetimeSecs: 36000, + }) + if err != nil { + fmt.Fprintf(os.Stderr, "failed add key to keyring: %v, %v\n", keyPath, err) + continue + } + } + + return +} diff --git a/ssh/run.go b/ssh/run.go index e0889b52..f894f6eb 100644 --- a/ssh/run.go +++ b/ssh/run.go @@ -13,13 +13,15 @@ import ( ) type Run struct { - ServerList []string - Conf conf.Config - IsTerm bool - IsParallel bool - ExecCmd []string - StdinData []byte - OutputData *bytes.Buffer + ServerList []string + Conf conf.Config + IsTerm bool + IsParallel bool + PortForwardLocal string + PortForwardRemote string + ExecCmd []string + StdinData []byte + OutputData *bytes.Buffer } func (r *Run) Start() { @@ -45,6 +47,10 @@ func (r *Run) printRunCommand() { fmt.Fprintf(os.Stderr, "Run Command :%s\n", runCmdStr) } +func (r *Run) printPortForward(forwardLocal, forwardRemote string) { + fmt.Fprintf(os.Stderr, "Port Forward :local[%s] <=> remote[%s]\n", forwardLocal, forwardRemote) +} + func (r *Run) printProxy() { if len(r.ServerList) == 1 { proxyList := []string{} diff --git a/ssh/run_term.go b/ssh/run_term.go index 78de4de9..da55b0f9 100644 --- a/ssh/run_term.go +++ b/ssh/run_term.go @@ -12,6 +12,7 @@ import ( "github.com/blacknon/lssh/common" "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/agent" ) func (r *Run) term() (err error) { @@ -24,7 +25,6 @@ func (r *Run) term() (err error) { // print header r.printSelectServer() r.printProxy() - fmt.Println() // print newline // create ssh session session, err := c.CreateSession() @@ -52,8 +52,7 @@ func (r *Run) term() (err error) { } if c.IsLocalRc { - fmt.Fprintf(os.Stderr, "Infomation : This connect use local bashrc. \n") - fmt.Println() // print newline + fmt.Fprintf(os.Stderr, "Infomation :This connect use local bashrc. \n") if len(serverConf.LocalRcPath) > 0 { c.LocalRcData, err = common.GetFilesBase64(serverConf.LocalRcPath) if err != nil { @@ -79,6 +78,37 @@ func (r *Run) term() (err error) { defer runCmdLocal(postCmd) } + // Overwrite port forward option. + if len(r.PortForwardLocal) > 0 { + serverConf.PortForwardLocal = r.PortForwardLocal + } + if len(r.PortForwardRemote) > 0 { + serverConf.PortForwardRemote = r.PortForwardRemote + } + + // Port Forwarding + if len(serverConf.PortForwardLocal) > 0 && len(serverConf.PortForwardRemote) > 0 { + c.ForwardLocal = serverConf.PortForwardLocal + c.ForwardRemote = serverConf.PortForwardRemote + + r.printPortForward(c.ForwardLocal, c.ForwardRemote) + + go func() { + c.PortForwarder() + }() + } + + // ssh-agent + if serverConf.SSHAgentUse { + fmt.Fprintf(os.Stderr, "Infomation :This connect use ssh agent. \n") + keyring := c.CreateSshAgentKeyring() + agent.ForwardToAgent(c.sshClient, keyring) + agent.RequestAgentForwarding(session) + } + + // print newline + fmt.Println("------------------------------") + // Connect ssh terminal finished := make(chan bool) go func() {