amazon_pay 2.0.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.
@@ -0,0 +1,193 @@
1
+ module AmazonPay
2
+
3
+ # This will extend the client class to add additional
4
+ # helper methods that combine core API calls.
5
+ class Client
6
+
7
+ # This method combines multiple API calls to perform
8
+ # a complete transaction with minimum requirements.
9
+ # @param amazon_reference_id [String]
10
+ # @param authorization_reference_id [String]
11
+ # @param charge_amount [String]
12
+ # @optional charge_currency_code [String]
13
+ # @optional charge_note [String]
14
+ # @optional charge_order [String]
15
+ # @optional store_name [String]
16
+ # @optional custom_information [String]
17
+ # @optional soft_descriptor [String]
18
+ # @optional platform_id [String]
19
+ # @optional merchant_id [String]
20
+ # @optional mws_auth_token [String]
21
+ def charge(
22
+ amazon_reference_id,
23
+ authorization_reference_id,
24
+ charge_amount,
25
+ charge_currency_code: @currency_code,
26
+ charge_note: nil,
27
+ charge_order_id: nil,
28
+ store_name: nil,
29
+ custom_information: nil,
30
+ soft_descriptor: nil,
31
+ platform_id: nil,
32
+ merchant_id: @merchant_id,
33
+ mws_auth_token: nil)
34
+
35
+ if is_order_reference?(amazon_reference_id)
36
+ response = call_order_reference_api(
37
+ amazon_reference_id,
38
+ authorization_reference_id,
39
+ charge_amount,
40
+ charge_currency_code,
41
+ charge_note,
42
+ charge_order_id,
43
+ store_name,
44
+ custom_information,
45
+ soft_descriptor,
46
+ platform_id,
47
+ merchant_id,
48
+ mws_auth_token)
49
+ return response
50
+ end
51
+
52
+ if is_billing_agreement?(amazon_reference_id)
53
+ response = call_billing_agreement_api(
54
+ amazon_reference_id,
55
+ authorization_reference_id,
56
+ charge_amount,
57
+ charge_currency_code,
58
+ charge_note,
59
+ charge_order_id,
60
+ store_name,
61
+ custom_information,
62
+ soft_descriptor,
63
+ platform_id,
64
+ merchant_id,
65
+ mws_auth_token)
66
+ return response
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def call_order_reference_api(
73
+ amazon_reference_id,
74
+ authorization_reference_id,
75
+ charge_amount,
76
+ charge_currency_code,
77
+ charge_note,
78
+ charge_order_id,
79
+ store_name,
80
+ custom_information,
81
+ soft_descriptor,
82
+ platform_id,
83
+ merchant_id,
84
+ mws_auth_token)
85
+
86
+ response = set_order_reference_details(
87
+ amazon_reference_id,
88
+ charge_amount,
89
+ currency_code: charge_currency_code,
90
+ platform_id: platform_id,
91
+ seller_note: charge_note,
92
+ seller_order_id: charge_order_id,
93
+ store_name: store_name,
94
+ custom_information: custom_information,
95
+ merchant_id: merchant_id,
96
+ mws_auth_token: mws_auth_token)
97
+ if response.success
98
+ response = confirm_order_reference(
99
+ amazon_reference_id,
100
+ merchant_id: merchant_id,
101
+ mws_auth_token: mws_auth_token)
102
+ if response.success
103
+ response = authorize(
104
+ amazon_reference_id,
105
+ authorization_reference_id,
106
+ charge_amount,
107
+ currency_code: charge_currency_code,
108
+ seller_authorization_note: charge_note,
109
+ transaction_timeout: 0,
110
+ capture_now: true,
111
+ soft_descriptor: soft_descriptor,
112
+ merchant_id: merchant_id,
113
+ mws_auth_token: mws_auth_token)
114
+ return response
115
+ else
116
+ return response
117
+ end
118
+ else
119
+ return response
120
+ end
121
+ end
122
+
123
+ def call_billing_agreement_api(
124
+ amazon_reference_id,
125
+ authorization_reference_id,
126
+ charge_amount,
127
+ charge_currency_code,
128
+ charge_note,
129
+ charge_order_id,
130
+ store_name,
131
+ custom_information,
132
+ soft_descriptor,
133
+ platform_id,
134
+ merchant_id,
135
+ mws_auth_token)
136
+
137
+ response = get_billing_agreement_details(
138
+ amazon_reference_id,
139
+ merchant_id: merchant_id,
140
+ mws_auth_token: mws_auth_token)
141
+ if response.get_element('GetBillingAgreementDetailsResponse/GetBillingAgreementDetailsResult/BillingAgreementDetails/BillingAgreementStatus','State').eql?('Draft')
142
+ response = set_billing_agreement_details(
143
+ amazon_reference_id,
144
+ platform_id: platform_id,
145
+ seller_note: charge_note,
146
+ seller_billing_agreement_id: charge_order_id,
147
+ store_name: store_name,
148
+ custom_information: custom_information,
149
+ merchant_id: merchant_id,
150
+ mws_auth_token: mws_auth_token)
151
+ if response.success
152
+ response = confirm_billing_agreement(
153
+ amazon_reference_id,
154
+ merchant_id: merchant_id,
155
+ mws_auth_token: mws_auth_token)
156
+ if response.success.eql?(false)
157
+ return response
158
+ end
159
+ end
160
+ end
161
+
162
+ response = authorize_on_billing_agreement(
163
+ amazon_reference_id,
164
+ authorization_reference_id,
165
+ charge_amount,
166
+ currency_code: charge_currency_code,
167
+ seller_authorization_note: charge_note,
168
+ transaction_timeout: 0,
169
+ capture_now: true,
170
+ soft_descriptor: soft_descriptor,
171
+ seller_note: charge_note,
172
+ platform_id: platform_id,
173
+ seller_order_id: charge_order_id,
174
+ store_name: store_name,
175
+ custom_information: custom_information,
176
+ inherit_shipping_address: true,
177
+ merchant_id: merchant_id,
178
+ mws_auth_token: mws_auth_token)
179
+ return response
180
+ end
181
+
182
+
183
+ def is_order_reference?(amazon_reference_id)
184
+ amazon_reference_id.start_with?('S','P')
185
+ end
186
+
187
+ def is_billing_agreement?(amazon_reference_id)
188
+ amazon_reference_id.start_with?('C','B')
189
+ end
190
+
191
+ end
192
+
193
+ end
@@ -0,0 +1,222 @@
1
+ require 'base64'
2
+ require 'json'
3
+ require 'net/http'
4
+ require 'net/https'
5
+ require 'openssl'
6
+ require 'uri'
7
+
8
+ module AmazonPay
9
+
10
+ class IpnWasNotAuthenticError < StandardError
11
+ end
12
+
13
+ # AmazonPay Ipn Handler
14
+ #
15
+ # This class authenticates an sns message sent from Amazon. It
16
+ # will validate the header, subject, and certificate. After validation
17
+ # there are many helper methods in place to extract information received
18
+ # from the ipn notification.
19
+ class IpnHandler
20
+
21
+ SIGNABLE_KEYS = [
22
+ 'Message',
23
+ 'MessageId',
24
+ 'Timestamp',
25
+ 'TopicArn',
26
+ 'Type',
27
+ ].freeze
28
+
29
+ COMMON_NAME = 'sns.amazonaws.com'
30
+
31
+ attr_reader(:headers, :body)
32
+ attr_accessor(:proxy_addr, :proxy_port, :proxy_user, :proxy_pass)
33
+
34
+ # @param headers [request.headers]
35
+ # @param body [request.body.read]
36
+ # @optional proxy_addr [String]
37
+ # @optional proxy_port [String]
38
+ # @optional proxy_user [String]
39
+ # @optional proxy_pass [String]
40
+ def initialize(
41
+ headers,
42
+ body,
43
+ proxy_addr: :ENV,
44
+ proxy_port: nil,
45
+ proxy_user: nil,
46
+ proxy_pass: nil)
47
+
48
+ @body = body
49
+ @raw = parse_from(@body)
50
+ @headers = headers
51
+ @proxy_addr = proxy_addr
52
+ @proxy_port = proxy_port
53
+ @proxy_user = proxy_user
54
+ @proxy_pass = proxy_pass
55
+ end
56
+
57
+ # This method will authenticate the ipn message sent from Amazon.
58
+ # It will return true if everything is verified. It will raise an
59
+ # error message if verification fails.
60
+ def authentic?
61
+ begin
62
+ decoded_from_base64 = Base64.decode64(signature)
63
+ validate_header
64
+ validate_subject(get_certificate.subject)
65
+ public_key = get_public_key_from(get_certificate)
66
+ verify_public_key(public_key, decoded_from_base64, canonical_string)
67
+
68
+ return true
69
+ rescue IpnWasNotAuthenticError => e
70
+ raise e.message
71
+ end
72
+ end
73
+
74
+ def type
75
+ @raw['Type']
76
+ end
77
+
78
+ def message_id
79
+ @raw['MessageId']
80
+ end
81
+
82
+ def topic_arn
83
+ @raw['TopicArn']
84
+ end
85
+
86
+ def message
87
+ @raw['Message']
88
+ end
89
+
90
+ def timestamp
91
+ @raw['Timestamp']
92
+ end
93
+
94
+ def signature
95
+ @raw['Signature']
96
+ end
97
+
98
+ def signature_version
99
+ @raw['SignatureVersion']
100
+ end
101
+
102
+ def signing_cert_url
103
+ @raw['SigningCertURL']
104
+ end
105
+
106
+ def unsubscribe_url
107
+ @raw['UnsubscribeURL']
108
+ end
109
+
110
+ def notification_type
111
+ parse_from(@raw['Message'])["NotificationType"]
112
+ end
113
+
114
+ def seller_id
115
+ parse_from(@raw['Message'])["SellerId"]
116
+ end
117
+
118
+ def environment
119
+ parse_from(@raw['Message'])["ReleaseEnvironment"]
120
+ end
121
+
122
+ def version
123
+ parse_from(@raw['Message'])["Version"]
124
+ end
125
+
126
+ def notification_data
127
+ parse_from(@raw['Message'])["NotificationData"]
128
+ end
129
+
130
+ def message_timestamp
131
+ parse_from(@raw['Message'])["Timestamp"]
132
+ end
133
+
134
+ def parse_from(json)
135
+ JSON.parse(json)
136
+ end
137
+
138
+ protected
139
+
140
+ def get_certificate
141
+ cert_pem = download_cert(signing_cert_url)
142
+ OpenSSL::X509::Certificate.new(cert_pem)
143
+ end
144
+
145
+ def get_public_key_from(certificate)
146
+ OpenSSL::PKey::RSA.new(certificate.public_key)
147
+ end
148
+
149
+ def canonical_string
150
+ text = ''
151
+ SIGNABLE_KEYS.each do |key|
152
+ value = @raw[key]
153
+ next if value.nil? or value.empty?
154
+ text << key << "\n"
155
+ text << value << "\n"
156
+ end
157
+ text
158
+ end
159
+
160
+ def download_cert(url)
161
+ uri = URI.parse(url)
162
+ unless
163
+ uri.scheme == 'https' &&
164
+ uri.host.match(/^sns\.[a-zA-Z0-9\-]{3,}\.amazonaws\.com(\.cn)?$/) &&
165
+ File.extname(uri.path) == '.pem'
166
+ then
167
+ msg = "Error - certificate is not hosted at AWS URL (https): #{url}"
168
+ raise IpnWasNotAuthenticError, msg
169
+ end
170
+ tries = 0
171
+ begin
172
+ resp = https_get(url)
173
+ resp.body
174
+ rescue => error
175
+ tries += 1
176
+ retry if tries < 3
177
+ raise error
178
+ end
179
+ end
180
+
181
+ def https_get(url)
182
+ uri = URI.parse(url)
183
+ http = Net::HTTP.new(uri.host, uri.port, @proxy_addr, @proxy_port, @proxy_user, @proxy_pass)
184
+ http.use_ssl = true
185
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
186
+ http.start
187
+ resp = http.request(Net::HTTP::Get.new(uri.request_uri))
188
+ http.finish
189
+ resp
190
+ end
191
+
192
+ def validate_header
193
+ unless
194
+ @headers['x-amz-sns-message-type'] == 'Notification'
195
+ then
196
+ msg = "Error - Header does not contain x-amz-sns-message-type header"
197
+ raise IpnWasNotAuthenticError, msg
198
+ end
199
+ end
200
+
201
+ def validate_subject(certificate_subject)
202
+ subject = certificate_subject.to_a
203
+ unless
204
+ subject[4][1] == COMMON_NAME
205
+ then
206
+ msg = "Error - Unable to verify certificate subject issued by Amazon"
207
+ raise IpnWasNotAuthenticError, msg
208
+ end
209
+ end
210
+
211
+ def verify_public_key(public_key, decoded_signature, signed_string)
212
+ unless
213
+ public_key.verify(OpenSSL::Digest::SHA1.new, decoded_signature, signed_string)
214
+ then
215
+ msg = "Error - Unable to verify public key with signature and signed string"
216
+ raise IpnWasNotAuthenticError, msg
217
+ end
218
+ end
219
+
220
+ end
221
+
222
+ end
@@ -0,0 +1,75 @@
1
+ require 'uri'
2
+ require 'net/http'
3
+ require 'net/https'
4
+ require 'json'
5
+ require 'openssl'
6
+
7
+ module AmazonPay
8
+
9
+ # AmazonPay API
10
+ #
11
+ # This class allows you to obtain user profile
12
+ # information once a user has logged into your
13
+ # application using their Amazon credentials.
14
+ class Login
15
+
16
+ attr_reader(:region)
17
+
18
+ attr_accessor(:client_id, :sandbox)
19
+
20
+ # @param client_id [String]
21
+ # @optional region [Symbol] Default: :na
22
+ # @optional sandbox [Boolean] Default: false
23
+ def initialize(client_id, region: :na, sandbox: false)
24
+ @client_id = client_id
25
+ @region = region
26
+ @endpoint = region_hash[@region]
27
+ @sandbox = sandbox
28
+ @sandbox_str = @sandbox ? "api.sandbox" : "api"
29
+ end
30
+
31
+ # This method will validate the access token and
32
+ # return the user's profile information.
33
+ # @param access_token [String]
34
+ def get_login_profile(access_token)
35
+ decoded_access_token = URI.decode(access_token)
36
+ encoded_access_token = URI.encode(decoded_access_token)
37
+ uri = URI("https://#{@sandbox_str}.#{@endpoint}/auth/o2/tokeninfo?access_token=#{encoded_access_token}")
38
+ req = Net::HTTP::Get.new(uri.request_uri)
39
+ http = Net::HTTP.new(uri.host, uri.port)
40
+ http.use_ssl = true
41
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
42
+ response = http.request(req)
43
+ decode = JSON.parse(response.body)
44
+
45
+ if decode['aud'] != @client_id
46
+ raise "Invalid Access Token"
47
+ end
48
+
49
+ uri = URI.parse("https://#{@sandbox_str}.#{@endpoint}/user/profile")
50
+ req = Net::HTTP::Get.new(uri.request_uri)
51
+ req['Authorization'] = "bearer " + decoded_access_token
52
+ http = Net::HTTP.new(uri.host, uri.port)
53
+ http.use_ssl = true
54
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
55
+ response = http.request(req)
56
+ decoded_login_profile = JSON.parse(response.body)
57
+ return decoded_login_profile
58
+ end
59
+
60
+ private
61
+
62
+ def region_hash
63
+ {
64
+ :jp => 'amazon.co.jp',
65
+ :uk => 'amazon.co.uk',
66
+ :de => 'amazon.de',
67
+ :eu => 'amazon.co.uk',
68
+ :us => 'amazon.com',
69
+ :na => 'amazon.com'
70
+ }
71
+ end
72
+
73
+ end
74
+
75
+ end
@@ -0,0 +1,114 @@
1
+ require 'uri'
2
+ require 'net/http'
3
+ require 'net/https'
4
+ require 'base64'
5
+ require 'openssl'
6
+
7
+ module AmazonPay
8
+
9
+ # This class creates the request to send to the
10
+ # specified MWS endpoint.
11
+ class Request
12
+
13
+ MAX_RETRIES = 3
14
+
15
+ def initialize(
16
+ parameters,
17
+ optional,
18
+ default_hash,
19
+ mws_endpoint,
20
+ sandbox_str,
21
+ secret_key,
22
+ proxy_addr,
23
+ proxy_port,
24
+ proxy_user,
25
+ proxy_pass,
26
+ throttle,
27
+ application_name,
28
+ application_version)
29
+
30
+ @parameters = parameters
31
+ @optional = optional
32
+ @default_hash = default_hash
33
+ @mws_endpoint = mws_endpoint
34
+ @sandbox_str = sandbox_str
35
+ @secret_key = secret_key
36
+ @proxy_addr = proxy_addr
37
+ @proxy_port = proxy_port
38
+ @proxy_user = proxy_user
39
+ @proxy_pass = proxy_pass
40
+ @throttle = throttle
41
+ @application_name = application_name
42
+ @application_version = application_version
43
+ end
44
+
45
+ # This method sends the post request.
46
+ def send_post
47
+ post_url = build_post_url
48
+ post(@mws_endpoint, @sandbox_str, post_url)
49
+ end
50
+
51
+ private
52
+
53
+ # This method combines the required and optional
54
+ # parameters to sign the post body and generate
55
+ # the post url.
56
+ def build_post_url
57
+ @optional.map { |k, v| @parameters[k] = v unless v.nil? }
58
+ @parameters = @default_hash.merge(@parameters)
59
+ post_url = @parameters.sort.map { |k, v| "#{k}=#{ custom_escape(v) }" }.join("&")
60
+ post_body = ["POST", "#{@mws_endpoint}", "/#{@sandbox_str}/#{AmazonPay::API_VERSION}", post_url].join("\n")
61
+ post_url += "&Signature=" + sign(post_body)
62
+ return post_url
63
+ end
64
+
65
+ # This method signs the post body that is being sent
66
+ # using the secret key provided.
67
+ def sign(post_body)
68
+ custom_escape(Base64.strict_encode64(OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, @secret_key, post_body)))
69
+ end
70
+
71
+ # This method performs the post to the MWS endpoint.
72
+ # It will retry three times after the initial post if
73
+ # the status code comes back as either 500 or 503.
74
+ def post(mws_endpoint, sandbox_str, post_url)
75
+ uri = URI("https://#{mws_endpoint}/#{sandbox_str}/#{AmazonPay::API_VERSION}")
76
+ https = Net::HTTP.new(uri.host, uri.port, @proxy_addr, @proxy_port, @proxy_user, @proxy_pass)
77
+ https.use_ssl = true
78
+ https.verify_mode = OpenSSL::SSL::VERIFY_PEER
79
+
80
+ user_agent = {"User-Agent" => "#{AmazonPay::SDK_NAME}/#{AmazonPay::VERSION}; (#{@application_name + '/' if @application_name }#{@application_version.to_s + ';' if @application_version} #{RUBY_VERSION}; #{RUBY_PLATFORM})"}
81
+
82
+ tries = 0
83
+ begin
84
+ response = https.post(uri.path, post_url, user_agent)
85
+ if @throttle.eql?(true)
86
+ if response.code.eql?('500')
87
+ raise 'InternalServerError'
88
+ elsif response.code.eql?('503')
89
+ raise 'ServiceUnavailable or RequestThrottled'
90
+ end
91
+ end
92
+ AmazonPay::Response.new(response)
93
+ rescue => error
94
+ tries += 1
95
+ sleep(get_seconds_for_try_count(tries))
96
+ retry if tries <= MAX_RETRIES
97
+ raise error.message
98
+ end
99
+ end
100
+
101
+ def get_seconds_for_try_count(try_count)
102
+ seconds = { 1=>1, 2=>4, 3=>10, 4=>0 }
103
+ seconds[try_count]
104
+ end
105
+
106
+ def custom_escape(val)
107
+ val.to_s.gsub(/([^\w.~-]+)/) do
108
+ "%" + $1.unpack("H2" * $1.bytesize).join("%").upcase
109
+ end
110
+ end
111
+
112
+ end
113
+
114
+ end
@@ -0,0 +1,42 @@
1
+ require 'rexml/document'
2
+
3
+ module AmazonPay
4
+
5
+ # This class provides helpers to parse the response
6
+ class Response
7
+
8
+ def initialize(response)
9
+ @response = response
10
+ end
11
+
12
+ def body
13
+ @response.body
14
+ end
15
+
16
+ def to_xml
17
+ REXML::Document.new(body)
18
+ end
19
+
20
+ def get_element(xpath, xml_element)
21
+ xml = self.to_xml
22
+ xml.elements.each(xpath) do |element|
23
+ @value = element.elements[xml_element].text
24
+ end
25
+ return @value
26
+ end
27
+
28
+ def code
29
+ @response.code
30
+ end
31
+
32
+ def success
33
+ if @response.code.eql? '200'
34
+ return true
35
+ else
36
+ return false
37
+ end
38
+ end
39
+
40
+ end
41
+
42
+ end
@@ -0,0 +1,5 @@
1
+ module AmazonPay
2
+ VERSION = "2.0.0"
3
+ SDK_NAME = "amazon-pay-sdk-ruby"
4
+ API_VERSION = "2013-01-01"
5
+ end
data/lib/amazon_pay.rb ADDED
@@ -0,0 +1,7 @@
1
+ require 'amazon_pay/client'
2
+ require 'amazon_pay/client_helper'
3
+ require 'amazon_pay/ipn_handler'
4
+ require 'amazon_pay/login'
5
+ require 'amazon_pay/request'
6
+ require 'amazon_pay/response'
7
+ require 'amazon_pay/version'