# frozen_string_literal: truemoduleStripemoduleWebhookDEFAULT_TOLERANCE=300# Initializes an Event object from a JSON payload.## This may raise JSON::ParserError if the payload is not valid JSON, or# SignatureVerificationError if the signature verification fails.defself.construct_event(payload,sig_header,secret,tolerance: DEFAULT_TOLERANCE)Signature.verify_header(payload,sig_header,secret,tolerance: tolerance)# It's a good idea to parse the payload only after verifying it. We use# `symbolize_names` so it would otherwise be technically possible to# flood a target's memory if they were on an older version of Ruby that# doesn't GC symbols. It also decreases the likelihood that we receive a# bad payload that fails to parse and throws an exception.data=JSON.parse(payload,symbolize_names: true)Event.construct_from(data)endmoduleSignatureEXPECTED_SCHEME="v1"# Computes a webhook signature given a time (probably the current time),# a payload, and a signing secret.defself.compute_signature(timestamp,payload,secret)raiseArgumentError,"timestamp should be an instance of Time"\unlesstimestamp.is_a?(Time)raiseArgumentError,"payload should be a string"\unlesspayload.is_a?(String)raiseArgumentError,"secret should be a string"\unlesssecret.is_a?(String)timestamped_payload="#{timestamp.to_i}.#{payload}"OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"),secret,timestamped_payload)end# Generates a value that would be added to a `Stripe-Signature` for a# given webhook payload.## Note that this isn't needed to verify webhooks in any way, and is# mainly here for use in test cases (those that are both within this# project and without).defself.generate_header(timestamp,signature,scheme: EXPECTED_SCHEME)raiseArgumentError,"timestamp should be an instance of Time"\unlesstimestamp.is_a?(Time)raiseArgumentError,"signature should be a string"\unlesssignature.is_a?(String)raiseArgumentError,"scheme should be a string"\unlessscheme.is_a?(String)"t=#{timestamp.to_i},#{scheme}=#{signature}"end# Extracts the timestamp and the signature(s) with the desired scheme# from the headerdefself.get_timestamp_and_signatures(header,scheme)list_items=header.split(/,\s*/).map{|i|i.split("=",2)}timestamp=Integer(list_items.select{|i|i[0]=="t"}[0][1])signatures=list_items.select{|i|i[0]==scheme}.map{|i|i[1]}[Time.at(timestamp),signatures]endprivate_class_method:get_timestamp_and_signatures# Verifies the signature header for a given payload.## Raises a SignatureVerificationError in the following cases:# - the header does not match the expected format# - no signatures found with the expected scheme# - no signatures matching the expected signature# - a tolerance is provided and the timestamp is not within the# tolerance## Returns true otherwisedefself.verify_header(payload,header,secret,tolerance: nil)begintimestamp,signatures=get_timestamp_and_signatures(header,EXPECTED_SCHEME)# TODO: Try to knock over this blanket rescue as it can unintentionally# swallow many valid errors. Instead, try to validate an incoming# header one piece at a time, and error with a known exception class if# any part is found to be invalid. Rescue that class here.rescueStandardErrorraiseSignatureVerificationError.new("Unable to extract timestamp and signatures from header",header,http_body: payload)endifsignatures.empty?raiseSignatureVerificationError.new("No signatures found with expected scheme #{EXPECTED_SCHEME}",header,http_body: payload)endexpected_sig=compute_signature(timestamp,payload,secret)unlesssignatures.any?{|s|Util.secure_compare(expected_sig,s)}raiseSignatureVerificationError.new("No signatures found matching the expected signature for payload",header,http_body: payload)endiftolerance&×tamp<Time.now-toleranceformatted_timestamp=Time.at(timestamp).strftime("%F %T")raiseSignatureVerificationError.new("Timestamp outside the tolerance zone (#{formatted_timestamp})",header,http_body: payload)endtrueendendendend