// Package ec2metadata provides the client for making API calls to the
// EC2 Metadata service.
//
// This package's client can be disabled completely by setting the environment
// variable "AWS_EC2_METADATA_DISABLED=true". This environment variable set to
// true instructs the SDK to disable the EC2 Metadata client. The client cannot
// be used while the environment variable is set to true, (case insensitive).
//
// The endpoint of the EC2 IMDS client can be configured via the environment
// variable, AWS_EC2_METADATA_SERVICE_ENDPOINT when creating the client with a
// Session. See aws/session#Options.EC2IMDSEndpoint for more details.
package ec2metadata

import (
	"bytes"
	"io"
	"net/http"
	"net/url"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/awserr"
	"github.com/aws/aws-sdk-go/aws/client"
	"github.com/aws/aws-sdk-go/aws/client/metadata"
	"github.com/aws/aws-sdk-go/aws/corehandlers"
	"github.com/aws/aws-sdk-go/aws/request"
)

const (
	// ServiceName is the name of the service.
	ServiceName          = "ec2metadata"
	disableServiceEnvVar = "AWS_EC2_METADATA_DISABLED"

	// Headers for Token and TTL
	ttlHeader   = "x-aws-ec2-metadata-token-ttl-seconds"
	tokenHeader = "x-aws-ec2-metadata-token"

	// Named Handler constants
	fetchTokenHandlerName          = "FetchTokenHandler"
	unmarshalMetadataHandlerName   = "unmarshalMetadataHandler"
	unmarshalTokenHandlerName      = "unmarshalTokenHandler"
	enableTokenProviderHandlerName = "enableTokenProviderHandler"

	// TTL constants
	defaultTTL          = 21600 * time.Second
	ttlExpirationWindow = 30 * time.Second
)

// A EC2Metadata is an EC2 Metadata service Client.
type EC2Metadata struct {
	*client.Client
}

// New creates a new instance of the EC2Metadata client with a session.
// This client is safe to use across multiple goroutines.
//
// Example:
//
//	// Create a EC2Metadata client from just a session.
//	svc := ec2metadata.New(mySession)
//
//	// Create a EC2Metadata client with additional configuration
//	svc := ec2metadata.New(mySession, aws.NewConfig().WithLogLevel(aws.LogDebugHTTPBody))
func New(p client.ConfigProvider, cfgs ...*aws.Config) *EC2Metadata {
	c := p.ClientConfig(ServiceName, cfgs...)
	return NewClient(*c.Config, c.Handlers, c.Endpoint, c.SigningRegion)
}

// NewClient returns a new EC2Metadata client. Should be used to create
// a client when not using a session. Generally using just New with a session
// is preferred.
//
// Will remove the URL path from the endpoint provided to ensure the EC2 IMDS
// client is able to communicate with the EC2 IMDS API.
//
// If an unmodified HTTP client is provided from the stdlib default, or no client
// the EC2RoleProvider's EC2Metadata HTTP client's timeout will be shortened.
// To disable this set Config.EC2MetadataDisableTimeoutOverride to false. Enabled by default.
func NewClient(cfg aws.Config, handlers request.Handlers, endpoint, signingRegion string, opts ...func(*client.Client)) *EC2Metadata {
	if !aws.BoolValue(cfg.EC2MetadataDisableTimeoutOverride) && httpClientZero(cfg.HTTPClient) {
		// If the http client is unmodified and this feature is not disabled
		// set custom timeouts for EC2Metadata requests.
		cfg.HTTPClient = &http.Client{
			// use a shorter timeout than default because the metadata
			// service is local if it is running, and to fail faster
			// if not running on an ec2 instance.
			Timeout: 1 * time.Second,
		}
		// max number of retries on the client operation
		cfg.MaxRetries = aws.Int(2)
	}

	if u, err := url.Parse(endpoint); err == nil {
		// Remove path from the endpoint since it will be added by requests.
		// This is an artifact of the SDK adding `/latest` to the endpoint for
		// EC2 IMDS, but this is now moved to the operation definition.
		u.Path = ""
		u.RawPath = ""
		endpoint = u.String()
	}

	svc := &EC2Metadata{
		Client: client.New(
			cfg,
			metadata.ClientInfo{
				ServiceName: ServiceName,
				ServiceID:   ServiceName,
				Endpoint:    endpoint,
				APIVersion:  "latest",
			},
			handlers,
		),
	}

	// token provider instance
	tp := newTokenProvider(svc, defaultTTL)

	// NamedHandler for fetching token
	svc.Handlers.Sign.PushBackNamed(request.NamedHandler{
		Name: fetchTokenHandlerName,
		Fn:   tp.fetchTokenHandler,
	})
	// NamedHandler for enabling token provider
	svc.Handlers.Complete.PushBackNamed(request.NamedHandler{
		Name: enableTokenProviderHandlerName,
		Fn:   tp.enableTokenProviderHandler,
	})

	svc.Handlers.Unmarshal.PushBackNamed(unmarshalHandler)
	svc.Handlers.UnmarshalError.PushBack(unmarshalError)
	svc.Handlers.Validate.Clear()
	svc.Handlers.Validate.PushBack(validateEndpointHandler)

	// Disable the EC2 Metadata service if the environment variable is set.
	// This short-circuits the service's functionality to always fail to send
	// requests.
	if strings.ToLower(os.Getenv(disableServiceEnvVar)) == "true" {
		svc.Handlers.Send.SwapNamed(request.NamedHandler{
			Name: corehandlers.SendHandler.Name,
			Fn: func(r *request.Request) {
				r.HTTPResponse = &http.Response{
					Header: http.Header{},
				}
				r.Error = awserr.New(
					request.CanceledErrorCode,
					"EC2 IMDS access disabled via "+disableServiceEnvVar+" env var",
					nil)
			},
		})
	}

	// Add additional options to the service config
	for _, option := range opts {
		option(svc.Client)
	}
	return svc
}

func httpClientZero(c *http.Client) bool {
	return c == nil || (c.Transport == nil && c.CheckRedirect == nil && c.Jar == nil && c.Timeout == 0)
}

type metadataOutput struct {
	Content string
}

type tokenOutput struct {
	Token string
	TTL   time.Duration
}

// unmarshal token handler is used to parse the response of a getToken operation
var unmarshalTokenHandler = request.NamedHandler{
	Name: unmarshalTokenHandlerName,
	Fn: func(r *request.Request) {
		defer r.HTTPResponse.Body.Close()
		var b bytes.Buffer
		if _, err := io.Copy(&b, r.HTTPResponse.Body); err != nil {
			r.Error = awserr.NewRequestFailure(awserr.New(request.ErrCodeSerialization,
				"unable to unmarshal EC2 metadata response", err), r.HTTPResponse.StatusCode, r.RequestID)
			return
		}

		v := r.HTTPResponse.Header.Get(ttlHeader)
		data, ok := r.Data.(*tokenOutput)
		if !ok {
			return
		}

		data.Token = b.String()
		// TTL is in seconds
		i, err := strconv.ParseInt(v, 10, 64)
		if err != nil {
			r.Error = awserr.NewRequestFailure(awserr.New(request.ParamFormatErrCode,
				"unable to parse EC2 token TTL response", err), r.HTTPResponse.StatusCode, r.RequestID)
			return
		}
		t := time.Duration(i) * time.Second
		data.TTL = t
	},
}

var unmarshalHandler = request.NamedHandler{
	Name: unmarshalMetadataHandlerName,
	Fn: func(r *request.Request) {
		defer r.HTTPResponse.Body.Close()
		var b bytes.Buffer
		if _, err := io.Copy(&b, r.HTTPResponse.Body); err != nil {
			r.Error = awserr.NewRequestFailure(awserr.New(request.ErrCodeSerialization,
				"unable to unmarshal EC2 metadata response", err), r.HTTPResponse.StatusCode, r.RequestID)
			return
		}

		if data, ok := r.Data.(*metadataOutput); ok {
			data.Content = b.String()
		}
	},
}

func unmarshalError(r *request.Request) {
	defer r.HTTPResponse.Body.Close()
	var b bytes.Buffer

	if _, err := io.Copy(&b, r.HTTPResponse.Body); err != nil {
		r.Error = awserr.NewRequestFailure(
			awserr.New(request.ErrCodeSerialization, "unable to unmarshal EC2 metadata error response", err),
			r.HTTPResponse.StatusCode, r.RequestID)
		return
	}

	// Response body format is not consistent between metadata endpoints.
	// Grab the error message as a string and include that as the source error
	r.Error = awserr.NewRequestFailure(
		awserr.New("EC2MetadataError", "failed to make EC2Metadata request\n"+b.String(), nil),
		r.HTTPResponse.StatusCode, r.RequestID)
}

func validateEndpointHandler(r *request.Request) {
	if r.ClientInfo.Endpoint == "" {
		r.Error = aws.ErrMissingEndpoint
	}
}
