package main

import (
	"crypto/x509"
	"fmt"
	"io/ioutil"
	"net"
	"os"
	"strings"
)

func isDir(path string) (bool, error) {
	fi, err := os.Stat(path)
	if err != nil {
		return false, err
	}

	return fi.IsDir(), nil
}

func isRegular(path string) (bool, error) {
	fi, err := os.Stat(path)
	if err != nil {
		return false, err
	}

	return fi.Mode().IsRegular(), nil
}

// KeyConfig holds the configuration data for a single key.
type KeyConfig struct {
	// Path to the configuration directory.
	ConfigPath string

	// Paths to the files themselves.
	keyPath            string
	allowedClientsPath string
	allowedHostsPath   string
	emailToPath        string

	// Allowed certificates.
	allowedClientCerts *x509.CertPool

	// Allowed hosts.
	allowedHosts []string
}

// NewKeyConfig makes a new KeyConfig based on the given path. Note that there
// is no check about the key existing or being valid.
func NewKeyConfig(configPath string) *KeyConfig {
	return &KeyConfig{
		ConfigPath:         configPath,
		keyPath:            configPath + "/key",
		allowedClientsPath: configPath + "/allowed_clients",
		allowedHostsPath:   configPath + "/allowed_hosts",
		emailToPath:        configPath + "/email_to",
		allowedClientCerts: x509.NewCertPool(),
	}
}

// Exists checks if this key exists.
func (kc *KeyConfig) Exists() (bool, error) {
	isDir, err := isDir(kc.ConfigPath)
	if os.IsNotExist(err) {
		return false, nil
	} else if err != nil {
		return false, err
	}
	if !isDir {
		return false, nil
	}

	isRegular, err := isRegular(kc.keyPath)
	if os.IsNotExist(err) {
		return false, nil
	} else if err != nil {
		return false, err
	}

	return isRegular, nil
}

// LoadClientCerts loads the client certificates allowed for this key.
func (kc *KeyConfig) LoadClientCerts() error {
	rawContents, err := ioutil.ReadFile(kc.allowedClientsPath)
	if os.IsNotExist(err) {
		return nil
	} else if err != nil {
		return err
	}

	if !kc.allowedClientCerts.AppendCertsFromPEM(rawContents) {
		return fmt.Errorf("error parsing client certificate file")
	}

	return nil
}

// LoadAllowedHosts loads the hosts allowed for this key.
func (kc *KeyConfig) LoadAllowedHosts() error {
	contents, err := ioutil.ReadFile(kc.allowedHostsPath)
	if os.IsNotExist(err) {
		return nil
	} else if err != nil {
		return err
	}

	// If the file is there, we want our array to exist, even if it's
	// empty, to avoid authorizing everyone on an empty file (which means
	// authorize noone).
	kc.allowedHosts = make([]string, 1)
	for _, line := range strings.Split(string(contents), "\n") {
		line = strings.TrimSpace(line)
		if line == "" {
			continue
		}

		if net.ParseIP(line) != nil {
			kc.allowedHosts = append(kc.allowedHosts, line)
		} else {
			names, err := net.LookupHost(line)
			if err != nil {
				continue
			}
			kc.allowedHosts = append(kc.allowedHosts, names...)
		}
	}

	return nil
}

// IsAnyCertAllowed checks if any of the given certificates is allowed to
// access this key. If so, it returns the chain for each of them.
func (kc *KeyConfig) IsAnyCertAllowed(
	certs []*x509.Certificate) [][]*x509.Certificate {
	opts := x509.VerifyOptions{
		Roots: kc.allowedClientCerts,
	}
	for _, cert := range certs {
		chains, err := cert.Verify(opts)
		if err == nil && len(chains) > 0 {
			return chains
		}
	}
	return nil
}

// IsHostAllowed checks if the given host is allowed to access this key.
func (kc *KeyConfig) IsHostAllowed(addr string) error {
	if kc.allowedHosts == nil {
		return nil
	}

	host, _, err := net.SplitHostPort(addr)
	if err != nil {
		return err
	}

	for _, allowedHost := range kc.allowedHosts {
		if allowedHost == host {
			return nil
		}
	}

	return fmt.Errorf("host %q not allowed", host)
}

// Key returns the private key.
func (kc *KeyConfig) Key() (key []byte, err error) {
	return ioutil.ReadFile(kc.keyPath)
}

// EmailTo returns the list of addresses to email when this key is accessed.
func (kc *KeyConfig) EmailTo() ([]string, error) {
	contents, err := ioutil.ReadFile(kc.emailToPath)
	if os.IsNotExist(err) {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}

	var emails []string
	for _, line := range strings.Split(string(contents), "\n") {
		email := strings.TrimSpace(line)
		if !strings.Contains(email, "@") {
			continue
		}
		emails = append(emails, email)
	}

	return emails, nil
}
