// A simple SSH server providing bash sessions // // Requires: // go get github.com/kr/pty // go get golang.org/x/crypto/ssh // // Server: // ssh-keygen -t rsa #generate server keypair // go run sshd.go // // Client: // ssh foo@localhost -p 2022 #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() { // An SSH server is represented by a ServerConfig, which holds // certificate details and handles authentication of ServerConns. config := &ssh.ServerConfig{ 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 can generate a keypair with 'ssh-keygen -t rsa -C "test@example.com"' 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:2022") if err != nil { log.Fatal("failed to listen on 2022") } // Accept all connections log.Print("listening on 2022...") 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()) // Print incoming [out-of-band] Requests go handleRequests(reqs) // Accept all channels go handleChannels(chans) } } func handleRequests(reqs <-chan *ssh.Request) { for req := range reqs { log.Printf("recieved request: %+v", req) } } func handleChannels(chans <-chan ssh.NewChannel) { // Service the incoming Channel channel. for newChannel := range chans { // Channels have a type, depending on the application level // protocol intended. In the case of a shell, the type is // "session" and ServerShell may be used to present a simple // terminal interface. if t := newChannel.ChannelType(); t != "session" { newChannel.Reject(ssh.UnknownChannelType, fmt.Sprintf("unknown channel type: %s", t)) continue } channel, requests, err := newChannel.Accept() if err != nil { log.Printf("could not accept channel (%s)", err) continue } // allocate a terminal for this channel log.Print("creating pty...") //fire up bash for this session c := exec.Command("bash") f, err := pty.Start(c) if err != nil { log.Printf("could not start pty (%s)", err) continue } //teardown session var once sync.Once close := func() { channel.Close() log.Printf("session closed") } //link session and bash streams go func() { io.Copy(channel, f) once.Do(close) }() go func() { io.Copy(f, channel) once.Do(close) }() // Sessions have out-of-band requests such as "shell", // "pty-req" and "env". Here we handle only the // "shell" request. go func(in <-chan *ssh.Request) { for req := range in { ok := false switch req.Type { case "shell": // We don't accept any commands (Payload), // only the default shell. if len(req.Payload) == 0 { ok = true } case "pty-req": // Responding 'ok' here will let the client // know we have a pty ready for input ok = true // Parse body... termLen := req.Payload[3] termEnv := string(req.Payload[4 : termLen+4]) w, h := parseDims(req.Payload[termLen+4:]) SetWinsize(f.Fd(), w, h) log.Printf("pty-req '%s'", termEnv) case "window-change": w, h := parseDims(req.Payload) SetWinsize(f.Fd(), w, h) continue //no response } if !ok { log.Printf("declining %s request...", req.Type) } req.Reply(ok, nil) } }(requests) } } // ======================= // Window size helpers 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) { log.Printf("window resize %dx%d", w, h) ws := &Winsize{Width: uint16(w), Height: uint16(h)} syscall.Syscall(syscall.SYS_IOCTL, fd, uintptr(syscall.TIOCSWINSZ), uintptr(unsafe.Pointer(ws))) }