Created
February 25, 2017 17:26
-
-
Save artyom/1a7d46c9511dd93b0714baf7b9745605 to your computer and use it in GitHub Desktop.
Example ssh server with both interactive terminal & sftp support
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package main | |
| import ( | |
| "bufio" | |
| "bytes" | |
| "flag" | |
| "fmt" | |
| "io" | |
| "io/ioutil" | |
| "log" | |
| "net" | |
| "os" | |
| "strings" | |
| "golang.org/x/crypto/ssh" | |
| "golang.org/x/crypto/ssh/terminal" | |
| "github.com/artyom/autoflags" | |
| "github.com/pkg/sftp" | |
| ) | |
| func main() { | |
| args := struct { | |
| Addr string `flag:"addr,address to listen"` | |
| Auth string `flag:"auth,path to authorized_keys file"` | |
| PK string `flag:"hostkey,path to private host key"` | |
| }{ | |
| Addr: "localhost:2022", | |
| Auth: "authorized_keys", | |
| PK: "id_rsa", | |
| } | |
| autoflags.Define(&args) | |
| flag.Parse() | |
| if err := run(args.Addr, args.Auth, args.PK); err != nil { | |
| log.Fatal(err) | |
| } | |
| } | |
| func run(addr, keysFile, pkFile string) error { | |
| config := &ssh.ServerConfig{} | |
| if err := addHostKey(config, pkFile); err != nil { | |
| return err | |
| } | |
| pkeyAuthFunc, err := authChecker(keysFile) | |
| if err != nil { | |
| return err | |
| } | |
| config.PublicKeyCallback = pkeyAuthFunc | |
| ln, err := net.Listen("tcp", addr) | |
| if err != nil { | |
| return err | |
| } | |
| defer ln.Close() | |
| for { | |
| conn, err := ln.Accept() | |
| if err != nil { | |
| return err | |
| } | |
| go func(conn net.Conn) { | |
| if err := serveConn(conn, config); err != nil { | |
| log.Println(err) | |
| } | |
| }(conn) | |
| } | |
| } | |
| func serveConn(conn net.Conn, config *ssh.ServerConfig) error { | |
| defer log.Println("serveConn finished") | |
| defer conn.Close() | |
| sconn, chans, reqs, err := ssh.NewServerConn(conn, config) | |
| if err != nil { | |
| return err | |
| } | |
| defer sconn.Close() | |
| _ = sconn // TODO: check Permissions | |
| go ssh.DiscardRequests(reqs) | |
| for newChannel := range chans { | |
| if newChannel.ChannelType() != "session" { | |
| newChannel.Reject(ssh.UnknownChannelType, "unknown channel type") | |
| continue | |
| } | |
| channel, requests, err := newChannel.Accept() | |
| if err != nil { | |
| return err | |
| } | |
| go func(sshCh ssh.Channel, in <-chan *ssh.Request) { | |
| defer log.Println("channel/requets handler finished") | |
| for req := range in { | |
| var ok bool | |
| switch { | |
| case req.Type == "pty-req": | |
| req.Reply(true, nil) | |
| continue | |
| case req.Type == "shell": | |
| ok = true | |
| go func() { | |
| // defer sconn.Close() // XXX(?) | |
| defer sshCh.Close() // SSH_MSG_CHANNEL_CLOSE | |
| defer sshCh.CloseWrite() // SSH_MSG_CHANNEL_EOF | |
| defer sshCh.SendRequest("[email protected]", false, nil) | |
| switch err := serveTerminal(sshCh); err { | |
| case nil: | |
| sshCh.SendRequest("exit-status", false, ssh.Marshal(&exitStatusMsg{0})) | |
| default: | |
| sshCh.SendRequest("exit-status", false, ssh.Marshal(&exitStatusMsg{1})) | |
| } | |
| }() | |
| case req.Type == "subsystem" && string(req.Payload[4:]) == "sftp": | |
| ok = true | |
| go func() { | |
| defer sshCh.Close() // SSH_MSG_CHANNEL_CLOSE | |
| sftpServer, err := sftp.NewServer(sshCh, sftp.ReadOnly()) | |
| if err != nil { | |
| return | |
| } | |
| _ = sftpServer.Serve() | |
| }() | |
| } | |
| req.Reply(ok, nil) | |
| if ok { | |
| break | |
| } | |
| } | |
| for req := range in { | |
| req.Reply(false, nil) | |
| } | |
| }(channel, requests) | |
| } | |
| return nil | |
| } | |
| func serveTerminal(rw io.ReadWriter) error { | |
| log.Println("serveTerminal started") | |
| defer log.Println("serveTerminal finished") | |
| term := terminal.NewTerminal(rw, "> ") | |
| for { | |
| line, err := term.ReadLine() | |
| switch err { | |
| case nil: | |
| case io.EOF: | |
| return nil | |
| default: | |
| return err | |
| } | |
| log.Println("line read:", line) | |
| if _, err := fmt.Fprintf(term, "You said: %q\n", line); err != nil { | |
| return err | |
| } | |
| } | |
| } | |
| func authChecker(name string) (func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error), error) { | |
| f, err := os.Open(name) | |
| if err != nil { | |
| return nil, err | |
| } | |
| defer f.Close() | |
| type keyMeta struct { | |
| key ssh.PublicKey | |
| opts map[string]string | |
| } | |
| var pkeys []keyMeta | |
| sc := bufio.NewScanner(f) | |
| for sc.Scan() { | |
| pk, _, opts, _, err := ssh.ParseAuthorizedKey(sc.Bytes()) | |
| if err != nil { | |
| return nil, err | |
| } | |
| pkeys = append(pkeys, keyMeta{key: pk, opts: splitOpts(opts)}) | |
| } | |
| if err := sc.Err(); err != nil { | |
| return nil, err | |
| } | |
| return func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) { | |
| keyBytes := key.Marshal() | |
| for _, k := range pkeys { | |
| if bytes.Equal(keyBytes, k.key.Marshal()) { | |
| return &ssh.Permissions{ | |
| Extensions: k.opts, | |
| }, nil | |
| } | |
| } | |
| return nil, fmt.Errorf("no keys matched") | |
| }, nil | |
| } | |
| func splitOpts(opts []string) map[string]string { | |
| if len(opts) == 0 { | |
| return nil | |
| } | |
| m := make(map[string]string, len(opts)) | |
| for _, s := range opts { | |
| ss := strings.SplitN(s, "=", 2) | |
| switch len(ss) { | |
| case 1: | |
| m[s] = "" | |
| case 2: | |
| m[ss[0]] = ss[1] | |
| } | |
| } | |
| return m | |
| } | |
| func addHostKey(config *ssh.ServerConfig, keyFile string) error { | |
| privateBytes, err := ioutil.ReadFile(keyFile) | |
| if err != nil { | |
| return err | |
| } | |
| private, err := ssh.ParsePrivateKey(privateBytes) | |
| if err != nil { | |
| return err | |
| } | |
| config.AddHostKey(private) | |
| return nil | |
| } | |
| type exitStatusMsg struct { | |
| Status uint32 | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thank You!