//+build !windows

package sshserver

import (
	"fmt"
	"io/ioutil"
	"os"
	"path"

	"github.com/gliderlabs/ssh"
	"github.com/pkg/errors"
	gossh "golang.org/x/crypto/ssh"
)

var (
	systemConfigPath  = "/etc/cloudflared/"
	authorizedKeysDir = ".cloudflared/authorized_keys"
)

func (s *SSHServer) configureAuthentication() {
	caCert, err := getCACert()
	if err != nil {
		s.logger.Info(err)
	}
	s.caCert = caCert
	s.PublicKeyHandler = s.authenticationHandler
}

// authenticationHandler is a callback that returns true if the user attempting to connect is authenticated.
func (s *SSHServer) authenticationHandler(ctx ssh.Context, key ssh.PublicKey) bool {
	sshUser, err := lookupUser(ctx.User())
	if err != nil {
		s.logger.Debugf("Invalid user: %s", ctx.User())
		return false
	}
	ctx.SetValue("sshUser", sshUser)

	cert, ok := key.(*gossh.Certificate)
	if !ok {
		return s.authorizedKeyHandler(ctx, key)
	}
	return s.shortLivedCertHandler(ctx, cert)
}

func (s *SSHServer) authorizedKeyHandler(ctx ssh.Context, key ssh.PublicKey) bool {
	sshUser, ok := ctx.Value("sshUser").(*User)
	if !ok {
		s.logger.Error("Failed to retrieve user from context")
		return false
	}

	authorizedKeysPath := path.Join(sshUser.HomeDir, authorizedKeysDir)
	if _, err := os.Stat(authorizedKeysPath); os.IsNotExist(err) {
		s.logger.Debugf("authorized_keys file %s not found", authorizedKeysPath)
		return false
	}

	authorizedKeysBytes, err := ioutil.ReadFile(authorizedKeysPath)
	if err != nil {
		s.logger.WithError(err).Errorf("Failed to load authorized_keys %s", authorizedKeysPath)
		return false
	}

	for len(authorizedKeysBytes) > 0 {
		// Skips invalid keys. Returns error if no valid keys remain.
		pubKey, _, _, rest, err := ssh.ParseAuthorizedKey(authorizedKeysBytes)
		authorizedKeysBytes = rest
		if err != nil {
			s.logger.Errorf("Invalid key(s) found in %s", authorizedKeysPath)
			return false
		}

		if ssh.KeysEqual(pubKey, key) {
			return true
		}
	}
	s.logger.Debugf("Matching public key not found in %s", authorizedKeysPath)
	return false
}

func (s *SSHServer) shortLivedCertHandler(ctx ssh.Context, cert *gossh.Certificate) bool {
	if !ssh.KeysEqual(s.caCert, cert.SignatureKey) {
		s.logger.Debug("CA certificate does not match user certificate signer")
		return false
	}

	checker := gossh.CertChecker{}
	if err := checker.CheckCert(ctx.User(), cert); err != nil {
		s.logger.Debug(err)
		return false
	}
	return true
}

func getCACert() (ssh.PublicKey, error) {
	caCertPath := path.Join(systemConfigPath, "ca.pub")
	caCertBytes, err := ioutil.ReadFile(caCertPath)
	if err != nil {
		return nil, errors.Wrap(err, fmt.Sprintf("Failed to load CA certificate %s", caCertPath))
	}
	caCert, _, _, _, err := ssh.ParseAuthorizedKey(caCertBytes)
	if err != nil {
		return nil, errors.Wrap(err, "Failed to parse CA Certificate")
	}

	return caCert, nil
}
