# 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)## ## Retries# When initialized from the default credential chain, this provider defaults to `0` retries.# Breakdown of retries is as follows:## * **Configurable retries** (defaults to `1`): these retries handle errors when communicating# with the IMDS endpoint. There are two separate retry mechanisms within the provider:# * Entire token fetch and credential retrieval process# * Token fetching# * **JSON parsing retries**: Fixed at 3 attempts to handle cases when IMDS returns malformed JSON# responses. These retries are separate from configurable retries.## @see https://docs.aws.amazon.com/sdkref/latest/guide/feature-imds-credentials.html IMDS Credential ProviderclassInstanceProfileCredentialsincludeCredentialProviderincludeRefreshingCredentials# @api privateclassNon200Response<RuntimeErrorattr_reader:status_code,:bodydefinitialize(status_code,body=nil)@status_code=status_code@body=bodymsg="HTTP #{status_code}"msg+=": #{body}"ifbody&&!body.empty?super(msg)endend# @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 [Numeric, Proc] :backoff By default, failures are retried with exponential back-off, i.e.# `lambda { |num_failures| 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 [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 [Boolean] :disable_imds_v1 (false) Disable the use of the legacy EC2 Metadata Service v1.# @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 [IO] :http_debug_output (nil) HTTP wire traces are sent to this object.# You can specify something like `$stdout`.# @option options [Integer] :token_ttl (21600) Time-to-Live in seconds for EC2 Metadata Token used for fetching# Metadata Profile Credentials.# @option options [Proc] :before_refresh A 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={})@backoff=resolve_backoff(options[:backoff])@disable_imds_v1=resolve_disable_v1(options)@endpoint=resolve_endpoint(options)@http_open_timeout=options[:http_open_timeout]||1@http_read_timeout=options[:http_read_timeout]||1@http_debug_output=options[:http_debug_output]@port=options[:port]||80@retries=options[:retries]||1@token_ttl=options[:token_ttl]||21_600@async_refresh=false@imds_v1_fallback=false@no_refresh_until=nil@token=nil@metrics=['CREDENTIALS_IMDS']superend# @return [Boolean]attr_reader:disable_imds_v1# @return [Integer]attr_reader:token_ttl# @return [Integer]attr_reader:retries# @return [Proc]attr_reader:backoff# @return [String]attr_reader:endpoint# @return [Integer]attr_reader:port# @return [Integer]attr_reader:http_open_timeout# @return [Integer]attr_reader:http_read_timeout# @return [IO, nil]attr_reader:http_debug_outputprivatedefresolve_endpoint_mode(options)options[:endpoint_mode]||ENV['AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE']||Aws.shared_config.ec2_metadata_service_endpoint_mode(profile: options[:profile])||'IPv4'enddefresolve_endpoint(options)if(value=options[:ip_address])warn('The `:ip_address` option is deprecated. Use `:endpoint` instead.')returnvalueendvalue=options[:endpoint]||ENV['AWS_EC2_METADATA_SERVICE_ENDPOINT']||Aws.shared_config.ec2_metadata_service_endpoint(profile: options[:profile])||nilreturnvalueifvalueendpoint_mode=resolve_endpoint_mode(options)caseendpoint_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}"endenddefresolve_disable_v1(options)value=options[:disable_imds_v1]||ENV['AWS_EC2_METADATA_V1_DISABLED']||Aws.shared_config.ec2_metadata_v1_disabled(profile: options[:profile])||'false'Aws::Util.str_2_bool(value.to_s.downcase)enddefresolve_backoff(backoff)casebackoffwhenProcthenbackoffwhenNumericthen->(_){sleep(backoff)}else->(num_failures){Kernel.sleep(1.2**num_failures)}endenddefrefreshif@no_refresh_until&&@no_refresh_until>Time.nowwarn_expired_credentialsreturnendnew_creds=begin# 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.retry_errors([Aws::Json::ParseError],max_retries: 3)doAws::Json.load(retrieve_credentials.to_s)endrescueAws::Json::ParseErrorraiseAws::Errors::MetadataParserErrorendif@credentials&.set?&&empty_credentials?(new_creds)# credentials are already set, but there was an error getting new credentials# so don't update the credentials and use stale ones (static stability)@no_refresh_until=Time.now+rand(300..360)warn_expired_credentialselse# credentials are empty or successfully retrieved, update themupdate_credentials(new_creds)endenddefretrieve_credentials# Retry loading credentials a configurable number of times if# the instance metadata service is not responding.beginretry_errors(NETWORK_ERRORS,max_retries: @retries)doopen_connectiondo|conn|# attempt to fetch token to start secure flow first# and rescue to failoverfetch_token(conn)unless@imds_v1_fallback||(@token&&!@token.expired?)# disable insecure flow if we couldn't get token and imds v1 is disabledraiseTokenRetrivalErrorif@token.nil?&&@disable_imds_v1fetch_credentials(conn)endendrescueStandardError=>ewarn("Error retrieving instance profile credentials: #{e}")'{}'endenddefupdate_credentials(creds)@credentials=Credentials.new(creds['AccessKeyId'],creds['SecretAccessKey'],creds['Token'])@expiration=creds['Expiration']?Time.iso8601(creds['Expiration']):nilreturnunless@expiration&&@expiration<Time.now@no_refresh_until=Time.now+rand(300..360)warn_expired_credentialsenddeffetch_token(conn)created_time=Time.nowtoken_value,ttl=http_put(conn)@token=Token.new(token_value,ttl,created_time)iftoken_value&&ttlrescue*NETWORK_ERRORS# token attempt failed, reset token# fallback to non-token mode@imds_v1_fallback=trueenddeffetch_credentials(conn)metadata=http_get(conn,METADATA_PATH_BASE)profile_name=metadata.lines.first.striphttp_get(conn,METADATA_PATH_BASE+profile_name)rescueTokenExpiredError# Token has expired, reset it# The next retry should fetch it@token=nil@imds_v1_fallback=falseraiseNon200Response.new(401,'Token expired')enddeftoken_set?@token&&!@token.expired?enddefopen_connectionuri=URI.parse(@endpoint)http=Net::HTTP.new(uri.hostname||@endpoint,uri.port||@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)headers={'User-Agent'=>"aws-sdk-ruby3/#{CORE_GEM_VERSION}"}headers['x-aws-ec2-metadata-token']=@token.valueif@tokenresponse=connection.request(Net::HTTP::Get.new(path,headers))caseresponse.code.to_iwhen200response.bodywhen401raiseTokenExpiredErrorelseraiseNon200Response.new(response.code.to_i,response.body)endend# PUT request fetch token with ttldefhttp_put(connection)headers={'User-Agent'=>"aws-sdk-ruby3/#{CORE_GEM_VERSION}",'x-aws-ec2-metadata-token-ttl-seconds'=>@token_ttl.to_s}response=connection.request(Net::HTTP::Put.new(METADATA_TOKEN_PATH,headers))caseresponse.code.to_iwhen200[response.body,response.header['x-aws-ec2-metadata-token-ttl-seconds'].to_i]when400raiseTokenRetrivalErrorelseraiseNon200Response.new(response.code.to_i,response.body)endenddefretry_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_hash)!creds_hash['AccessKeyId']||creds_hash['AccessKeyId'].empty?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