diff --git a/example-sshd/main.go b/example-sshd/main.go index 4e638eb..51a85bd 100644 --- a/example-sshd/main.go +++ b/example-sshd/main.go @@ -6,8 +6,10 @@ import ( "io/ioutil" "log" - "code.google.com/p/go.crypto/ssh" - "code.google.com/p/go.crypto/ssh/terminal" + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/terminal" +// "code.google.com/p/go.crypto/ssh" +// "code.google.com/p/go.crypto/ssh/terminal" ) func main() { diff --git a/example-sshd/sshd.go b/example-sshd/sshd.go new file mode 100644 index 0000000..0b4384b --- /dev/null +++ b/example-sshd/sshd.go @@ -0,0 +1,202 @@ +// A small SSH daemon providing bash sessions +// +// Server: +// cd my/new/dir/ +// #generate server keypair +// ssh-keygen -t rsa +// go get -v . +// go run sshd.go +// +// Client: +// ssh foo@localhost -p 2200 #pass=bar + +package main + +import ( + "encoding/binary" + "fmt" + "io" + "io/ioutil" + "log" + "net" + "os/exec" + "sync" + "syscall" + "unsafe" + + "github.com/kr/pty" + "golang.org/x/crypto/ssh" +) + +func main() { + + // In the latest version of crypto/ssh (after Go 1.3), the SSH server type has been removed + // in favour of an SSH connection type. A ssh.ServerConn is created by passing an existing + // net.Conn and a ssh.ServerConfig to ssh.NewServerConn, in effect, upgrading the net.Conn + // into an ssh.ServerConn + + config := &ssh.ServerConfig{ + //Define a function to run when a client attempts a password login + PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) { + // Should use constant-time compare (or better, salt+hash) in a production setting. + if c.User() == "foo" && string(pass) == "bar" { + return nil, nil + } + return nil, fmt.Errorf("password rejected for %q", c.User()) + }, + // You may also explicitly allow anonymous client authentication, though anon bash + // sessions may not be a wise idea + // NoClientAuth: true, + } + + // You can generate a keypair with 'ssh-keygen -t rsa' + privateBytes, err := ioutil.ReadFile("id_rsa") + if err != nil { + log.Fatal("Failed to load private key (./id_rsa)") + } + + private, err := ssh.ParsePrivateKey(privateBytes) + if err != nil { + log.Fatal("Failed to parse private key") + } + + config.AddHostKey(private) + + // Once a ServerConfig has been configured, connections can be accepted. + listener, err := net.Listen("tcp", "0.0.0.0:2200") + if err != nil { + log.Fatalf("Failed to listen on 2200 (%s)", err) + } + + // Accept all connections + log.Print("Listening on 2200...") + for { + tcpConn, err := listener.Accept() + if err != nil { + log.Printf("Failed to accept incoming connection (%s)", err) + continue + } + // Before use, a handshake must be performed on the incoming net.Conn. + sshConn, chans, reqs, err := ssh.NewServerConn(tcpConn, config) + if err != nil { + log.Printf("Failed to handshake (%s)", err) + continue + } + + log.Printf("New SSH connection from %s (%s)", sshConn.RemoteAddr(), sshConn.ClientVersion()) + // Discard all global out-of-band Requests + go ssh.DiscardRequests(reqs) + // Accept all channels + go handleChannels(chans) + } +} + +func handleChannels(chans <-chan ssh.NewChannel) { + // Service the incoming Channel channel in go routine + for newChannel := range chans { + go handleChannel(newChannel) + } +} + +func handleChannel(newChannel ssh.NewChannel) { + // Since we're handling a shell, we expect a + // channel type of "session". The also describes + // "x11", "direct-tcpip" and "forwarded-tcpip" + // channel types. + if t := newChannel.ChannelType(); t != "session" { + newChannel.Reject(ssh.UnknownChannelType, fmt.Sprintf("unknown channel type: %s", t)) + return + } + + // At this point, we have the opportunity to reject the client's + // request for another logical connection + connection, requests, err := newChannel.Accept() + if err != nil { + log.Printf("Could not accept channel (%s)", err) + return + } + + // Fire up bash for this session + bash := exec.Command("bash") + + // Prepare teardown function + close := func() { + connection.Close() + _, err := bash.Process.Wait() + if err != nil { + log.Printf("Failed to exit bash (%s)", err) + } + log.Printf("Session closed") + } + + // Allocate a terminal for this channel + log.Print("Creating pty...") + bashf, err := pty.Start(bash) + if err != nil { + log.Printf("Could not start pty (%s)", err) + close() + return + } + + //pipe session to bash and visa-versa + var once sync.Once + go func() { + io.Copy(connection, bashf) + once.Do(close) + }() + go func() { + io.Copy(bashf, connection) + once.Do(close) + }() + + // Sessions have out-of-band requests such as "shell", "pty-req" and "env" + go func() { + for req := range requests { + switch req.Type { + case "shell": + // We only accept the default shell + // (i.e. no command in the Payload) + if len(req.Payload) == 0 { + req.Reply(true, nil) + } + case "pty-req": + termLen := req.Payload[3] + w, h := parseDims(req.Payload[termLen+4:]) + SetWinsize(bashf.Fd(), w, h) + // Responding true (OK) here will let the client + // know we have a pty ready for input + req.Reply(true, nil) + case "window-change": + w, h := parseDims(req.Payload) + SetWinsize(bashf.Fd(), w, h) + } + } + }() +} + +// ======================= + +// parseDims extracts terminal dimensions (width x height) from the provided buffer. +func parseDims(b []byte) (uint32, uint32) { + w := binary.BigEndian.Uint32(b) + h := binary.BigEndian.Uint32(b[4:]) + return w, h +} + +// ====================== + +// Winsize stores the Height and Width of a terminal. +type Winsize struct { + Height uint16 + Width uint16 + x uint16 // unused + y uint16 // unused +} + +// SetWinsize sets the size of the given pty. +func SetWinsize(fd uintptr, w, h uint32) { + ws := &Winsize{Width: uint16(w), Height: uint16(h)} + syscall.Syscall(syscall.SYS_IOCTL, fd, uintptr(syscall.TIOCSWINSZ), uintptr(unsafe.Pointer(ws))) +} + +// Borrowed from https://github.com/creack/termios/blob/master/win/win.go