# frozen_string_literal: truemoduleHTTPXmodulePlugins## This plugin adds AWS Sigv4 authentication.## https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html## https://gitlab.com/os85/httpx/wikis/AWS-SigV4#moduleAWSSigV4Credentials=Struct.new(:username,:password,:security_token)classSignerdefinitialize(service:,region:,credentials: nil,username: nil,password: nil,security_token: nil,provider_prefix: "aws",header_provider_field: "amz",unsigned_headers: [],apply_checksum_header: true,algorithm: "SHA256")@credentials=credentials||Credentials.new(username,password,security_token)@service=service@region=region@unsigned_headers=Set.new(unsigned_headers.map(&:downcase))@unsigned_headers<<"authorization"@unsigned_headers<<"x-amzn-trace-id"@unsigned_headers<<"expect"@apply_checksum_header=apply_checksum_header@provider_prefix=provider_prefix@header_provider_field=header_provider_field@algorithm=algorithmenddefsign!(request)lower_provider_prefix="#{@provider_prefix}4"upper_provider_prefix=lower_provider_prefix.upcasedowncased_algorithm=@algorithm.downcasedatetime=(request.headers["x-#{@header_provider_field}-date"]||=Time.now.utc.strftime("%Y%m%dT%H%M%SZ"))date=datetime[0,8]content_hashed=request.headers["x-#{@header_provider_field}-content-#{downcased_algorithm}"]||hexdigest(request.body)request.headers["x-#{@header_provider_field}-content-#{downcased_algorithm}"]||=content_hashedif@apply_checksum_headerrequest.headers["x-#{@header_provider_field}-security-token"]||=@credentials.security_tokenif@credentials.security_tokensignature_headers=request.headers.each.rejectdo|k,_|@unsigned_headers.include?(k)end# aws sigv4 needs to declare the host, regardless of protocol versionsignature_headers<<["host",request.authority]unlessrequest.headers.key?("host")signature_headers.sort_by!(&:first)signed_headers=signature_headers.map(&:first).join(";")canonical_headers=signature_headers.mapdo|k,v|# eliminate whitespace between value fields, unless it's a quoted value"#{k}:#{v.start_with?("\"")&&v.end_with?("\"")?v:v.gsub(/\s+/," ").strip}\n"end.join# canonical requestcreq="#{request.verb}"\"\n#{request.canonical_path}"\"\n#{request.canonical_query}"\"\n#{canonical_headers}"\"\n#{signed_headers}"\"\n#{content_hashed}"credential_scope="#{date}"\"/#{@region}"\"/#{@service}"\"/#{lower_provider_prefix}_request"algo_line="#{upper_provider_prefix}-HMAC-#{@algorithm}"# string to signsts="#{algo_line}"\"\n#{datetime}"\"\n#{credential_scope}"\"\n#{hexdigest(creq)}"# signaturek_date=hmac("#{upper_provider_prefix}#{@credentials.password}",date)k_region=hmac(k_date,@region)k_service=hmac(k_region,@service)k_credentials=hmac(k_service,"#{lower_provider_prefix}_request")sig=hexhmac(k_credentials,sts)credential="#{@credentials.username}/#{credential_scope}"# apply signaturerequest.headers["authorization"]="#{algo_line} "\"Credential=#{credential}, "\"SignedHeaders=#{signed_headers}, "\"Signature=#{sig}"endprivatedefhexdigest(value)ifvalue.respond_to?(:to_path)# files, pathnamesOpenSSL::Digest.new(@algorithm).file(value.to_path).hexdigestelsifvalue.respond_to?(:each)digest=OpenSSL::Digest.new(@algorithm)mb_buffer=value.each.with_object("".b)do|chunk,buffer|buffer<<chunkbreakifbuffer.bytesize>=1024*1024enddigest.update(mb_buffer)value.rewinddigest.hexdigestelseOpenSSL::Digest.new(@algorithm).hexdigest(value)endenddefhmac(key,value)OpenSSL::HMAC.digest(OpenSSL::Digest.new(@algorithm),key,value)enddefhexhmac(key,value)OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new(@algorithm),key,value)endendclass<<selfdefload_dependencies(*)require"set"require"digest/sha2"require"openssl"enddefconfigure(klass)klass.plugin(:expect)klass.plugin(:compression)endendmoduleOptionsMethodsdefoption_sigv4_signer(value)value.is_a?(Signer)?value:Signer.new(value)endendmoduleInstanceMethodsdefaws_sigv4_authentication(**options)with(sigv4_signer: Signer.new(**options))enddefbuild_request(*,_)request=superreturnrequestifrequest.headers.key?("authorization")signer=request.options.sigv4_signerreturnrequestunlesssignersigner.sign!(request)requestendendmoduleRequestMethodsdefcanonical_pathpath=uri.path.duppath<<"/"ifpath.empty?path.gsub(%r{[^/]+}){|part|CGI.escape(part.encode("UTF-8")).gsub("+","%20").gsub("%7E","~")}enddefcanonical_queryparams=query.split("&")# params = params.map { |p| p.match(/=/) ? p : p + '=' }# From: https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html# Sort the parameter names by character code point in ascending order.# Parameters with duplicate names should be sorted by value.## Default sort <=> in JRuby will swap members# occasionally when <=> is 0 (considered still sorted), but this# causes our normalized query string to not match the sent querystring.# When names match, we then sort by their values. When values also# match then we sort by their original orderparams.each.with_index.sortdo|a,b|a,a_offset=ab,b_offset=ba_name,a_value=a.split("=")b_name,b_value=b.split("=")ifa_name==b_nameifa_value==b_valuea_offset<=>b_offsetelsea_value<=>b_valueendelsea_name<=>b_nameendend.map(&:first).join("&")endendendregister_plugin:aws_sigv4,AWSSigV4endend