// Copyright 2015 Light Code Labs, LLC
//
// 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 caddytls

import (
	"crypto/tls"
	"fmt"
	"strings"

	"github.com/mholt/caddy/telemetry"
)

// configGroup is a type that keys configs by their hostname
// (hostnames can have wildcard characters; use the getConfig
// method to get a config by matching its hostname).
type configGroup map[string]*Config

// getConfig gets the config by the first key match for name.
// In other words, "sub.foo.bar" will get the config for "*.foo.bar"
// if that is the closest match. If no match is found, the first
// (random) config will be loaded, which will defer any TLS alerts
// to the certificate validation (this may or may not be ideal;
// let's talk about it if this becomes problematic).
//
// This function follows nearly the same logic to lookup
// a hostname as the getCertificate function uses.
func (cg configGroup) getConfig(name string) *Config {
	name = strings.ToLower(name)

	// exact match? great, let's use it
	if config, ok := cg[name]; ok {
		return config
	}

	// try replacing labels in the name with wildcards until we get a match
	labels := strings.Split(name, ".")
	for i := range labels {
		labels[i] = "*"
		candidate := strings.Join(labels, ".")
		if config, ok := cg[candidate]; ok {
			return config
		}
	}

	// try a config that serves all names (the above
	// loop doesn't try empty string; for hosts defined
	// with only a port, for instance, like ":443") -
	// also known as the default config
	if config, ok := cg[""]; ok {
		return config
	}

	return nil
}

// GetConfigForClient gets a TLS configuration satisfying clientHello.
// In getting the configuration, it abides the rules and settings
// defined in the Config that matches clientHello.ServerName. If no
// tls.Config is set on the matching Config, a nil value is returned.
//
// This method is safe for use as a tls.Config.GetConfigForClient callback.
func (cg configGroup) GetConfigForClient(clientHello *tls.ClientHelloInfo) (*tls.Config, error) {
	config := cg.getConfig(clientHello.ServerName)
	if config != nil {
		return config.tlsConfig, nil
	}
	return nil, nil
}

// ClientHelloInfo is our own version of the standard lib's
// tls.ClientHelloInfo. As of May 2018, any fields populated
// by the Go standard library are not guaranteed to have their
// values in the original order as on the wire.
type ClientHelloInfo struct {
	Version            uint16        `json:"version,omitempty"`
	CipherSuites       []uint16      `json:"cipher_suites,omitempty"`
	Extensions         []uint16      `json:"extensions,omitempty"`
	CompressionMethods []byte        `json:"compression,omitempty"`
	Curves             []tls.CurveID `json:"curves,omitempty"`
	Points             []uint8       `json:"points,omitempty"`

	// Whether a couple of fields are unknown; if not, the key will encode
	// differently to reflect that, as opposed to being known empty values.
	// (some fields may be unknown depending on what package is being used;
	// i.e. the Go standard lib doesn't expose some things)
	// (very important to NOT encode these to JSON)
	ExtensionsUnknown         bool `json:"-"`
	CompressionMethodsUnknown bool `json:"-"`
}

// Key returns a standardized string form of the data in info,
// useful for identifying duplicates.
func (info ClientHelloInfo) Key() string {
	extensions, compressionMethods := "?", "?"
	if !info.ExtensionsUnknown {
		extensions = fmt.Sprintf("%x", info.Extensions)
	}
	if !info.CompressionMethodsUnknown {
		compressionMethods = fmt.Sprintf("%x", info.CompressionMethods)
	}
	return telemetry.FastHash([]byte(fmt.Sprintf("%x-%x-%s-%s-%x-%x",
		info.Version, info.CipherSuites, extensions,
		compressionMethods, info.Curves, info.Points)))
}

// ClientHelloTelemetry determines whether to report
// TLS ClientHellos to telemetry. Disable if doing
// it from a different package.
var ClientHelloTelemetry = true