// Copyright 2019 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package web

import (
	"crypto/tls"
	"crypto/x509"
	"errors"
	"fmt"
	"log/slog"
	"net"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/coreos/go-systemd/v22/activation"
	"github.com/mdlayher/vsock"
	config_util "github.com/prometheus/common/config"
	"go.yaml.in/yaml/v2"
	"golang.org/x/sync/errgroup"
)

var (
	errNoTLSConfig = errors.New("TLS config is not present")
	ErrNoListeners = errors.New("no web listen address or systemd socket flag specified")
)

type Config struct {
	TLSConfig  TLSConfig                     `yaml:"tls_server_config"`
	HTTPConfig HTTPConfig                    `yaml:"http_server_config"`
	Users      map[string]config_util.Secret `yaml:"basic_auth_users"`
}

type TLSConfig struct {
	TLSCert                  string             `yaml:"cert"`
	TLSKey                   config_util.Secret `yaml:"key"`
	ClientCAsText            string             `yaml:"client_ca"`
	TLSCertPath              string             `yaml:"cert_file"`
	TLSKeyPath               string             `yaml:"key_file"`
	ClientAuth               string             `yaml:"client_auth_type"`
	ClientCAs                string             `yaml:"client_ca_file"`
	CipherSuites             []Cipher           `yaml:"cipher_suites"`
	CurvePreferences         []Curve            `yaml:"curve_preferences"`
	MinVersion               TLSVersion         `yaml:"min_version"`
	MaxVersion               TLSVersion         `yaml:"max_version"`
	PreferServerCipherSuites bool               `yaml:"prefer_server_cipher_suites"`
	ClientAllowedSans        []string           `yaml:"client_allowed_sans"`
}

type FlagConfig struct {
	WebListenAddresses *[]string
	WebSystemdSocket   *bool
	WebConfigFile      *string
}

// SetDirectory joins any relative file paths with dir.
func (t *TLSConfig) SetDirectory(dir string) {
	t.TLSCertPath = config_util.JoinDir(dir, t.TLSCertPath)
	t.TLSKeyPath = config_util.JoinDir(dir, t.TLSKeyPath)
	t.ClientCAs = config_util.JoinDir(dir, t.ClientCAs)
}

// VerifyPeerCertificate will check the SAN entries of the client cert if there is configuration for it
func (t *TLSConfig) VerifyPeerCertificate(rawCerts [][]byte, _ [][]*x509.Certificate) error {
	// sender cert comes first, see https://www.rfc-editor.org/rfc/rfc5246#section-7.4.2
	cert, err := x509.ParseCertificate(rawCerts[0])
	if err != nil {
		return fmt.Errorf("error parsing client certificate: %s", err)
	}

	// Build up a slice of strings with all Subject Alternate Name values
	sanValues := append(cert.DNSNames, cert.EmailAddresses...)

	for _, ip := range cert.IPAddresses {
		sanValues = append(sanValues, ip.String())
	}

	for _, uri := range cert.URIs {
		sanValues = append(sanValues, uri.String())
	}

	for _, sanValue := range sanValues {
		for _, allowedSan := range t.ClientAllowedSans {
			if sanValue == allowedSan {
				return nil
			}
		}
	}

	return fmt.Errorf("could not find allowed SANs in client cert, found: %v", t.ClientAllowedSans)
}

type HTTPConfig struct {
	HTTP2  bool              `yaml:"http2"`
	Header map[string]string `yaml:"headers,omitempty"`
}

func getConfig(configPath string) (*Config, error) {
	content, err := os.ReadFile(configPath)
	if err != nil {
		return nil, err
	}
	c := &Config{
		TLSConfig: TLSConfig{
			MinVersion:               tls.VersionTLS12,
			MaxVersion:               tls.VersionTLS13,
			PreferServerCipherSuites: true,
		},
		HTTPConfig: HTTPConfig{HTTP2: true},
	}
	err = yaml.UnmarshalStrict(content, c)
	if err == nil {
		err = validateHeaderConfig(c.HTTPConfig.Header)
	}
	c.TLSConfig.SetDirectory(filepath.Dir(configPath))
	return c, err
}

func getTLSConfig(configPath string) (*tls.Config, error) {
	c, err := getConfig(configPath)
	if err != nil {
		return nil, err
	}
	return ConfigToTLSConfig(&c.TLSConfig)
}

func validateTLSPaths(c *TLSConfig) error {
	if c.TLSCertPath == "" && c.TLSCert == "" &&
		c.TLSKeyPath == "" && c.TLSKey == "" &&
		c.ClientCAs == "" && c.ClientCAsText == "" &&
		c.ClientAuth == "" {
		return errNoTLSConfig
	}

	if c.TLSCertPath == "" && c.TLSCert == "" {
		return errors.New("missing one of cert or cert_file")
	}

	if c.TLSKeyPath == "" && c.TLSKey == "" {
		return errors.New("missing one of key or key_file")
	}

	return nil
}

// ConfigToTLSConfig generates the golang tls.Config from the TLSConfig struct.
func ConfigToTLSConfig(c *TLSConfig) (*tls.Config, error) {
	if err := validateTLSPaths(c); err != nil {
		return nil, err
	}

	loadCert := func() (*tls.Certificate, error) {
		var certData, keyData []byte
		var err error

		if c.TLSCertPath != "" {
			certData, err = os.ReadFile(c.TLSCertPath)
			if err != nil {
				return nil, fmt.Errorf("failed to read cert_file (%s): %s", c.TLSCertPath, err)
			}
		} else {
			certData = []byte(c.TLSCert)
		}

		if c.TLSKeyPath != "" {
			keyData, err = os.ReadFile(c.TLSKeyPath)
			if err != nil {
				return nil, fmt.Errorf("failed to read key_file (%s): %s", c.TLSKeyPath, err)
			}
		} else {
			keyData = []byte(c.TLSKey)
		}

		cert, err := tls.X509KeyPair(certData, keyData)
		if err != nil {
			return nil, fmt.Errorf("failed to load X509KeyPair: %w", err)
		}
		return &cert, nil
	}

	// Confirm that certificate and key paths are valid.
	if _, err := loadCert(); err != nil {
		return nil, err
	}

	cfg := &tls.Config{
		MinVersion:               (uint16)(c.MinVersion),
		MaxVersion:               (uint16)(c.MaxVersion),
		PreferServerCipherSuites: c.PreferServerCipherSuites,
	}

	cfg.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
		return loadCert()
	}

	var cf []uint16
	for _, c := range c.CipherSuites {
		cf = append(cf, (uint16)(c))
	}
	if len(cf) > 0 {
		cfg.CipherSuites = cf
	}

	var cp []tls.CurveID
	for _, c := range c.CurvePreferences {
		cp = append(cp, (tls.CurveID)(c))
	}
	if len(cp) > 0 {
		cfg.CurvePreferences = cp
	}

	if c.ClientCAs != "" {
		clientCAPool := x509.NewCertPool()
		clientCAFile, err := os.ReadFile(c.ClientCAs)
		if err != nil {
			return nil, err
		}
		clientCAPool.AppendCertsFromPEM(clientCAFile)
		cfg.ClientCAs = clientCAPool
	} else if c.ClientCAsText != "" {
		clientCAPool := x509.NewCertPool()
		clientCAPool.AppendCertsFromPEM([]byte(c.ClientCAsText))
		cfg.ClientCAs = clientCAPool
	}

	if c.ClientAllowedSans != nil {
		// verify that the client cert contains an allowed SAN
		cfg.VerifyPeerCertificate = c.VerifyPeerCertificate
	}

	switch c.ClientAuth {
	case "RequestClientCert":
		cfg.ClientAuth = tls.RequestClientCert
	case "RequireAnyClientCert", "RequireClientCert": // Preserved for backwards compatibility.
		cfg.ClientAuth = tls.RequireAnyClientCert
	case "VerifyClientCertIfGiven":
		cfg.ClientAuth = tls.VerifyClientCertIfGiven
	case "RequireAndVerifyClientCert":
		cfg.ClientAuth = tls.RequireAndVerifyClientCert
	case "", "NoClientCert":
		cfg.ClientAuth = tls.NoClientCert
	default:
		return nil, errors.New("invalid ClientAuth: " + c.ClientAuth)
	}

	if (c.ClientCAs != "" || c.ClientCAsText != "") && cfg.ClientAuth == tls.NoClientCert {
		return nil, errors.New("client CA's have been configured without a Client Auth Policy")
	}

	return cfg, nil
}

// ServeMultiple starts the server on the given listeners. The FlagConfig is
// also passed on to Serve.
func ServeMultiple(listeners []net.Listener, server *http.Server, flags *FlagConfig, logger *slog.Logger) error {
	errs := new(errgroup.Group)
	for _, l := range listeners {
		errs.Go(func() error {
			return Serve(l, server, flags, logger)
		})
	}
	return errs.Wait()
}

// ListenAndServe starts the server on addresses given in WebListenAddresses in
// the FlagConfig. When address starts looks like vsock://:{port}, it listens on
// vsock. More info check https://wiki.qemu.org/Features/VirtioVsock .
// Or instead uses systemd socket activated listeners if WebSystemdSocket in the
// FlagConfig is true.
// The FlagConfig is also passed on to ServeMultiple.
func ListenAndServe(server *http.Server, flags *FlagConfig, logger *slog.Logger) error {
	if flags.WebSystemdSocket == nil && (flags.WebListenAddresses == nil || len(*flags.WebListenAddresses) == 0) {
		return ErrNoListeners
	}

	if flags.WebSystemdSocket != nil && *flags.WebSystemdSocket {
		logger.Info("Listening on systemd activated listeners instead of port listeners.")
		listeners, err := activation.Listeners()
		if err != nil {
			return err
		}
		if len(listeners) < 1 {
			return errors.New("no socket activation file descriptors found")
		}
		return ServeMultiple(listeners, server, flags, logger)
	}

	listeners := make([]net.Listener, 0, len(*flags.WebListenAddresses))
	for _, address := range *flags.WebListenAddresses {
		var err error
		var listener net.Listener
		if strings.HasPrefix(address, "vsock://") {
			port, err := parseVsockPort(address)
			if err != nil {
				return err
			}
			listener, err = vsock.Listen(port, nil)
			if err != nil {
				return err
			}
		} else {
			listener, err = net.Listen("tcp", address)
			if err != nil {
				return err
			}
		}
		defer listener.Close()
		listeners = append(listeners, listener)
	}
	return ServeMultiple(listeners, server, flags, logger)
}

func parseVsockPort(address string) (uint32, error) {
	uri, err := url.Parse(address)
	if err != nil {
		return 0, err
	}
	_, portStr, err := net.SplitHostPort(uri.Host)
	if err != nil {
		return 0, err
	}
	port, err := strconv.ParseUint(portStr, 10, 32)
	if err != nil {
		return 0, err
	}
	return uint32(port), nil
}

// Server starts the server on the given listener. Based on the file path
// WebConfigFile in the FlagConfig, TLS or basic auth could be enabled.
func Serve(l net.Listener, server *http.Server, flags *FlagConfig, logger *slog.Logger) error {
	logger.Info("Listening on", "address", l.Addr().String())
	tlsConfigPath := *flags.WebConfigFile
	if tlsConfigPath == "" {
		logger.Info("TLS is disabled.", "http2", false, "address", l.Addr().String())
		return server.Serve(l)
	}

	if err := validateUsers(tlsConfigPath); err != nil {
		return err
	}

	// Setup basic authentication.
	var handler http.Handler = http.DefaultServeMux
	if server.Handler != nil {
		handler = server.Handler
	}

	c, err := getConfig(tlsConfigPath)
	if err != nil {
		return err
	}

	server.Handler = &webHandler{
		tlsConfigPath: tlsConfigPath,
		logger:        logger,
		handler:       handler,
		cache:         newCache(),
	}

	config, err := ConfigToTLSConfig(&c.TLSConfig)
	switch err {
	case nil:
		if !c.HTTPConfig.HTTP2 {
			server.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler))
		}
		// Valid TLS config.
		logger.Info("TLS is enabled.", "http2", c.HTTPConfig.HTTP2, "address", l.Addr().String())
	case errNoTLSConfig:
		// No TLS config, back to plain HTTP.
		logger.Info("TLS is disabled.", "http2", false, "address", l.Addr().String())
		return server.Serve(l)
	default:
		// Invalid TLS config.
		return err
	}

	server.TLSConfig = config

	// Set the GetConfigForClient method of the HTTPS server so that the config
	// and certs are reloaded on new connections.
	server.TLSConfig.GetConfigForClient = func(*tls.ClientHelloInfo) (*tls.Config, error) {
		config, err := getTLSConfig(tlsConfigPath)
		if err != nil {
			return nil, err
		}
		config.NextProtos = server.TLSConfig.NextProtos
		return config, nil
	}
	return server.ServeTLS(l, "", "")
}

// Validate configuration file by reading the configuration and the certificates.
func Validate(tlsConfigPath string) error {
	if tlsConfigPath == "" {
		return nil
	}
	if err := validateUsers(tlsConfigPath); err != nil {
		return err
	}
	c, err := getConfig(tlsConfigPath)
	if err != nil {
		return err
	}
	_, err = ConfigToTLSConfig(&c.TLSConfig)
	if err == errNoTLSConfig {
		return nil
	}
	return err
}

type Cipher uint16

func (c *Cipher) UnmarshalYAML(unmarshal func(interface{}) error) error {
	var s string
	err := unmarshal(&s)
	if err != nil {
		return err
	}
	for _, cs := range tls.CipherSuites() {
		if cs.Name == s {
			*c = (Cipher)(cs.ID)
			return nil
		}
	}
	return errors.New("unknown cipher: " + s)
}

func (c Cipher) MarshalYAML() (interface{}, error) {
	return tls.CipherSuiteName((uint16)(c)), nil
}

type Curve tls.CurveID

var curves = map[string]Curve{
	"CurveP256": (Curve)(tls.CurveP256),
	"CurveP384": (Curve)(tls.CurveP384),
	"CurveP521": (Curve)(tls.CurveP521),
	"X25519":    (Curve)(tls.X25519),
}

func (c *Curve) UnmarshalYAML(unmarshal func(interface{}) error) error {
	var s string
	err := unmarshal(&s)
	if err != nil {
		return err
	}
	if curveid, ok := curves[s]; ok {
		*c = curveid
		return nil
	}
	return errors.New("unknown curve: " + s)
}

func (c *Curve) MarshalYAML() (interface{}, error) {
	for s, curveid := range curves {
		if *c == curveid {
			return s, nil
		}
	}
	return fmt.Sprintf("%v", c), nil
}

type TLSVersion uint16

var tlsVersions = map[string]TLSVersion{
	"TLS13": (TLSVersion)(tls.VersionTLS13),
	"TLS12": (TLSVersion)(tls.VersionTLS12),
	"TLS11": (TLSVersion)(tls.VersionTLS11),
	"TLS10": (TLSVersion)(tls.VersionTLS10),
}

func (tv *TLSVersion) UnmarshalYAML(unmarshal func(interface{}) error) error {
	var s string
	err := unmarshal(&s)
	if err != nil {
		return err
	}
	if v, ok := tlsVersions[s]; ok {
		*tv = v
		return nil
	}
	return errors.New("unknown TLS version: " + s)
}

func (tv *TLSVersion) MarshalYAML() (interface{}, error) {
	for s, v := range tlsVersions {
		if *tv == v {
			return s, nil
		}
	}
	return fmt.Sprintf("%v", tv), nil
}

// Listen starts the server on the given address. Based on the file
// tlsConfigPath, TLS or basic auth could be enabled.
//
// Deprecated: Use ListenAndServe instead.
func Listen(server *http.Server, flags *FlagConfig, logger *slog.Logger) error {
	return ListenAndServe(server, flags, logger)
}
