# frozen_string_literal: truemoduleAwsmodulePlugins# @api privateclassChecksumAlgorithm<Seahorse::Client::PluginCHUNK_SIZE=1*1024*1024# one MB# determine the set of supported client side checksum algorithms# CRC32c requires aws-crt (optional sdk dependency) for supportCLIENT_ALGORITHMS=beginsupported=%w[SHA256 SHA1 CRC32]beginrequire'aws-crt'supported<<'CRC32C'rescueLoadErrorendsupportedend.freeze# priority order of checksum algorithms to validate responses against# Remove any algorithms not supported by client (ie, depending on CRT availability)CHECKSUM_ALGORITHM_PRIORITIES=%w[CRC32C SHA1 CRC32 SHA256]&CLIENT_ALGORITHMS# byte size of checksums, used in computing the trailer lengthCHECKSUM_SIZE={'CRC32'=>16,'CRC32C'=>16,'SHA1'=>36,'SHA256'=>52}# Interface for computing digests on request/response bodies# which may be files, strings or IO like objects# Applies only to digest functions that produce 32 bit integer checksums# (eg CRC32)classDigest32attr_reader:value# @param [Object] digest_fndefinitialize(digest_fn)@digest_fn=digest_fn@value=0enddefupdate(chunk)@value=@digest_fn.call(chunk,@value)enddefbase64digestBase64.encode64([@value].pack('N')).chompendenddefadd_handlers(handlers,_config)handlers.add(OptionHandler,step: :initialize)# priority set low to ensure checksum is computed AFTER the request is# built but before it is signedhandlers.add(ChecksumHandler,priority: 15,step: :build)endprivatedefself.request_algorithm_selection(context)returnunlesscontext.operation.http_checksuminput_member=context.operation.http_checksum['requestAlgorithmMember']context.params[input_member.to_sym]&.upcaseifinput_memberenddefself.request_validation_mode(context)returnunlesscontext.operation.http_checksuminput_member=context.operation.http_checksum['requestValidationModeMember']context.params[input_member.to_sym]ifinput_memberenddefself.operation_response_algorithms(context)returnunlesscontext.operation.http_checksumcontext.operation.http_checksum['responseAlgorithms']end# @api privateclassOptionHandler<Seahorse::Client::Handlerdefcall(context)context[:http_checksum]||={}# validate request configurationif(request_input=ChecksumAlgorithm.request_algorithm_selection(context))unlessCLIENT_ALGORITHMS.include?request_inputif(request_input=='CRC32C')raiseArgumentError,"CRC32C requires crt support - install the aws-crt gem for support."elseraiseArgumentError,"#{request_input} is not a supported checksum algorithm."endendend# validate response configurationif(ChecksumAlgorithm.request_validation_mode(context))# Compute an ordered list as the union between priority supported and the# operation's modeled response algorithms.validation_list=CHECKSUM_ALGORITHM_PRIORITIES&ChecksumAlgorithm.operation_response_algorithms(context)context[:http_checksum][:validation_list]=validation_listend@handler.call(context)endend# @api privateclassChecksumHandler<Seahorse::Client::Handlerdefcall(context)ifshould_calculate_request_checksum?(context)request_algorithm_input=ChecksumAlgorithm.request_algorithm_selection(context)||context[:default_request_checksum_algorithm]context[:checksum_algorithms]=request_algorithm_inputrequest_checksum_property={'algorithm'=>request_algorithm_input,'in'=>checksum_request_in(context),'name'=>"x-amz-checksum-#{request_algorithm_input.downcase}"}calculate_request_checksum(context,request_checksum_property)endifshould_verify_response_checksum?(context)add_verify_response_checksum_handlers(context)end@handler.call(context)endprivatedefshould_calculate_request_checksum?(context)context.operation.http_checksum&&(ChecksumAlgorithm.request_algorithm_selection(context)||context[:default_request_checksum_algorithm])enddefshould_verify_response_checksum?(context)context[:http_checksum][:validation_list]&&!context[:http_checksum][:validation_list].empty?enddefcalculate_request_checksum(context,checksum_properties)casechecksum_properties['in']when'header'header_name=checksum_properties['name']body=context.http_request.body_contentsifbodycontext.http_request.headers[header_name]||=ChecksumAlgorithm.calculate_checksum(checksum_properties['algorithm'],body)endwhen'trailer'apply_request_trailer_checksum(context,checksum_properties)endenddefapply_request_trailer_checksum(context,checksum_properties)location_name=checksum_properties['name']# set required headersheaders=context.http_request.headersheaders['Content-Encoding']='aws-chunked'headers['X-Amz-Content-Sha256']='STREAMING-UNSIGNED-PAYLOAD-TRAILER'headers['X-Amz-Trailer']=location_name# We currently always compute the size in the modified body wrapper - allowing us# to set the Content-Length header (set by content_length plugin).# This means we cannot use Transfer-Encoding=chunkedif!context.http_request.body.respond_to?(:size)raiseAws::Errors::ChecksumError,'Could not determine length of the body'endheaders['X-Amz-Decoded-Content-Length']=context.http_request.body.sizecontext.http_request.body=AwsChunkedTrailerDigestIO.new(context.http_request.body,checksum_properties['algorithm'],location_name)end# Add events to the http_response to verify the checksum as its read# This prevents the body from being read multiple times# verification is done only once a successful response has completeddefadd_verify_response_checksum_handlers(context)http_response=context.http_responsechecksum_context={}http_response.on_headersdo|_status,headers|header_name,algorithm=response_header_to_verify(headers,context[:http_checksum][:validation_list])ifheader_nameexpected=headers[header_name]unlesscontext[:http_checksum][:skip_on_suffix]&&/-[\d]+$/.match(expected)checksum_context[:algorithm]=algorithmchecksum_context[:header_name]=header_namechecksum_context[:digest]=ChecksumAlgorithm.digest_for_algorithm(algorithm)checksum_context[:expected]=expectedendendendhttp_response.on_datado|chunk|checksum_context[:digest].update(chunk)ifchecksum_context[:digest]endhttp_response.on_successdoifchecksum_context[:digest]&&(computed=checksum_context[:digest].base64digest)ifcomputed!=checksum_context[:expected]raiseAws::Errors::ChecksumError,"Checksum validation failed on #{checksum_context[:header_name]} "\"computed: #{computed}, expected: #{checksum_context[:expected]}"endcontext[:http_checksum][:validated]=checksum_context[:algorithm]endendend# returns nil if no headers to verifydefresponse_header_to_verify(headers,validation_list)validation_list.eachdo|algorithm|header_name="x-amz-checksum-#{algorithm}"return[header_name,algorithm]ifheaders[header_name]endnilend# determine where (header vs trailer) a request checksum should be addeddefchecksum_request_in(context)ifcontext.operation['unsignedPayload']||context.operation['authtype']=='v4-unsigned-body''trailer'else'header'endendenddefself.calculate_checksum(algorithm,body)digest=ChecksumAlgorithm.digest_for_algorithm(algorithm)ifbody.respond_to?(:read)ChecksumAlgorithm.update_in_chunks(digest,body)elsedigest.update(body)enddigest.base64digestenddefself.digest_for_algorithm(algorithm)casealgorithmwhen'CRC32'Digest32.new(Zlib.method(:crc32))when'CRC32C'# this will only be used if input algorithm is CRC32C AND client supports it (crt available)Digest32.new(Aws::Crt::Checksums.method(:crc32c))when'SHA1'Digest::SHA1.newwhen'SHA256'Digest::SHA256.newendend# The trailer size (in bytes) is the overhead + the trailer name +# the length of the base64 encoded checksumdefself.trailer_length(algorithm,location_name)CHECKSUM_SIZE[algorithm]+location_name.sizeenddefself.update_in_chunks(digest,io)loopdochunk=io.read(CHUNK_SIZE)breakunlesschunkdigest.update(chunk)endio.rewindend# Wrapper for request body that implements application-layer# chunking with Digest computed on chunks + added as a trailerclassAwsChunkedTrailerDigestIOCHUNK_SIZE=16384definitialize(io,algorithm,location_name)@io=io@location_name=location_name@algorithm=algorithm@digest=ChecksumAlgorithm.digest_for_algorithm(algorithm)@trailer_io=nilend# the size of the application layer aws-chunked + trailer bodydefsize# compute the number of chunks# a full chunk has 4 + 4 bytes overhead, a partial chunk is len.to_s(16).size + 4orig_body_size=@io.sizen_full_chunks=orig_body_size/CHUNK_SIZEpartial_bytes=orig_body_size%CHUNK_SIZEchunked_body_size=n_full_chunks*(CHUNK_SIZE+8)chunked_body_size+=partial_bytes.to_s(16).size+partial_bytes+4unlesspartial_bytes.zero?trailer_size=ChecksumAlgorithm.trailer_length(@algorithm,@location_name)chunked_body_size+trailer_sizeenddefrewind@io.rewindenddefread(length,buf=nil)# account for possible leftover bytes at the end, if we have trailer bytes, send themif@trailer_ioreturn@trailer_io.read(length,buf)endchunk=@io.read(length)ifchunk@digest.update(chunk)application_chunked="#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n"returnStringIO.new(application_chunked).read(application_chunked.size,buf)elsetrailers={}trailers[@location_name]=@digest.base64digesttrailers=trailers.map{|k,v|"#{k}:#{v}"}.join("\r\n")@trailer_io=StringIO.new("0\r\n#{trailers}\r\n\r\n")chunk=@trailer_io.read(length,buf)endchunkendendendendend