package main import ( "errors" "fmt" "io" "log" "os" "strings" "github.com/chzyer/readline" "github.com/gliderlabs/ssh" "github.com/google/shlex" "github.com/kr/pty" "golang.org/x/crypto/ssh/terminal" "github.com/alecthomas/colour" "github.com/alecthomas/kong" ) type context struct { kong *kong.Context rl *readline.Instance } // Handle a single SSH interactive connection. func handle(log *log.Logger, s ssh.Session) error { log.Printf("New SSH") sshPty, _, isPty := s.Pty() if !isPty { return errors.New("No PTY requested") } log.Printf("Using TERM=%s width=%d height=%d", sshPty.Term, sshPty.Window.Width, sshPty.Window.Height) cpty, tty, err := pty.Open() if err != nil { return err } defer tty.Close() state, err := terminal.GetState(int(cpty.Fd())) if err != nil { return err } defer terminal.Restore(int(cpty.Fd()), state) colour.Fprintln(tty, "^BWelcome!^R") go io.Copy(cpty, s) go io.Copy(s, cpty) parser, err := buildShellParser(tty) if err != nil { return err } rl, err := readline.NewEx(&readline.Config{ Prompt: "> ", Stderr: tty, Stdout: tty, Stdin: tty, FuncOnWidthChanged: func(f func()) {}, FuncMakeRaw: func() error { _, err := terminal.MakeRaw(int(cpty.Fd())) // nolint: govet return err }, FuncExitRaw: func() error { return nil }, }) if err != nil { return err } log.Printf("Loop") for { tty.Sync() var line string line, err = rl.Readline() if err != nil { if err == io.EOF { return nil } return err } var args []string args, err := shlex.Split(string(line)) if err != nil { parser.Errorf("%s", err) continue } var ctx *kong.Context ctx, err = parser.Parse(args) if err != nil { parser.Errorf("%s", err) if err, ok := err.(*kong.ParseError); ok { log.Println(err.Error()) err.Context.PrintUsage(false) } continue } err = ctx.Run(ctx) if err != nil { parser.Errorf("%s", err) continue } } } func buildShellParser(tty *os.File) (*kong.Kong, error) { parser, err := kong.New(&grammar{}, kong.Name(""), kong.Description("Example using Kong for interactive command parsing."), kong.Writers(tty, tty), kong.Exit(func(int) {}), kong.ConfigureHelp(kong.HelpOptions{ NoAppSummary: true, }), kong.NoDefaultHelp(), ) return parser, err } func handlerWithError(handle func(log *log.Logger, s ssh.Session) error) ssh.Handler { return func(s ssh.Session) { prefix := fmt.Sprintf("%s->%s ", s.LocalAddr(), s.RemoteAddr()) l := log.New(os.Stdout, prefix, log.LstdFlags) err := handle(l, s) if err != nil { log.Printf("error: %s", err) s.Exit(1) } else { log.Printf("Bye") s.Exit(0) } } } var cli struct { HostKey string `type:"existingfile" help:"SSH host key to use." default:"server_rsa_key"` Bind string `help:"Bind address for server." default:"127.0.0.1:6740"` } func main() { ctx := kong.Parse(&cli, kong.Name("server"), kong.Description("A network server using Kong for interacting with clients.")) ssh.Handle(handlerWithError(handle)) log.Printf("SSH listening on: %s", cli.Bind) log.Printf("Using host key: %s", cli.HostKey) log.Println() parts := strings.Split(cli.Bind, ":") log.Printf("Connect with: ssh -p %s %s", parts[1], parts[0]) log.Println() err := ssh.ListenAndServe(cli.Bind, nil, ssh.HostKeyFile(cli.HostKey)) ctx.FatalIfErrorf(err) }