Skip to content

Instantly share code, notes, and snippets.

@max107
Forked from jpillora/sshd.go
Last active August 29, 2015 14:11
Show Gist options
  • Save max107/e4c56ee4b60f75699d6b to your computer and use it in GitHub Desktop.
Save max107/e4c56ee4b60f75699d6b to your computer and use it in GitHub Desktop.
// A simple SSH server providing bash sessions
//
// Server:
// cd my/new/dir/
// ssh-keygen -t rsa #generate server keypair
// go get -v .
// 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{
// NoClientAuth: true,
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 out-of-band 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()
_, err := c.Process.Wait()
if err != nil {
log.Printf("failed to exit bash (%s)", err)
}
log.Printf("session closed")
}
//pipe session to bash and visa-versa
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"
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)
}
}
// =======================
// parseDims extracts two uint32s 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) {
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)))
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment