You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

171 lines
4.0 KiB
Go

package main
import (
"errors"
"fmt"
"io"
"log"
"net"
"golang.org/x/crypto/ssh"
)
type Sshd struct {
AuthorizedKeysMap map[string]bool
Filestore Filestore
}
func (s *Sshd) Listen(iface string) error {
// An SSH server is represented by a ServerConfig, which holds
// certificate details and handles authentication of ServerConns.
config := &ssh.ServerConfig{
// Remove to disable public key auth.
PublicKeyCallback: func(c ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) {
if s.AuthorizedKeysMap[string(pubKey.Marshal())] {
return &ssh.Permissions{
// Record the public key used for authentication.
Extensions: map[string]string{
"pubkey-fp": ssh.FingerprintSHA256(pubKey),
},
}, nil
}
return nil, fmt.Errorf("unknown public key for %q", c.User())
},
}
bitSize := 4096
privateKey, err := generatePrivateKey(bitSize)
if err != nil {
return err
}
privateBytes := encodePrivateKeyToPEM(privateKey)
private, err := ssh.ParsePrivateKey(privateBytes)
if err != nil {
return fmt.Errorf("Failed to parse private key: %w", err)
}
config.AddHostKey(private)
// Once a ServerConfig has been configured, connections can be
// accepted.
listener, err := net.Listen("tcp", iface)
if err != nil {
return err
}
return s.HandleRequests(listener, config)
}
func (s *Sshd) HandleRequests(listener net.Listener, config *ssh.ServerConfig) error {
for {
nConn, err := listener.Accept()
if err != nil {
return fmt.Errorf("failed to accept incoming connection: %w", err)
}
// Before use, a handshake must be performed on the incoming
// net.Conn.
conn, chans, reqs, err := ssh.NewServerConn(nConn, config)
if err != nil {
log.Printf("failed to handshake: %s", err)
continue
}
log.Printf("logged in with key %s", conn.Permissions.Extensions["pubkey-fp"])
// The incoming Request channel must be serviced.
go ssh.DiscardRequests(reqs)
// 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 newChannel.ChannelType() != "session" {
newChannel.Reject(ssh.UnknownChannelType, "unknown channel type")
continue
}
channel, requests, err := newChannel.Accept()
if err != nil {
return fmt.Errorf("Could not accept channel: %w", err)
}
session := &SshSession{
shell: NewShell("user@host:/# "),
filestore: s.Filestore,
}
// Sessions have out-of-band requests such as "shell",
// "pty-req" and "env". Here we handle only the
// "shell" request.
go session.HandleRequests(requests)
go session.RunShell(channel)
}
}
}
type SshSession struct {
shell *Shell
filestore Filestore
}
func (s *SshSession) HandleRequests(in <-chan *ssh.Request) {
for req := range in {
log.Printf("received request of type \"%s\"", req.Type)
var ok bool
switch req.Type {
case "shell":
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:])
s.shell.SetWinsize(w, h)
log.Printf("pty-req '%s'", termEnv)
}
req.Reply(ok, nil)
}
}
func (s *SshSession) RunShell(channel io.ReadWriteCloser) {
s.shell.Attach(channel)
handler := NewCommandHandler(s.shell, s.filestore)
go func() {
for {
comm, err := s.shell.ReadCommand()
if err != nil {
if errors.Is(err, io.EOF) {
log.Println("Received EOF, closing TTY")
} else {
log.Println("Error reading command: ", err)
}
if !errors.Is(err, ErrEmptyCommand) {
s.shell.Close()
return
}
} else {
log.Printf("command: \"%s\" %s", comm.Name, comm.Arguments)
handler.Handle(comm)
}
err = s.shell.WritePrompt()
if err != nil {
log.Println(err)
}
}
}()
}