adyen_jpiqueras 2.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|