amazon_pay 2.0.0

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