github.com/aavshr/aws-sdk-go@v1.41.3/aws/ec2metadata/service.go (about)

     1  // Package ec2metadata provides the client for making API calls to the
     2  // EC2 Metadata service.
     3  //
     4  // This package's client can be disabled completely by setting the environment
     5  // variable "AWS_EC2_METADATA_DISABLED=true". This environment variable set to
     6  // true instructs the SDK to disable the EC2 Metadata client. The client cannot
     7  // be used while the environment variable is set to true, (case insensitive).
     8  //
     9  // The endpoint of the EC2 IMDS client can be configured via the environment
    10  // variable, AWS_EC2_METADATA_SERVICE_ENDPOINT when creating the client with a
    11  // Session. See aws/session#Options.EC2IMDSEndpoint for more details.
    12  package ec2metadata
    13  
    14  import (
    15  	"bytes"
    16  	"io"
    17  	"net/http"
    18  	"net/url"
    19  	"os"
    20  	"strconv"
    21  	"strings"
    22  	"time"
    23  
    24  	"github.com/aavshr/aws-sdk-go/aws"
    25  	"github.com/aavshr/aws-sdk-go/aws/awserr"
    26  	"github.com/aavshr/aws-sdk-go/aws/client"
    27  	"github.com/aavshr/aws-sdk-go/aws/client/metadata"
    28  	"github.com/aavshr/aws-sdk-go/aws/corehandlers"
    29  	"github.com/aavshr/aws-sdk-go/aws/request"
    30  )
    31  
    32  const (
    33  	// ServiceName is the name of the service.
    34  	ServiceName          = "ec2metadata"
    35  	disableServiceEnvVar = "AWS_EC2_METADATA_DISABLED"
    36  
    37  	// Headers for Token and TTL
    38  	ttlHeader   = "x-aws-ec2-metadata-token-ttl-seconds"
    39  	tokenHeader = "x-aws-ec2-metadata-token"
    40  
    41  	// Named Handler constants
    42  	fetchTokenHandlerName          = "FetchTokenHandler"
    43  	unmarshalMetadataHandlerName   = "unmarshalMetadataHandler"
    44  	unmarshalTokenHandlerName      = "unmarshalTokenHandler"
    45  	enableTokenProviderHandlerName = "enableTokenProviderHandler"
    46  
    47  	// TTL constants
    48  	defaultTTL          = 21600 * time.Second
    49  	ttlExpirationWindow = 30 * time.Second
    50  )
    51  
    52  // A EC2Metadata is an EC2 Metadata service Client.
    53  type EC2Metadata struct {
    54  	*client.Client
    55  }
    56  
    57  // New creates a new instance of the EC2Metadata client with a session.
    58  // This client is safe to use across multiple goroutines.
    59  //
    60  //
    61  // Example:
    62  //     // Create a EC2Metadata client from just a session.
    63  //     svc := ec2metadata.New(mySession)
    64  //
    65  //     // Create a EC2Metadata client with additional configuration
    66  //     svc := ec2metadata.New(mySession, aws.NewConfig().WithLogLevel(aws.LogDebugHTTPBody))
    67  func New(p client.ConfigProvider, cfgs ...*aws.Config) *EC2Metadata {
    68  	c := p.ClientConfig(ServiceName, cfgs...)
    69  	return NewClient(*c.Config, c.Handlers, c.Endpoint, c.SigningRegion)
    70  }
    71  
    72  // NewClient returns a new EC2Metadata client. Should be used to create
    73  // a client when not using a session. Generally using just New with a session
    74  // is preferred.
    75  //
    76  // Will remove the URL path from the endpoint provided to ensure the EC2 IMDS
    77  // client is able to communicate with the EC2 IMDS API.
    78  //
    79  // If an unmodified HTTP client is provided from the stdlib default, or no client
    80  // the EC2RoleProvider's EC2Metadata HTTP client's timeout will be shortened.
    81  // To disable this set Config.EC2MetadataDisableTimeoutOverride to false. Enabled by default.
    82  func NewClient(cfg aws.Config, handlers request.Handlers, endpoint, signingRegion string, opts ...func(*client.Client)) *EC2Metadata {
    83  	if !aws.BoolValue(cfg.EC2MetadataDisableTimeoutOverride) && httpClientZero(cfg.HTTPClient) {
    84  		// If the http client is unmodified and this feature is not disabled
    85  		// set custom timeouts for EC2Metadata requests.
    86  		cfg.HTTPClient = &http.Client{
    87  			// use a shorter timeout than default because the metadata
    88  			// service is local if it is running, and to fail faster
    89  			// if not running on an ec2 instance.
    90  			Timeout: 1 * time.Second,
    91  		}
    92  		// max number of retries on the client operation
    93  		cfg.MaxRetries = aws.Int(2)
    94  	}
    95  
    96  	if u, err := url.Parse(endpoint); err == nil {
    97  		// Remove path from the endpoint since it will be added by requests.
    98  		// This is an artifact of the SDK adding `/latest` to the endpoint for
    99  		// EC2 IMDS, but this is now moved to the operation definition.
   100  		u.Path = ""
   101  		u.RawPath = ""
   102  		endpoint = u.String()
   103  	}
   104  
   105  	svc := &EC2Metadata{
   106  		Client: client.New(
   107  			cfg,
   108  			metadata.ClientInfo{
   109  				ServiceName: ServiceName,
   110  				ServiceID:   ServiceName,
   111  				Endpoint:    endpoint,
   112  				APIVersion:  "latest",
   113  			},
   114  			handlers,
   115  		),
   116  	}
   117  
   118  	// token provider instance
   119  	tp := newTokenProvider(svc, defaultTTL)
   120  
   121  	// NamedHandler for fetching token
   122  	svc.Handlers.Sign.PushBackNamed(request.NamedHandler{
   123  		Name: fetchTokenHandlerName,
   124  		Fn:   tp.fetchTokenHandler,
   125  	})
   126  	// NamedHandler for enabling token provider
   127  	svc.Handlers.Complete.PushBackNamed(request.NamedHandler{
   128  		Name: enableTokenProviderHandlerName,
   129  		Fn:   tp.enableTokenProviderHandler,
   130  	})
   131  
   132  	svc.Handlers.Unmarshal.PushBackNamed(unmarshalHandler)
   133  	svc.Handlers.UnmarshalError.PushBack(unmarshalError)
   134  	svc.Handlers.Validate.Clear()
   135  	svc.Handlers.Validate.PushBack(validateEndpointHandler)
   136  
   137  	// Disable the EC2 Metadata service if the environment variable is set.
   138  	// This short-circuits the service's functionality to always fail to send
   139  	// requests.
   140  	if strings.ToLower(os.Getenv(disableServiceEnvVar)) == "true" {
   141  		svc.Handlers.Send.SwapNamed(request.NamedHandler{
   142  			Name: corehandlers.SendHandler.Name,
   143  			Fn: func(r *request.Request) {
   144  				r.HTTPResponse = &http.Response{
   145  					Header: http.Header{},
   146  				}
   147  				r.Error = awserr.New(
   148  					request.CanceledErrorCode,
   149  					"EC2 IMDS access disabled via "+disableServiceEnvVar+" env var",
   150  					nil)
   151  			},
   152  		})
   153  	}
   154  
   155  	// Add additional options to the service config
   156  	for _, option := range opts {
   157  		option(svc.Client)
   158  	}
   159  	return svc
   160  }
   161  
   162  func httpClientZero(c *http.Client) bool {
   163  	return c == nil || (c.Transport == nil && c.CheckRedirect == nil && c.Jar == nil && c.Timeout == 0)
   164  }
   165  
   166  type metadataOutput struct {
   167  	Content string
   168  }
   169  
   170  type tokenOutput struct {
   171  	Token string
   172  	TTL   time.Duration
   173  }
   174  
   175  // unmarshal token handler is used to parse the response of a getToken operation
   176  var unmarshalTokenHandler = request.NamedHandler{
   177  	Name: unmarshalTokenHandlerName,
   178  	Fn: func(r *request.Request) {
   179  		defer r.HTTPResponse.Body.Close()
   180  		var b bytes.Buffer
   181  		if _, err := io.Copy(&b, r.HTTPResponse.Body); err != nil {
   182  			r.Error = awserr.NewRequestFailure(awserr.New(request.ErrCodeSerialization,
   183  				"unable to unmarshal EC2 metadata response", err), r.HTTPResponse.StatusCode, r.RequestID)
   184  			return
   185  		}
   186  
   187  		v := r.HTTPResponse.Header.Get(ttlHeader)
   188  		data, ok := r.Data.(*tokenOutput)
   189  		if !ok {
   190  			return
   191  		}
   192  
   193  		data.Token = b.String()
   194  		// TTL is in seconds
   195  		i, err := strconv.ParseInt(v, 10, 64)
   196  		if err != nil {
   197  			r.Error = awserr.NewRequestFailure(awserr.New(request.ParamFormatErrCode,
   198  				"unable to parse EC2 token TTL response", err), r.HTTPResponse.StatusCode, r.RequestID)
   199  			return
   200  		}
   201  		t := time.Duration(i) * time.Second
   202  		data.TTL = t
   203  	},
   204  }
   205  
   206  var unmarshalHandler = request.NamedHandler{
   207  	Name: unmarshalMetadataHandlerName,
   208  	Fn: func(r *request.Request) {
   209  		defer r.HTTPResponse.Body.Close()
   210  		var b bytes.Buffer
   211  		if _, err := io.Copy(&b, r.HTTPResponse.Body); err != nil {
   212  			r.Error = awserr.NewRequestFailure(awserr.New(request.ErrCodeSerialization,
   213  				"unable to unmarshal EC2 metadata response", err), r.HTTPResponse.StatusCode, r.RequestID)
   214  			return
   215  		}
   216  
   217  		if data, ok := r.Data.(*metadataOutput); ok {
   218  			data.Content = b.String()
   219  		}
   220  	},
   221  }
   222  
   223  func unmarshalError(r *request.Request) {
   224  	defer r.HTTPResponse.Body.Close()
   225  	var b bytes.Buffer
   226  
   227  	if _, err := io.Copy(&b, r.HTTPResponse.Body); err != nil {
   228  		r.Error = awserr.NewRequestFailure(
   229  			awserr.New(request.ErrCodeSerialization, "unable to unmarshal EC2 metadata error response", err),
   230  			r.HTTPResponse.StatusCode, r.RequestID)
   231  		return
   232  	}
   233  
   234  	// Response body format is not consistent between metadata endpoints.
   235  	// Grab the error message as a string and include that as the source error
   236  	r.Error = awserr.NewRequestFailure(
   237  		awserr.New("EC2MetadataError", "failed to make EC2Metadata request\n"+b.String(), nil),
   238  		r.HTTPResponse.StatusCode, r.RequestID)
   239  }
   240  
   241  func validateEndpointHandler(r *request.Request) {
   242  	if r.ClientInfo.Endpoint == "" {
   243  		r.Error = aws.ErrMissingEndpoint
   244  	}
   245  }