// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2016-present Datadog, Inc.

package api

import (
	"bytes"
	"context"
	"fmt"
	"io"
	stdlog "log"
	"net/http"
	"net/http/httputil"
	"strconv"
	"strings"
	"time"

	"github.com/DataDog/datadog-agent/pkg/trace/api/apiutil"
	"github.com/DataDog/datadog-agent/pkg/trace/api/internal/header"
	"github.com/DataDog/datadog-agent/pkg/trace/config"
	"github.com/DataDog/datadog-agent/pkg/trace/log"
	"github.com/DataDog/datadog-go/v5/statsd"
)

const (
	validSubdomainSymbols       = "_-."
	validPathSymbols            = "/_-+"
	validPathQueryStringSymbols = "/_-+@?&=.:\""
)

// EvpProxyAllowedHeaders contains the headers that the proxy will forward. All others will be cleared.
var EvpProxyAllowedHeaders = []string{"Content-Type", "Accept-Encoding", "Content-Encoding", "User-Agent", "DD-CI-PROVIDER-NAME"}

// evpProxyEndpointsFromConfig returns the configured list of endpoints to forward payloads to.
func evpProxyEndpointsFromConfig(conf *config.AgentConfig) []config.Endpoint {
	apiKey := conf.EVPProxy.APIKey
	if apiKey == "" {
		apiKey = conf.APIKey()
	}
	endpoint := conf.EVPProxy.DDURL
	if endpoint == "" {
		endpoint = conf.Site
	}
	endpoints := []config.Endpoint{{Host: endpoint, APIKey: apiKey}} // main endpoint
	for host, keys := range conf.EVPProxy.AdditionalEndpoints {
		for _, key := range keys {
			endpoints = append(endpoints, config.Endpoint{
				Host:   host,
				APIKey: key,
			})
		}
	}
	return endpoints
}

func (r *HTTPReceiver) evpProxyHandler(apiVersion int) http.Handler {
	if !r.conf.EVPProxy.Enabled {
		return evpProxyErrorHandler("Has been disabled in config")
	}
	handler := evpProxyForwarder(r.conf, r.statsd)
	return http.StripPrefix(fmt.Sprintf("/evp_proxy/v%d", apiVersion), handler)
}

// evpProxyErrorHandler returns an HTTP handler that will always return
// http.StatusMethodNotAllowed along with a clarification.
func evpProxyErrorHandler(message string) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		msg := fmt.Sprintf("EVPProxy is disabled: %v", message)
		http.Error(w, msg, http.StatusMethodNotAllowed)
	})
}

// evpProxyForwarder creates an http.ReverseProxy which can forward payloads to
// one or more endpoints, based on the request received and the Agent configuration.
// Headers are not proxied, instead we add our own known set of headers.
// See also evpProxyTransport below.
func evpProxyForwarder(conf *config.AgentConfig, statsd statsd.ClientInterface) http.Handler {
	endpoints := evpProxyEndpointsFromConfig(conf)
	logger := stdlog.New(log.NewThrottled(5, 10*time.Second), "EVPProxy: ", 0) // limit to 5 messages every 10 seconds
	return &httputil.ReverseProxy{
		Director: func(req *http.Request) {
			// The X-Forwarded-For header can be abused to fake the origin of requests and we don't need it,
			// so we set it to null to tell ReverseProxy to not set it.
			req.Header["X-Forwarded-For"] = nil
		},
		ErrorLog:  logger,
		Transport: &evpProxyTransport{conf.NewHTTPTransport(), endpoints, conf, NewIDProvider(conf.ContainerProcRoot, conf.ContainerIDFromOriginInfo), statsd},
	}
}

// evpProxyTransport sends HTTPS requests to multiple targets using an
// underlying http.RoundTripper. API keys are set separately for each target.
// When multiple endpoints are in use the response from the first endpoint
// is proxied back to the client, while for all aditional endpoints the
// response is discarded.
type evpProxyTransport struct {
	transport           http.RoundTripper
	endpoints           []config.Endpoint
	conf                *config.AgentConfig
	containerIDProvider IDProvider
	statsd              statsd.ClientInterface
}

func (t *evpProxyTransport) RoundTrip(req *http.Request) (rresp *http.Response, rerr error) {
	if req.Body != nil && t.conf.EVPProxy.MaxPayloadSize > 0 {
		req.Body = apiutil.NewLimitedReader(req.Body, t.conf.EVPProxy.MaxPayloadSize)
	}
	start := time.Now()
	tags := []string{} // these tags are only for the debug metrics, not the payloads we forward
	if ct := req.Header.Get("Content-Type"); ct != "" {
		tags = append(tags, "content_type:"+ct)
	}
	defer func() {
		_ = t.statsd.Count("datadog.trace_agent.evp_proxy.request", 1, tags, 1)
		_ = t.statsd.Count("datadog.trace_agent.evp_proxy.request_bytes", req.ContentLength, tags, 1)
		_ = t.statsd.Timing("datadog.trace_agent.evp_proxy.request_duration_ms", time.Since(start), tags, 1)
		if rerr != nil {
			_ = t.statsd.Count("datadog.trace_agent.evp_proxy.request_error", 1, tags, 1)
		}
	}()

	subdomain := req.Header.Get("X-Datadog-EVP-Subdomain")
	containerID := t.containerIDProvider.GetContainerID(req.Context(), req.Header)
	needsAppKey := (strings.ToLower(req.Header.Get("X-Datadog-NeedsAppKey")) == "true")

	// Sanitize the input, don't accept any valid URL but just some limited subset
	if len(subdomain) == 0 {
		return nil, fmt.Errorf("EVPProxy: no subdomain specified")
	}
	if !isValidSubdomain(subdomain) {
		return nil, fmt.Errorf("EVPProxy: invalid subdomain: %s", subdomain)
	}
	tags = append(tags, "subdomain:"+subdomain)
	if !isValidPath(req.URL.Path) {
		return nil, fmt.Errorf("EVPProxy: invalid target path: %s", req.URL.Path)
	}
	if !isValidQueryString(req.URL.RawQuery) {
		return nil, fmt.Errorf("EVPProxy: invalid query string: %s", req.URL.RawQuery)
	}

	if needsAppKey && t.conf.EVPProxy.ApplicationKey == "" {
		return nil, fmt.Errorf("EVPProxy: ApplicationKey needed but not set")
	}

	// We don't want to forward arbitrary headers, create a copy of the input headers and clear them
	inputHeaders := req.Header
	req.Header = http.Header{}

	// Set standard headers
	req.Header.Set("User-Agent", "") // Set to empty string so Go doesn't set its default
	req.Header.Set("Via", fmt.Sprintf("trace-agent %s", t.conf.AgentVersion))

	// Copy allowed headers from the input request
	for _, header := range EvpProxyAllowedHeaders {
		val := inputHeaders.Get(header)
		if val != "" {
			req.Header.Set(header, val)
		}
	}

	// Set Datadog headers, except API key which is set per-endpoint
	if containerID != "" {
		req.Header.Set(header.ContainerID, containerID)
		if ctags := getContainerTags(t.conf.ContainerTags, containerID); ctags != "" {
			ctagsHeader := normalizeHTTPHeader(ctags)
			req.Header.Set("X-Datadog-Container-Tags", ctagsHeader)
			log.Debugf("Setting header X-Datadog-Container-Tags=%s for evp proxy", ctagsHeader)
		}
	}
	req.Header.Set("X-Datadog-Hostname", t.conf.Hostname)
	req.Header.Set("X-Datadog-AgentDefaultEnv", t.conf.DefaultEnv)
	log.Debugf("Setting headers X-Datadog-Hostnames=%s, X-Datadog-AgentDefaultEnv=%s for evp proxy", t.conf.Hostname, t.conf.DefaultEnv)
	req.Header.Set(header.ContainerID, containerID)
	if needsAppKey {
		req.Header.Set("DD-APPLICATION-KEY", t.conf.EVPProxy.ApplicationKey)
	}

	// Timeout: Our outbound request(s) can't take longer than the WriteTimeout of the server
	timeout := getConfiguredEVPRequestTimeoutDuration(t.conf)
	req.Header.Set("X-Datadog-Timeout", strconv.Itoa((int(timeout.Seconds()))))
	deadline := time.Now().Add(timeout)
	//nolint:govet,lostcancel we don't need to manually cancel this context, we can rely on the parent context being cancelled
	ctx, _ := context.WithDeadline(req.Context(), deadline)
	req = req.WithContext(ctx)

	// Set target URL and API key header (per domain)
	req.URL.Scheme = "https"
	setTarget := func(r *http.Request, host, apiKey string) {
		targetHost := subdomain + "." + host
		r.Host = targetHost
		r.URL.Host = targetHost
		r.Header.Set("DD-API-KEY", apiKey)
	}

	// Shortcut if we only have one endpoint
	if len(t.endpoints) == 1 {
		setTarget(req, t.endpoints[0].Host, t.endpoints[0].APIKey)
		return t.transport.RoundTrip(req)
	}

	// There's more than one destination endpoint
	var slurp []byte
	if req.Body != nil {
		body, err := io.ReadAll(req.Body)
		if err != nil {
			return nil, err
		}
		slurp = body
	}
	for i, endpointDomain := range t.endpoints {
		newreq := req.Clone(req.Context())
		if slurp != nil {
			newreq.Body = io.NopCloser(bytes.NewReader(slurp))
		}
		setTarget(newreq, endpointDomain.Host, endpointDomain.APIKey)
		if i == 0 {
			// given the way we construct the list of targets the main endpoint
			// will be the first one called, we return its response and error
			rresp, rerr = t.transport.RoundTrip(newreq)
			continue
		}

		if resp, err := t.transport.RoundTrip(newreq); err == nil {
			// we discard responses for all subsequent requests
			io.Copy(io.Discard, resp.Body) //nolint:errcheck
			resp.Body.Close()
		} else {
			log.Error(err)
		}
	}
	return rresp, rerr
}

// We don't want to accept any valid URL, we are strict in what we accept as a subdomain, path
// or query string to prevent abusing this API for other purposes than forwarding evp payloads.
func isValidSubdomain(s string) bool {
	for _, c := range s {
		if (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9') && !strings.ContainsRune(validSubdomainSymbols, c) {
			return false
		}
	}
	return true
}

func isValidPath(s string) bool {
	for _, c := range s {
		if (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9') && !strings.ContainsRune(validPathSymbols, c) {
			return false
		}
	}
	return true
}

func isValidQueryString(s string) bool {
	for _, c := range s {
		if (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9') && !strings.ContainsRune(validPathQueryStringSymbols, c) {
			return false
		}
	}
	return true
}
