adyen_jpiqueras 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.travis.yml +30 -0
- data/CHANGELOG.md +128 -0
- data/CONTRIBUTING.md +85 -0
- data/Gemfile +11 -0
- data/LICENSE +21 -0
- data/README.md +31 -0
- data/Rakefile +54 -0
- data/adyen_jpiqueras.gemspec +44 -0
- data/config.ru +5 -0
- data/lib/adyen.rb +16 -0
- data/lib/adyen/api.rb +424 -0
- data/lib/adyen/api/cacert.pem +3894 -0
- data/lib/adyen/api/payment_service.rb +374 -0
- data/lib/adyen/api/recurring_service.rb +188 -0
- data/lib/adyen/api/response.rb +61 -0
- data/lib/adyen/api/simple_soap_client.rb +134 -0
- data/lib/adyen/api/templates/payment_service.rb +159 -0
- data/lib/adyen/api/templates/recurring_service.rb +71 -0
- data/lib/adyen/api/test_helpers.rb +133 -0
- data/lib/adyen/api/xml_querier.rb +137 -0
- data/lib/adyen/base.rb +17 -0
- data/lib/adyen/configuration.rb +179 -0
- data/lib/adyen/form.rb +419 -0
- data/lib/adyen/hpp.rb +27 -0
- data/lib/adyen/hpp/request.rb +192 -0
- data/lib/adyen/hpp/response.rb +52 -0
- data/lib/adyen/hpp/signature.rb +34 -0
- data/lib/adyen/matchers.rb +92 -0
- data/lib/adyen/notification_generator.rb +30 -0
- data/lib/adyen/railtie.rb +13 -0
- data/lib/adyen/rest.rb +67 -0
- data/lib/adyen/rest/authorise_payment.rb +234 -0
- data/lib/adyen/rest/authorise_recurring_payment.rb +46 -0
- data/lib/adyen/rest/client.rb +127 -0
- data/lib/adyen/rest/errors.rb +33 -0
- data/lib/adyen/rest/modify_payment.rb +89 -0
- data/lib/adyen/rest/payout.rb +89 -0
- data/lib/adyen/rest/request.rb +104 -0
- data/lib/adyen/rest/response.rb +80 -0
- data/lib/adyen/rest/signature.rb +27 -0
- data/lib/adyen/signature.rb +76 -0
- data/lib/adyen/templates/notification_migration.rb +29 -0
- data/lib/adyen/templates/notification_model.rb +69 -0
- data/lib/adyen/util.rb +147 -0
- data/lib/adyen/version.rb +5 -0
- data/spec/api/api_spec.rb +231 -0
- data/spec/api/payment_service_spec.rb +505 -0
- data/spec/api/recurring_service_spec.rb +236 -0
- data/spec/api/response_spec.rb +59 -0
- data/spec/api/simple_soap_client_spec.rb +133 -0
- data/spec/api/spec_helper.rb +463 -0
- data/spec/api/test_helpers_spec.rb +84 -0
- data/spec/functional/api_spec.rb +117 -0
- data/spec/functional/initializer.rb.ci +3 -0
- data/spec/functional/initializer.rb.sample +3 -0
- data/spec/spec_helper.rb +8 -0
- data/test/form_test.rb +303 -0
- data/test/functional/payment_authorisation_api_test.rb +107 -0
- data/test/functional/payment_modification_api_test.rb +58 -0
- data/test/functional/payout_api_test.rb +93 -0
- data/test/helpers/capybara.rb +12 -0
- data/test/helpers/configure_adyen.rb +6 -0
- data/test/helpers/example_server.rb +136 -0
- data/test/helpers/public/adyen.encrypt.js +679 -0
- data/test/helpers/public/adyen.encrypt.min.js +14 -0
- data/test/helpers/test_cards.rb +20 -0
- data/test/helpers/views/authorized.erb +7 -0
- data/test/helpers/views/hpp.erb +20 -0
- data/test/helpers/views/index.erb +6 -0
- data/test/helpers/views/pay.erb +36 -0
- data/test/helpers/views/redirect_shopper.erb +18 -0
- data/test/hpp/signature_test.rb +37 -0
- data/test/hpp_test.rb +250 -0
- data/test/integration/hpp_integration_test.rb +52 -0
- data/test/integration/payment_using_3d_secure_integration_test.rb +41 -0
- data/test/integration/payment_with_client_side_encryption_integration_test.rb +26 -0
- data/test/rest/signature_test.rb +36 -0
- data/test/rest_list_recurring_details_response_test.rb +22 -0
- data/test/rest_request_test.rb +43 -0
- data/test/rest_response_test.rb +19 -0
- data/test/signature_test.rb +76 -0
- data/test/test_helper.rb +45 -0
- data/test/util_test.rb +78 -0
- data/yard_extensions.rb +16 -0
- metadata +308 -0
@@ -0,0 +1,33 @@
|
|
1
|
+
module Adyen
|
2
|
+
module REST
|
3
|
+
|
4
|
+
# The main exception class for error reporting when using the REST API Client.
|
5
|
+
class Error < Adyen::Error
|
6
|
+
end
|
7
|
+
|
8
|
+
# Exception class for errors on requests
|
9
|
+
class RequestValidationFailed < Adyen::REST::Error
|
10
|
+
end
|
11
|
+
|
12
|
+
# Exception class for error responses from the Adyen API.
|
13
|
+
#
|
14
|
+
# @!attribute category
|
15
|
+
# @return [String, nil]
|
16
|
+
# @!attribute code
|
17
|
+
# @return [Integer, nil]
|
18
|
+
# @!attribute description
|
19
|
+
# @return [String, nil]
|
20
|
+
class ResponseError < Adyen::REST::Error
|
21
|
+
attr_accessor :category, :code, :description
|
22
|
+
|
23
|
+
def initialize(response_body)
|
24
|
+
if match = /\A(\w+)\s(\d+)\s(.*)\z/.match(response_body)
|
25
|
+
@category, @code, @description = match[1], match[2].to_i, match[3]
|
26
|
+
super("API request error: #{description} (code: #{code}/#{category})")
|
27
|
+
else
|
28
|
+
super("API request error: #{response_body}")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module Adyen
|
2
|
+
module REST
|
3
|
+
|
4
|
+
# This module implements the <b>Payment.capture</b> API to capture
|
5
|
+
# previously authorised payments.
|
6
|
+
module ModifyPayment
|
7
|
+
class Request < Adyen::REST::Request
|
8
|
+
def set_modification_amount(currency, value)
|
9
|
+
self['modification_amount'] = { currency: currency, value: value }
|
10
|
+
end
|
11
|
+
|
12
|
+
alias_method :set_amount, :set_modification_amount
|
13
|
+
end
|
14
|
+
|
15
|
+
class Response < Adyen::REST::Response
|
16
|
+
attr_reader :expected_response
|
17
|
+
|
18
|
+
def initialize(http_response, options = {})
|
19
|
+
super
|
20
|
+
@expected_response = options[:expects]
|
21
|
+
end
|
22
|
+
|
23
|
+
def received?
|
24
|
+
self[:response] == expected_response
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Constructs and issues a Payment.capture API call.
|
29
|
+
def capture_payment(attributes = {})
|
30
|
+
request = capture_payment_request(attributes)
|
31
|
+
execute_request(request)
|
32
|
+
end
|
33
|
+
|
34
|
+
def capture_payment_request(attributes = {})
|
35
|
+
Adyen::REST::ModifyPayment::Request.new('Payment.capture', attributes,
|
36
|
+
response_class: Adyen::REST::ModifyPayment::Response,
|
37
|
+
response_options: {
|
38
|
+
expects: '[capture-received]'
|
39
|
+
}
|
40
|
+
)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Constructs and issues a Payment.cancel API call.
|
44
|
+
def cancel_payment(attributes = {})
|
45
|
+
request = cancel_payment_request(attributes)
|
46
|
+
execute_request(request)
|
47
|
+
end
|
48
|
+
|
49
|
+
def cancel_payment_request(attributes = {})
|
50
|
+
Adyen::REST::ModifyPayment::Request.new('Payment.cancel', attributes,
|
51
|
+
response_class: Adyen::REST::ModifyPayment::Response,
|
52
|
+
response_options: {
|
53
|
+
expects: '[cancel-received]'
|
54
|
+
}
|
55
|
+
)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Constructs and issues a Payment.cancel API call.
|
59
|
+
def refund_payment(attributes = {})
|
60
|
+
request = refund_payment_request(attributes)
|
61
|
+
execute_request(request)
|
62
|
+
end
|
63
|
+
|
64
|
+
def refund_payment_request(attributes = {})
|
65
|
+
Adyen::REST::ModifyPayment::Request.new('Payment.refund', attributes,
|
66
|
+
response_class: Adyen::REST::ModifyPayment::Response,
|
67
|
+
response_options: {
|
68
|
+
expects: '[refund-received]'
|
69
|
+
}
|
70
|
+
)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Constructs and issues a Payment.cancel API call.
|
74
|
+
def cancel_or_refund_payment(attributes = {})
|
75
|
+
request = cancel_or_refund_payment_request(attributes)
|
76
|
+
execute_request(request)
|
77
|
+
end
|
78
|
+
|
79
|
+
def cancel_or_refund_payment_request(attributes = {})
|
80
|
+
Adyen::REST::ModifyPayment::Request.new('Payment.cancelOrRefund', attributes,
|
81
|
+
response_class: Adyen::REST::ModifyPayment::Response,
|
82
|
+
response_options: {
|
83
|
+
expects: '[cancelOrRefund-received]'
|
84
|
+
}
|
85
|
+
)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module Adyen
|
2
|
+
module REST
|
3
|
+
# This module implements the <b>Payout</b>
|
4
|
+
# API calls, and includes a custom response class to make handling the response easier.
|
5
|
+
# https://docs.adyen.com/developers/payout-manual
|
6
|
+
module Payout
|
7
|
+
class Request < Adyen::REST::Request
|
8
|
+
end
|
9
|
+
|
10
|
+
class Response < Adyen::REST::Response
|
11
|
+
|
12
|
+
def success?
|
13
|
+
result_code == SUCCESS
|
14
|
+
end
|
15
|
+
|
16
|
+
def received?
|
17
|
+
result_code == RECEIVED
|
18
|
+
end
|
19
|
+
|
20
|
+
def confirmed?
|
21
|
+
response == CONFIRMED
|
22
|
+
end
|
23
|
+
|
24
|
+
def declined?
|
25
|
+
response == DECLINED
|
26
|
+
end
|
27
|
+
|
28
|
+
def result_code
|
29
|
+
self[:result_code]
|
30
|
+
end
|
31
|
+
|
32
|
+
def psp_reference
|
33
|
+
self[:psp_reference]
|
34
|
+
end
|
35
|
+
|
36
|
+
def response
|
37
|
+
self[:response]
|
38
|
+
end
|
39
|
+
|
40
|
+
SUCCESS = 'Success'.freeze
|
41
|
+
RECEIVED = '[payout-submit-received]'.freeze
|
42
|
+
CONFIRMED = '[payout-confirm-received]'.freeze
|
43
|
+
DECLINED = '[payout-decline-received]'.freeze
|
44
|
+
private_constant :SUCCESS, :RECEIVED, :CONFIRMED, :DECLINED
|
45
|
+
end
|
46
|
+
|
47
|
+
# Constructs and issues a Payment.capture API call.
|
48
|
+
def store_payout(attributes = {})
|
49
|
+
request = store_request('Payout.storeDetail', attributes)
|
50
|
+
execute_request(request)
|
51
|
+
end
|
52
|
+
|
53
|
+
def submit_payout(attributes = {})
|
54
|
+
request = store_request('Payout.submit', attributes)
|
55
|
+
execute_request(request)
|
56
|
+
end
|
57
|
+
|
58
|
+
def submit_and_store_payout(attributes = {})
|
59
|
+
request = store_request('Payout.storeDetailAndSubmit', attributes)
|
60
|
+
execute_request(request)
|
61
|
+
end
|
62
|
+
|
63
|
+
def confirm_payout(attributes = {})
|
64
|
+
request = review_request('Payout.confirm', attributes)
|
65
|
+
execute_request(request)
|
66
|
+
end
|
67
|
+
|
68
|
+
def decline_payout(attributes = {})
|
69
|
+
request = review_request('Payout.decline', attributes)
|
70
|
+
execute_request(request)
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
# Require you to use a client initialize with payout_store
|
75
|
+
def store_request(action, attributes)
|
76
|
+
Adyen::REST::Payout::Request.new(action, attributes,
|
77
|
+
response_class: Adyen::REST::Payout::Response
|
78
|
+
)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Require you to use a client initialize with payout review
|
82
|
+
def review_request(action, attributes)
|
83
|
+
Adyen::REST::Payout::Request.new(action, attributes,
|
84
|
+
response_class: Adyen::REST::Payout::Response
|
85
|
+
)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'adyen/util'
|
2
|
+
require 'adyen/rest/errors'
|
3
|
+
require 'adyen/rest/response'
|
4
|
+
|
5
|
+
module Adyen
|
6
|
+
module REST
|
7
|
+
|
8
|
+
# The request object models an API request to be sent to Adyen's webservice.
|
9
|
+
#
|
10
|
+
# Some API calls may use a subclass to model their request.
|
11
|
+
#
|
12
|
+
# @!attribute prefix [r]
|
13
|
+
# The prefix to use for every request attribute (except action)
|
14
|
+
# @return [String]
|
15
|
+
# @!attribute form_data [r]
|
16
|
+
# The attributes to include in the API request as form data.
|
17
|
+
# @return [Hash<String, String>] A dictionary of key value pairs
|
18
|
+
# @!required_attributes [r]
|
19
|
+
# The list of required attributes that should show up in the request.
|
20
|
+
# {#validate!} will fail if any of these attributes is missing or empty.
|
21
|
+
# @return [Array<String>]
|
22
|
+
# @!attribute response_class [rw]
|
23
|
+
# The response class to use to wrap the HTTP response to this request.
|
24
|
+
# @return [Class]
|
25
|
+
# @!attribute response_options [rw]
|
26
|
+
# The options to send to the response class initializer.
|
27
|
+
# @return [Hash]
|
28
|
+
#
|
29
|
+
# @see Adyen::REST::Client
|
30
|
+
# @see Adyen::REST::Response
|
31
|
+
class Request
|
32
|
+
attr_reader :prefix, :form_data, :required_attributes, :path
|
33
|
+
attr_accessor :response_class, :response_options
|
34
|
+
|
35
|
+
def initialize(action, attributes, options = {})
|
36
|
+
@form_data = generate_form_data(attributes)
|
37
|
+
@path = generate_path(action)
|
38
|
+
|
39
|
+
@response_class = options[:response_class] || Adyen::REST::Response
|
40
|
+
@response_options = options[:response_options] || {}
|
41
|
+
|
42
|
+
@required_attributes = []
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns the request's action
|
46
|
+
# @return [String]
|
47
|
+
def action
|
48
|
+
form_data['action']
|
49
|
+
end
|
50
|
+
|
51
|
+
# Retrieves an attribute from the request
|
52
|
+
def [](attribute)
|
53
|
+
form_data[canonical_name(attribute)]
|
54
|
+
end
|
55
|
+
|
56
|
+
# Sets an attribute on the request
|
57
|
+
def []=(attribute, value)
|
58
|
+
form_data.merge!(Adyen::Util.flatten(attribute => value))
|
59
|
+
value
|
60
|
+
end
|
61
|
+
|
62
|
+
def merchant_account=(value)
|
63
|
+
self[:merchantAccount] = value
|
64
|
+
end
|
65
|
+
|
66
|
+
# Runs validations on the request before it is sent.
|
67
|
+
# @return [void]
|
68
|
+
# @raises [Adyen::REST::RequestValidationFailed]
|
69
|
+
def validate!
|
70
|
+
required_attributes.each do |attribute|
|
71
|
+
if form_data[attribute].nil? || form_data[attribute].empty?
|
72
|
+
raise Adyen::REST::RequestValidationFailed, "#{attribute} is empty, but required!"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Builds a Adyen::REST::Response instnace for a given Net::HTTP response.
|
78
|
+
# @param http_response [Net::HTTPResponse] The HTTP response return for this request.
|
79
|
+
# @return [Adyen::REST::Response] An instance of {Adyen::REST::Response}, or a subclass.
|
80
|
+
def build_response(http_response)
|
81
|
+
response_class.new(http_response, response_options)
|
82
|
+
end
|
83
|
+
|
84
|
+
protected
|
85
|
+
|
86
|
+
def canonical_name(name)
|
87
|
+
Adyen::Util.camelize(name)
|
88
|
+
end
|
89
|
+
|
90
|
+
# @return [Hash<String, String>] A dictionary of API request attributes that
|
91
|
+
def generate_form_data(attributes)
|
92
|
+
Adyen::Util.flatten(attributes)
|
93
|
+
end
|
94
|
+
|
95
|
+
def generate_path(action)
|
96
|
+
PATH % action.split('.')
|
97
|
+
end
|
98
|
+
|
99
|
+
# @see Adyen::REST::Request#set_path
|
100
|
+
PATH = '/pal/servlet/%s/v12/%s'
|
101
|
+
private_constant :PATH
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'adyen/util'
|
2
|
+
|
3
|
+
module Adyen
|
4
|
+
module REST
|
5
|
+
|
6
|
+
# The Response class models the HTTP response that is the result of a
|
7
|
+
# API call to Adyen's REST webservice.
|
8
|
+
#
|
9
|
+
# Some API calls may respond with an instance of a subclass, to make
|
10
|
+
# dealing with the response easier.
|
11
|
+
#
|
12
|
+
# @!attribute http_response [r]
|
13
|
+
# The underlying net/http response.
|
14
|
+
# @return [Net::HTTPResponse]
|
15
|
+
# @!attribute prefix [r]
|
16
|
+
# The prefix to use when reading attributes from the response
|
17
|
+
# @return [String]
|
18
|
+
#
|
19
|
+
# @see Adyen::REST::Client
|
20
|
+
# @see Adyen::REST::Request
|
21
|
+
class Response
|
22
|
+
attr_reader :http_response, :prefix, :attributes
|
23
|
+
|
24
|
+
def initialize(http_response, options = {})
|
25
|
+
@http_response = http_response
|
26
|
+
@prefix = options.key?(:prefix) ? options[:prefix].to_s : nil
|
27
|
+
@attributes = parse_response_attributes
|
28
|
+
end
|
29
|
+
|
30
|
+
# Looks up an attribute in the response.
|
31
|
+
# @return [String, nil] The value of the attribute if it was included in the response.
|
32
|
+
def [](name)
|
33
|
+
attributes[canonical_name(name)]
|
34
|
+
end
|
35
|
+
|
36
|
+
def has_attribute?(name)
|
37
|
+
attributes.has_key?(canonical_name(name))
|
38
|
+
end
|
39
|
+
|
40
|
+
def psp_reference
|
41
|
+
Integer(self[:psp_reference])
|
42
|
+
end
|
43
|
+
|
44
|
+
protected
|
45
|
+
|
46
|
+
def map_response_list(response_prefix, mapped_attributes)
|
47
|
+
list = []
|
48
|
+
index = 0
|
49
|
+
|
50
|
+
loop do
|
51
|
+
response = {}
|
52
|
+
mapped_attributes.each do |key, value|
|
53
|
+
new_value = attributes["#{response_prefix}.#{index.to_s}.#{value}"]
|
54
|
+
response[key] = new_value unless new_value.empty?
|
55
|
+
end
|
56
|
+
|
57
|
+
index += 1
|
58
|
+
break unless response.any?
|
59
|
+
list << response
|
60
|
+
end
|
61
|
+
|
62
|
+
list
|
63
|
+
end
|
64
|
+
|
65
|
+
def canonical_name(name)
|
66
|
+
Adyen::Util.camelize(apply_prefix(name))
|
67
|
+
end
|
68
|
+
|
69
|
+
def apply_prefix(name)
|
70
|
+
prefix ? name.to_s.sub(/\A(?!#{Regexp.quote(prefix)}\.)/, "#{prefix}.") : name.to_s
|
71
|
+
end
|
72
|
+
|
73
|
+
def parse_response_attributes
|
74
|
+
attributes = CGI.parse(http_response.body)
|
75
|
+
attributes.each { |key, values| attributes[key] = values.first }
|
76
|
+
attributes
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'adyen/signature'
|
2
|
+
|
3
|
+
module Adyen
|
4
|
+
module REST
|
5
|
+
# The Signature module can sign and verify HMAC SHA-256 signatures for API
|
6
|
+
module Signature
|
7
|
+
extend self
|
8
|
+
|
9
|
+
# Sign the parameters with the given shared secret
|
10
|
+
# @param [Hash] params The set of parameters to sign. Should sent `sharedSecret` to sign.
|
11
|
+
# @return [String] signature from parameters
|
12
|
+
def sign(params)
|
13
|
+
Adyen::Signature.sign(params, :rest)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Verify the parameters with the given shared secret
|
17
|
+
# @param [Hash] params The set of parameters to verify.
|
18
|
+
# Should include `sharedSecret` param to sign and the `hmacSignature` param to compare with the signature calculated
|
19
|
+
# @return [Boolean] true if the `hmacSignature` in the params matches our calculated signature
|
20
|
+
def verify(params)
|
21
|
+
their_sig = params.delete('hmacSignature')
|
22
|
+
raise ArgumentError, "params must include 'hmacSignature' for verification" if their_sig.empty?
|
23
|
+
Adyen::Signature.verify(params, their_sig, :rest)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'base64'
|
3
|
+
|
4
|
+
module Adyen
|
5
|
+
# The Signature module generic to sign and verify HMAC SHA-256 signatures
|
6
|
+
module Signature
|
7
|
+
extend self
|
8
|
+
|
9
|
+
# Sign the parameters with the given shared secret
|
10
|
+
# @param [Hash] params The set of parameters to verify. Must include a `shared_secret` param for signing/verification
|
11
|
+
#
|
12
|
+
# @param [String] type The type to sign (:hpp or :rest). Default is :hpp
|
13
|
+
# @return [String] The signature
|
14
|
+
def sign(params, type = :hpp)
|
15
|
+
shared_secret = params.delete('sharedSecret')
|
16
|
+
raise ArgumentError, "Cannot sign parameters without a shared secret" unless shared_secret
|
17
|
+
sig = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), Array(shared_secret).pack("H*"), string_to_sign(params, type))
|
18
|
+
Base64.encode64(sig).strip
|
19
|
+
end
|
20
|
+
|
21
|
+
# Compare a signature calculated with anoter HMAC Signature
|
22
|
+
# @param [Hash] params The set of parameters to verify. Must include a `shared_secret`
|
23
|
+
# param for signing/verification
|
24
|
+
# @param [String] hmacSignature will be compared to the signature calculated.
|
25
|
+
# @return [Boolean] true if the `hmacSignature` matches our calculated signature
|
26
|
+
def verify(params, hmacSignature, type = :hpp)
|
27
|
+
raise ArgumentError,"hmacSignature cannot be empty for verification" if hmacSignature.empty?
|
28
|
+
our_sig = sign(params, type)
|
29
|
+
secure_compare(hmacSignature, our_sig)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def string_to_sign(params, type)
|
35
|
+
string = ''
|
36
|
+
if type == :hpp
|
37
|
+
string = sorted_keys(params) + sorted_values(params)
|
38
|
+
elsif type == :rest
|
39
|
+
keys = %w(pspReference originalReference merchantAccountCode merchantReference value currency eventCode success)
|
40
|
+
string = sorted_values(params, keys)
|
41
|
+
else
|
42
|
+
raise NotImplementedError, 'Type sign not implemented'
|
43
|
+
end
|
44
|
+
|
45
|
+
string.map{ |el| escape_value(el) }.join(':')
|
46
|
+
end
|
47
|
+
|
48
|
+
def sorted_keys(hash, keys_to_sort = nil)
|
49
|
+
hash.sort.map{ |el| el[0] }
|
50
|
+
end
|
51
|
+
|
52
|
+
def sorted_values(hash, keys_to_sort = nil)
|
53
|
+
if keys_to_sort.is_a? Array
|
54
|
+
keys_to_sort.map { |key| hash[key] }
|
55
|
+
else
|
56
|
+
hash.sort.map{ |el| el[1] }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def escape_value(value)
|
61
|
+
value.gsub(':', '\\:').gsub('\\', '\\\\')
|
62
|
+
end
|
63
|
+
|
64
|
+
# Constant-time compare for two fixed-length strings
|
65
|
+
# Stolen from https://github.com/rails/rails/commit/c8c660002f4b0e9606de96325f20b95248b6ff2d
|
66
|
+
def secure_compare(a, b)
|
67
|
+
return false unless a.bytesize == b.bytesize
|
68
|
+
|
69
|
+
l = a.unpack "C#{a.bytesize}"
|
70
|
+
|
71
|
+
res = 0
|
72
|
+
b.each_byte { |byte| res |= byte ^ l.shift }
|
73
|
+
res == 0
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|