# frozen_string_literal: truerequire'time'require'net/http'moduleAws# An auto-refreshing credential provider that loads credentials from# EC2 instances.## instance_credentials = Aws::InstanceProfileCredentials.new# ec2 = Aws::EC2::Client.new(credentials: instance_credentials)classInstanceProfileCredentialsincludeCredentialProviderincludeRefreshingCredentials# @api privateclassNon200Response<RuntimeError;end# @api privateclassTokenRetrivalError<RuntimeError;end# @api privateclassTokenExpiredError<RuntimeError;end# These are the errors we trap when attempting to talk to the# instance metadata service. Any of these imply the service# is not present, no responding or some other non-recoverable# error.# @api privateNETWORK_ERRORS=[Errno::EHOSTUNREACH,Errno::ECONNREFUSED,Errno::EHOSTDOWN,Errno::ENETUNREACH,SocketError,Timeout::Error,Non200Response].freeze# Path base for GET request for profile and credentials# @api privateMETADATA_PATH_BASE='/latest/meta-data/iam/security-credentials/'.freeze# Path for PUT request for token# @api privateMETADATA_TOKEN_PATH='/latest/api/token'.freeze# @param [Hash] options# @option options [Integer] :retries (1) Number of times to retry# when retrieving credentials.# @option options [String] :endpoint ('http://169.254.169.254') The IMDS# endpoint. This option has precedence over the :endpoint_mode.# @option options [String] :endpoint_mode ('IPv4') The endpoint mode for# the instance metadata service. This is either 'IPv4' ('169.254.169.254')# or 'IPv6' ('[fd00:ec2::254]').# @option options [String] :ip_address ('169.254.169.254') Deprecated. Use# :endpoint instead. The IP address for the endpoint.# @option options [Integer] :port (80)# @option options [Float] :http_open_timeout (1)# @option options [Float] :http_read_timeout (1)# @option options [Numeric, Proc] :delay By default, failures are retried# with exponential back-off, i.e. `sleep(1.2 ** num_failures)`. You can# pass a number of seconds to sleep between failed attempts, or# a Proc that accepts the number of failures.# @option options [IO] :http_debug_output (nil) HTTP wire# traces are sent to this object. You can specify something# like $stdout.# @option options [Integer] :token_ttl Time-to-Live in seconds for EC2# Metadata Token used for fetching Metadata Profile Credentials, defaults# to 21600 seconds# @option options [Callable] before_refresh Proc called before# credentials are refreshed. `before_refresh` is called# with an instance of this object when# AWS credentials are required and need to be refreshed.definitialize(options={})@retries=options[:retries]||1endpoint_mode=resolve_endpoint_mode(options)@endpoint=resolve_endpoint(options,endpoint_mode)@port=options[:port]||80@http_open_timeout=options[:http_open_timeout]||1@http_read_timeout=options[:http_read_timeout]||1@http_debug_output=options[:http_debug_output]@backoff=backoff(options[:backoff])@token_ttl=options[:token_ttl]||21_600@token=nil@no_refresh_until=nil@async_refresh=falsesuperend# @return [Integer] Number of times to retry when retrieving credentials# from the instance metadata service. Defaults to 0 when resolving from# the default credential chain ({Aws::CredentialProviderChain}).attr_reader:retriesprivatedefresolve_endpoint_mode(options)value=options[:endpoint_mode]value||=ENV['AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE']value||=Aws.shared_config.ec2_metadata_service_endpoint_mode(profile: options[:profile])value||'IPv4'enddefresolve_endpoint(options,endpoint_mode)value=options[:endpoint]||options[:ip_address]value||=ENV['AWS_EC2_METADATA_SERVICE_ENDPOINT']value||=Aws.shared_config.ec2_metadata_service_endpoint(profile: options[:profile])returnvalueifvaluecaseendpoint_mode.downcasewhen'ipv4'then'http://169.254.169.254'when'ipv6'then'http://[fd00:ec2::254]'elseraiseArgumentError,':endpoint_mode is not valid, expected IPv4 or IPv6, '\"got: #{endpoint_mode}"endenddefbackoff(backoff)casebackoffwhenProcthenbackoffwhenNumericthen->(_){sleep(backoff)}else->(num_failures){Kernel.sleep(1.2**num_failures)}endenddefrefreshif@no_refresh_until&&@no_refresh_until>Time.nowwarn_expired_credentialsreturnend# Retry loading credentials up to 3 times is the instance metadata# service is responding but is returning invalid JSON documents# in response to the GET profile credentials call.beginretry_errors([Aws::Json::ParseError,StandardError],max_retries: 3)doc=Aws::Json.load(get_credentials.to_s)ifempty_credentials?(@credentials)@credentials=Credentials.new(c['AccessKeyId'],c['SecretAccessKey'],c['Token'])@expiration=c['Expiration']?Time.iso8601(c['Expiration']):nilif@expiration&&@expiration<Time.now@no_refresh_until=Time.now+refresh_offsetwarn_expired_credentialsendelse# credentials are already set, update them only if the new ones are not emptyif!c['AccessKeyId']||c['AccessKeyId'].empty?# error getting new credentials@no_refresh_until=Time.now+refresh_offsetwarn_expired_credentialselse@credentials=Credentials.new(c['AccessKeyId'],c['SecretAccessKey'],c['Token'])@expiration=c['Expiration']?Time.iso8601(c['Expiration']):nilif@expiration&&@expiration<Time.now@no_refresh_until=Time.now+refresh_offsetwarn_expired_credentialsendendendendrescueAws::Json::ParseErrorraiseAws::Errors::MetadataParserErrorendenddefget_credentials# Retry loading credentials a configurable number of times if# the instance metadata service is not responding.if_metadata_disabled?'{}'elsebeginretry_errors(NETWORK_ERRORS,max_retries: @retries)doopen_connectiondo|conn|# attempt to fetch token to start secure flow first# and rescue to failoverbeginretry_errors(NETWORK_ERRORS,max_retries: @retries)dounlesstoken_set?created_time=Time.nowtoken_value,ttl=http_put(conn,METADATA_TOKEN_PATH,@token_ttl)@token=Token.new(token_value,ttl,created_time)iftoken_value&&ttlendendrescue*NETWORK_ERRORS# token attempt failed, reset token# fallback to non-token mode@token=nilendtoken=@token.valueiftoken_set?beginmetadata=http_get(conn,METADATA_PATH_BASE,token)profile_name=metadata.lines.first.striphttp_get(conn,METADATA_PATH_BASE+profile_name,token)rescueTokenExpiredError# Token has expired, reset it# The next retry should fetch it@token=nilraiseNon200Responseendendendrescue'{}'endendenddeftoken_set?@token&&!@token.expired?enddef_metadata_disabled?ENV.fetch('AWS_EC2_METADATA_DISABLED','false').downcase=='true'enddefopen_connectionuri=URI.parse(@endpoint)http=Net::HTTP.new(uri.hostname||@endpoint,@port||uri.port)http.open_timeout=@http_open_timeouthttp.read_timeout=@http_read_timeouthttp.set_debug_output(@http_debug_output)if@http_debug_outputhttp.startyield(http).tap{http.finish}end# GET request fetch profile and credentialsdefhttp_get(connection,path,token=nil)headers={'User-Agent'=>"aws-sdk-ruby3/#{CORE_GEM_VERSION}"}headers['x-aws-ec2-metadata-token']=tokeniftokenresponse=connection.request(Net::HTTP::Get.new(path,headers))caseresponse.code.to_iwhen200response.bodywhen401raiseTokenExpiredErrorelseraiseNon200Responseendend# PUT request fetch token with ttldefhttp_put(connection,path,ttl)headers={'User-Agent'=>"aws-sdk-ruby3/#{CORE_GEM_VERSION}",'x-aws-ec2-metadata-token-ttl-seconds'=>ttl.to_s}response=connection.request(Net::HTTP::Put.new(path,headers))caseresponse.code.to_iwhen200[response.body,response.header['x-aws-ec2-metadata-token-ttl-seconds'].to_i]when400raiseTokenRetrivalErrorwhen401raiseTokenExpiredErrorelseraiseNon200Responseendenddefretry_errors(error_classes,options={},&_block)max_retries=options[:max_retries]retries=0beginyieldrescue*error_classesraiseunlessretries<max_retries@backoff.call(retries)retries+=1retryendenddefwarn_expired_credentialswarn("Attempting credential expiration extension due to a credential "\"service availability issue. A refresh of these credentials "\"will be attempted again in 5 minutes.")enddefempty_credentials?(creds)!creds||!creds.access_key_id||creds.access_key_id.empty?end# Compute an offset for refresh with jitterdefrefresh_offset300+rand(0..60)end# @api private# Token used to fetch IMDS profile and credentialsclassTokendefinitialize(value,ttl,created_time=Time.now)@ttl=ttl@value=value@created_time=created_timeend# [String] token valueattr_reader:valuedefexpired?Time.now-@created_time>@ttlendendendend