package caddytls

import (
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"io/ioutil"

	"net/url"
	"strings"

	"github.com/mholt/caddy"
	"github.com/xenolf/lego/acme"
)

// Config describes how TLS should be configured and used.
type Config struct {
	// The hostname or class of hostnames this config is
	// designated for; can contain wildcard characters
	// according to RFC 6125 ยง6.4.3 - this field MUST
	// be set in order for things to work as expected
	Hostname string

	// Whether TLS is enabled
	Enabled bool

	// Minimum and maximum protocol versions to allow
	ProtocolMinVersion uint16
	ProtocolMaxVersion uint16

	// The list of cipher suites; first should be
	// TLS_FALLBACK_SCSV to prevent degrade attacks
	Ciphers []uint16

	// Whether to prefer server cipher suites
	PreferServerCipherSuites bool

	// The list of preferred curves
	CurvePreferences []tls.CurveID

	// Client authentication policy
	ClientAuth tls.ClientAuthType

	// List of client CA certificates to allow, if
	// client authentication is enabled
	ClientCerts []string

	// Manual means user provides own certs and keys
	Manual bool

	// Managed means config qualifies for implicit,
	// automatic, managed TLS; as opposed to the user
	// providing and managing the certificate manually
	Managed bool

	// OnDemand means the class of hostnames this
	// config applies to may obtain and manage
	// certificates at handshake-time (as opposed
	// to pre-loaded at startup); OnDemand certs
	// will be managed the same way as preloaded
	// ones, however, if an OnDemand cert fails to
	// renew, it is removed from the in-memory
	// cache; if this is true, Managed must
	// necessarily be true
	OnDemand bool

	// SelfSigned means that this hostname is
	// served with a self-signed certificate
	// that we generated in memory for convenience
	SelfSigned bool

	// The endpoint of the directory for the ACME
	// CA we are to use
	CAUrl string

	// The host (ONLY the host, not port) to listen
	// on if necessary to start a listener to solve
	// an ACME challenge
	ListenHost string

	// The alternate port (ONLY port, not host)
	// to use for the ACME HTTP challenge; this
	// port will be used if we proxy challenges
	// coming in on port 80 to this alternate port
	AltHTTPPort string

	// The string identifier of the DNS provider
	// to use when solving the ACME DNS challenge
	DNSProvider string

	// The email address to use when creating or
	// using an ACME account (fun fact: if this
	// is set to "off" then this config will not
	// qualify for managed TLS)
	ACMEEmail string

	// The type of key to use when generating
	// certificates
	KeyType acme.KeyType

	// The storage creator; use StorageFor() to get a guaranteed
	// non-nil Storage instance. Note, Caddy may call this frequently
	// so implementors are encouraged to cache any heavy instantiations.
	StorageProvider string

	// The state needed to operate on-demand TLS
	OnDemandState OnDemandState

	// Add the must staple TLS extension to the CSR generated by lego/acme
	MustStaple bool
}

// OnDemandState contains some state relevant for providing
// on-demand TLS.
type OnDemandState struct {
	// The number of certificates that have been issued on-demand
	// by this config. It is only safe to modify this count atomically.
	// If it reaches MaxObtain, on-demand issuances must fail.
	ObtainedCount int32

	// Set from max_certs in tls config, it specifies the
	// maximum number of certificates that can be issued.
	MaxObtain int32
}

// ObtainCert obtains a certificate for name using c, as long
// as a certificate does not already exist in storage for that
// name. The name must qualify and c must be flagged as Managed.
// This function is a no-op if storage already has a certificate
// for name.
//
// It only obtains and stores certificates (and their keys),
// it does not load them into memory. If allowPrompts is true,
// the user may be shown a prompt.
func (c *Config) ObtainCert(name string, allowPrompts bool) error {
	if !c.Managed || !HostQualifies(name) {
		return nil
	}

	storage, err := c.StorageFor(c.CAUrl)
	if err != nil {
		return err
	}
	siteExists, err := storage.SiteExists(name)
	if err != nil {
		return err
	}
	if siteExists {
		return nil
	}
	if c.ACMEEmail == "" {
		c.ACMEEmail = getEmail(storage, allowPrompts)
	}

	client, err := newACMEClient(c, allowPrompts)
	if err != nil {
		return err
	}
	return client.Obtain(name)
}

// RenewCert renews the certificate for name using c. It stows the
// renewed certificate and its assets in storage if successful.
func (c *Config) RenewCert(name string, allowPrompts bool) error {
	client, err := newACMEClient(c, allowPrompts)
	if err != nil {
		return err
	}
	return client.Renew(name)
}

// StorageFor obtains a TLS Storage instance for the given CA URL which should
// be unique for every different ACME CA. If a StorageCreator is set on this
// Config, it will be used. Otherwise the default file storage implementation
// is used. When the error is nil, this is guaranteed to return a non-nil
// Storage instance.
func (c *Config) StorageFor(caURL string) (Storage, error) {
	// Validate CA URL
	if caURL == "" {
		caURL = DefaultCAUrl
	}
	if caURL == "" {
		return nil, fmt.Errorf("cannot create storage without CA URL")
	}
	caURL = strings.ToLower(caURL)

	// scheme required or host will be parsed as path (as of Go 1.6)
	if !strings.Contains(caURL, "://") {
		caURL = "https://" + caURL
	}

	u, err := url.Parse(caURL)
	if err != nil {
		return nil, fmt.Errorf("%s: unable to parse CA URL: %v", caURL, err)
	}

	if u.Host == "" {
		return nil, fmt.Errorf("%s: no host in CA URL", caURL)
	}

	// Create the storage based on the URL
	var s Storage
	if c.StorageProvider == "" {
		c.StorageProvider = "file"
	}

	creator, ok := storageProviders[c.StorageProvider]
	if !ok {
		return nil, fmt.Errorf("%s: Unknown storage: %v", caURL, c.StorageProvider)
	}

	s, err = creator(u)
	if err != nil {
		return nil, fmt.Errorf("%s: unable to create custom storage '%v': %v", caURL, c.StorageProvider, err)
	}

	return s, nil
}

// MakeTLSConfig reduces configs into a single tls.Config.
// If TLS is to be disabled, a nil tls.Config will be returned.
func MakeTLSConfig(configs []*Config) (*tls.Config, error) {
	if len(configs) == 0 {
		return nil, nil
	}

	config := new(tls.Config)
	ciphersAdded := make(map[uint16]struct{})
	curvesAdded := make(map[tls.CurveID]struct{})
	configMap := make(configGroup)

	for i, cfg := range configs {
		if cfg == nil {
			// avoid nil pointer dereference below
			configs[i] = new(Config)
			continue
		}

		// Key this config by its hostname; this
		// overwrites configs with the same hostname
		configMap[cfg.Hostname] = cfg

		// Can't serve TLS and not-TLS on same port
		if i > 0 && cfg.Enabled != configs[i-1].Enabled {
			thisConfProto, lastConfProto := "not TLS", "not TLS"
			if cfg.Enabled {
				thisConfProto = "TLS"
			}
			if configs[i-1].Enabled {
				lastConfProto = "TLS"
			}
			return nil, fmt.Errorf("cannot multiplex %s (%s) and %s (%s) on same listener",
				configs[i-1].Hostname, lastConfProto, cfg.Hostname, thisConfProto)
		}

		if !cfg.Enabled {
			continue
		}

		// Union cipher suites
		for _, ciph := range cfg.Ciphers {
			if _, ok := ciphersAdded[ciph]; !ok {
				ciphersAdded[ciph] = struct{}{}
				config.CipherSuites = append(config.CipherSuites, ciph)
			}
		}

		// Can't resolve conflicting PreferServerCipherSuites settings
		if i > 0 && cfg.PreferServerCipherSuites != configs[i-1].PreferServerCipherSuites {
			return nil, fmt.Errorf("cannot both PreferServerCipherSuites and not prefer them")
		}
		config.PreferServerCipherSuites = cfg.PreferServerCipherSuites

		// Union curves
		for _, curv := range cfg.CurvePreferences {
			if _, ok := curvesAdded[curv]; !ok {
				curvesAdded[curv] = struct{}{}
				config.CurvePreferences = append(config.CurvePreferences, curv)
			}
		}

		// Go with the widest range of protocol versions
		if config.MinVersion == 0 || cfg.ProtocolMinVersion < config.MinVersion {
			config.MinVersion = cfg.ProtocolMinVersion
		}
		if cfg.ProtocolMaxVersion > config.MaxVersion {
			config.MaxVersion = cfg.ProtocolMaxVersion
		}

		// Go with the strictest ClientAuth type
		if cfg.ClientAuth > config.ClientAuth {
			config.ClientAuth = cfg.ClientAuth
		}
	}

	// Is TLS disabled? If so, we're done here.
	// By now, we know that all configs agree
	// whether it is or not, so we can just look
	// at the first one.
	if len(configs) == 0 || !configs[0].Enabled {
		return nil, nil
	}

	// Default cipher suites
	if len(config.CipherSuites) == 0 {
		config.CipherSuites = defaultCiphers
	}

	// For security, ensure TLS_FALLBACK_SCSV is always included first
	if len(config.CipherSuites) == 0 || config.CipherSuites[0] != tls.TLS_FALLBACK_SCSV {
		config.CipherSuites = append([]uint16{tls.TLS_FALLBACK_SCSV}, config.CipherSuites...)
	}

	// Set up client authentication if enabled
	if config.ClientAuth != tls.NoClientCert {
		pool := x509.NewCertPool()
		clientCertsAdded := make(map[string]struct{})
		for _, cfg := range configs {
			for _, caFile := range cfg.ClientCerts {
				// don't add cert to pool more than once
				if _, ok := clientCertsAdded[caFile]; ok {
					continue
				}
				clientCertsAdded[caFile] = struct{}{}

				// Any client with a certificate from this CA will be allowed to connect
				caCrt, err := ioutil.ReadFile(caFile)
				if err != nil {
					return nil, err
				}

				if !pool.AppendCertsFromPEM(caCrt) {
					return nil, fmt.Errorf("error loading client certificate '%s': no certificates were successfully parsed", caFile)
				}
			}
		}
		config.ClientCAs = pool
	}

	// Associate the GetCertificate callback, or almost nothing we just did will work
	config.GetCertificate = configMap.GetCertificate

	return config, nil
}

// ConfigGetter gets a Config keyed by key.
type ConfigGetter func(c *caddy.Controller) *Config

var configGetters = make(map[string]ConfigGetter)

// RegisterConfigGetter registers fn as the way to get a
// Config for server type serverType.
func RegisterConfigGetter(serverType string, fn ConfigGetter) {
	configGetters[serverType] = fn
}

// SetDefaultTLSParams sets the default TLS cipher suites, protocol versions,
// and server preferences of a server.Config if they were not previously set
// (it does not overwrite; only fills in missing values).
func SetDefaultTLSParams(config *Config) {
	// If no ciphers provided, use default list
	if len(config.Ciphers) == 0 {
		config.Ciphers = defaultCiphers
	}

	// Not a cipher suite, but still important for mitigating protocol downgrade attacks
	// (prepend since having it at end breaks http2 due to non-h2-approved suites before it)
	config.Ciphers = append([]uint16{tls.TLS_FALLBACK_SCSV}, config.Ciphers...)

	// Set default protocol min and max versions - must balance compatibility and security
	if config.ProtocolMinVersion == 0 {
		config.ProtocolMinVersion = tls.VersionTLS11
	}
	if config.ProtocolMaxVersion == 0 {
		config.ProtocolMaxVersion = tls.VersionTLS12
	}

	// Prefer server cipher suites
	config.PreferServerCipherSuites = true
}

// Map of supported key types
var supportedKeyTypes = map[string]acme.KeyType{
	"P384":    acme.EC384,
	"P256":    acme.EC256,
	"RSA8192": acme.RSA8192,
	"RSA4096": acme.RSA4096,
	"RSA2048": acme.RSA2048,
}

// Map of supported protocols.
// HTTP/2 only supports TLS 1.2 and higher.
var supportedProtocols = map[string]uint16{
	"tls1.0": tls.VersionTLS10,
	"tls1.1": tls.VersionTLS11,
	"tls1.2": tls.VersionTLS12,
}

// Map of supported ciphers, used only for parsing config.
//
// Note that, at time of writing, HTTP/2 blacklists 276 cipher suites,
// including all but four of the suites below (the four GCM suites).
// See https://http2.github.io/http2-spec/#BadCipherSuites
//
// TLS_FALLBACK_SCSV is not in this list because we manually ensure
// it is always added (even though it is not technically a cipher suite).
//
// This map, like any map, is NOT ORDERED. Do not range over this map.
var supportedCiphersMap = map[string]uint16{
	"ECDHE-RSA-AES256-GCM-SHA384":   tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
	"ECDHE-ECDSA-AES256-GCM-SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
	"ECDHE-RSA-AES128-GCM-SHA256":   tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
	"ECDHE-ECDSA-AES128-GCM-SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
	"ECDHE-RSA-AES128-CBC-SHA":      tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
	"ECDHE-RSA-AES256-CBC-SHA":      tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
	"ECDHE-ECDSA-AES256-CBC-SHA":    tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
	"ECDHE-ECDSA-AES128-CBC-SHA":    tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
	"RSA-AES128-CBC-SHA":            tls.TLS_RSA_WITH_AES_128_CBC_SHA,
	"RSA-AES256-CBC-SHA":            tls.TLS_RSA_WITH_AES_256_CBC_SHA,
	"ECDHE-RSA-3DES-EDE-CBC-SHA":    tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
	"RSA-3DES-EDE-CBC-SHA":          tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
}

// List of all the ciphers we want to use by default
var defaultCiphers = []uint16{
	tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
	tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
	tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
	tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
	tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
	tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
	tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
	tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
	tls.TLS_RSA_WITH_AES_256_CBC_SHA,
	tls.TLS_RSA_WITH_AES_128_CBC_SHA,
}

// Map of supported curves
// https://golang.org/pkg/crypto/tls/#CurveID
var supportedCurvesMap = map[string]tls.CurveID{
	"P256": tls.CurveP256,
	"P384": tls.CurveP384,
	"P521": tls.CurveP521,
}

const (
	// HTTPChallengePort is the officially designated port for
	// the HTTP challenge.
	HTTPChallengePort = "80"

	// TLSSNIChallengePort is the officially designated port for
	// the TLS-SNI challenge.
	TLSSNIChallengePort = "443"

	// DefaultHTTPAlternatePort is the port on which the ACME
	// client will open a listener and solve the HTTP challenge.
	// If this alternate port is used instead of the default
	// port, then whatever is listening on the default port must
	// be capable of proxying or forwarding the request to this
	// alternate port.
	DefaultHTTPAlternatePort = "5033"
)