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.
222 lines
5.2 KiB
Go
222 lines
5.2 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"encoding/binary"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net"
|
|
|
|
"git.snorba.art/hugo/nssh/commands"
|
|
"git.snorba.art/hugo/nssh/storage"
|
|
"golang.org/x/crypto/ssh"
|
|
)
|
|
|
|
func generatePrivateKey(bitSize int) (*rsa.PrivateKey, error) {
|
|
// Private Key generation
|
|
privateKey, err := rsa.GenerateKey(rand.Reader, bitSize)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Validate Private Key
|
|
err = privateKey.Validate()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
log.Println("Private Key generated")
|
|
return privateKey, nil
|
|
}
|
|
|
|
func encodePrivateKeyToPEM(privateKey *rsa.PrivateKey) []byte {
|
|
// Get ASN.1 DER format
|
|
privDER := x509.MarshalPKCS1PrivateKey(privateKey)
|
|
|
|
// pem.Block
|
|
privBlock := pem.Block{
|
|
Type: "RSA PRIVATE KEY",
|
|
Headers: nil,
|
|
Bytes: privDER,
|
|
}
|
|
|
|
// Private key in PEM format
|
|
privatePEM := pem.EncodeToMemory(&privBlock)
|
|
|
|
return privatePEM
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
type Sshd struct {
|
|
AuthorizedKeysMap map[string]bool
|
|
Filestore storage.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: commands.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 *commands.Shell
|
|
filestore storage.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 := commands.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, commands.ErrEmptyCommand) {
|
|
s.shell.Close()
|
|
return
|
|
}
|
|
} else {
|
|
log.Printf("command: \"%s\" %s", comm.Name, comm.Arguments)
|
|
|
|
err := handler.Handle(comm)
|
|
if err != nil {
|
|
log.Println("Error handling command: " + err.Error())
|
|
}
|
|
}
|
|
|
|
err = s.shell.WritePrompt()
|
|
if err != nil {
|
|
log.Println(err)
|
|
}
|
|
}
|
|
}()
|
|
}
|