-
-
Save toffaletti/6ee027170addec5f9232 to your computer and use it in GitHub Desktop.
This file contains 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
// 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 "[email protected]"' | |
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