pay_with_amazon 1.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.
- checksums.yaml +7 -0
- data/LICENSE +202 -0
- data/NOTICE +9 -0
- data/README.md +181 -0
- data/lib/pay_with_amazon/client.rb +806 -0
- data/lib/pay_with_amazon/ipn_handler.rb +225 -0
- data/lib/pay_with_amazon/response.rb +43 -0
- data/lib/pay_with_amazon/version.rb +4 -0
- data/lib/pay_with_amazon.rb +4 -0
- metadata +52 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
require 'base64'
|
|
2
|
+
require 'json'
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'net/https'
|
|
5
|
+
require 'openssl'
|
|
6
|
+
require 'uri'
|
|
7
|
+
|
|
8
|
+
module PayWithAmazon
|
|
9
|
+
|
|
10
|
+
class IpnWasNotAuthenticError < StandardError
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Pay with Amazon 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
|
+
# To initialize this class you will be required to
|
|
35
|
+
# pass in the request header and body. The body can be
|
|
36
|
+
# passed in parsed by json or without.
|
|
37
|
+
# @param header [request.headers]
|
|
38
|
+
# @param body [request.body.read]
|
|
39
|
+
# @optional proxy_addr [String]
|
|
40
|
+
# @optional proxy_port [String]
|
|
41
|
+
# @optional proxy_user [String]
|
|
42
|
+
# @optional proxy_pass [String]
|
|
43
|
+
def initialize(
|
|
44
|
+
headers,
|
|
45
|
+
body,
|
|
46
|
+
proxy_addr: :ENV,
|
|
47
|
+
proxy_port: nil,
|
|
48
|
+
proxy_user: nil,
|
|
49
|
+
proxy_pass: nil)
|
|
50
|
+
|
|
51
|
+
@body = body
|
|
52
|
+
@raw = parse_from(@body)
|
|
53
|
+
@headers = headers
|
|
54
|
+
@proxy_addr = proxy_addr
|
|
55
|
+
@proxy_port = proxy_port
|
|
56
|
+
@proxy_user = proxy_user
|
|
57
|
+
@proxy_pass = proxy_pass
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# This method will authenticate the ipn message sent from Amazon.
|
|
61
|
+
# It will return true if everything is verified. It will display an
|
|
62
|
+
# error message if verification fails.
|
|
63
|
+
def authentic?
|
|
64
|
+
begin
|
|
65
|
+
decoded_from_base64 = Base64.decode64(signature)
|
|
66
|
+
validate_header
|
|
67
|
+
validate_subject(get_certificate.subject)
|
|
68
|
+
public_key = get_public_key_from(get_certificate)
|
|
69
|
+
verify_public_key(public_key, decoded_from_base64, canonical_string)
|
|
70
|
+
|
|
71
|
+
return true
|
|
72
|
+
rescue IpnWasNotAuthenticError => e
|
|
73
|
+
raise e.message
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def type
|
|
78
|
+
@raw['Type']
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def message_id
|
|
82
|
+
@raw['MessageId']
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def topic_arn
|
|
86
|
+
@raw['TopicArn']
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def message
|
|
90
|
+
@raw['Message']
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def timestamp
|
|
94
|
+
@raw['Timestamp']
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def signature
|
|
98
|
+
@raw['Signature']
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def signature_version
|
|
102
|
+
@raw['SignatureVersion']
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def signing_cert_url
|
|
106
|
+
@raw['SigningCertURL']
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def unsubscribe_url
|
|
110
|
+
@raw['UnsubscribeURL']
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def notification_type
|
|
114
|
+
parse_from(@raw['Message'])["NotificationType"]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def seller_id
|
|
118
|
+
parse_from(@raw['Message'])["SellerId"]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def environment
|
|
122
|
+
parse_from(@raw['Message'])["ReleaseEnvironment"]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def version
|
|
126
|
+
parse_from(@raw['Message'])["Version"]
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def notification_data
|
|
130
|
+
parse_from(@raw['Message'])["NotificationData"]
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def message_timestamp
|
|
134
|
+
parse_from(@raw['Message'])["Timestamp"]
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def parse_from(json)
|
|
138
|
+
JSON.parse(json)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
protected
|
|
142
|
+
|
|
143
|
+
def get_certificate
|
|
144
|
+
cert_pem = download_cert(signing_cert_url)
|
|
145
|
+
OpenSSL::X509::Certificate.new(cert_pem)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def get_public_key_from(certificate)
|
|
149
|
+
OpenSSL::PKey::RSA.new(certificate.public_key)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def canonical_string
|
|
153
|
+
text = ''
|
|
154
|
+
SIGNABLE_KEYS.each do |key|
|
|
155
|
+
value = @raw[key]
|
|
156
|
+
next if value.nil? or value.empty?
|
|
157
|
+
text << key << "\n"
|
|
158
|
+
text << value << "\n"
|
|
159
|
+
end
|
|
160
|
+
text
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def download_cert(url)
|
|
164
|
+
uri = URI.parse(url)
|
|
165
|
+
unless
|
|
166
|
+
uri.scheme == 'https' &&
|
|
167
|
+
uri.host.match(/^sns\.[a-zA-Z0-9\-]{3,}\.amazonaws\.com(\.cn)?$/) &&
|
|
168
|
+
File.extname(uri.path) == '.pem'
|
|
169
|
+
then
|
|
170
|
+
msg = "Error - certificate is not hosted at AWS URL (https): #{url}"
|
|
171
|
+
raise IpnWasNotAuthenticError, msg
|
|
172
|
+
end
|
|
173
|
+
tries = 0
|
|
174
|
+
begin
|
|
175
|
+
resp = https_get(url)
|
|
176
|
+
resp.body
|
|
177
|
+
rescue => error
|
|
178
|
+
tries += 1
|
|
179
|
+
retry if tries < 3
|
|
180
|
+
raise error
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def https_get(url)
|
|
185
|
+
uri = URI.parse(url)
|
|
186
|
+
http = Net::HTTP.new(uri.host, uri.port, @proxy_addr, @proxy_port, @proxy_user, @proxy_pass)
|
|
187
|
+
http.use_ssl = true
|
|
188
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
189
|
+
http.start
|
|
190
|
+
resp = http.request(Net::HTTP::Get.new(uri.request_uri))
|
|
191
|
+
http.finish
|
|
192
|
+
resp
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def validate_header
|
|
196
|
+
unless
|
|
197
|
+
@headers['x-amz-sns-message-type'] == 'Notification'
|
|
198
|
+
then
|
|
199
|
+
msg = "Error - Header does not contain x-amz-sns-message-type header"
|
|
200
|
+
raise IpnWasNotAuthenticError, msg
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def validate_subject(certificate_subject)
|
|
205
|
+
subject = certificate_subject.to_a
|
|
206
|
+
unless
|
|
207
|
+
subject[4][1] == COMMON_NAME
|
|
208
|
+
then
|
|
209
|
+
msg = "Error - Unable to verify certificate subject issued by Amazon"
|
|
210
|
+
raise IpnWasNotAuthenticError, msg
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def verify_public_key(public_key, decoded_signature, signed_string)
|
|
215
|
+
unless
|
|
216
|
+
public_key.verify(OpenSSL::Digest::SHA1.new, decoded_signature, signed_string)
|
|
217
|
+
then
|
|
218
|
+
msg = "Error - Unable to verify public key with signature and signed string"
|
|
219
|
+
raise IpnWasNotAuthenticError, msg
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
require 'rexml/document'
|
|
2
|
+
|
|
3
|
+
module PayWithAmazon
|
|
4
|
+
|
|
5
|
+
# This class provides helpers to parse the response
|
|
6
|
+
class Response
|
|
7
|
+
|
|
8
|
+
# @param response [String]
|
|
9
|
+
def initialize(response)
|
|
10
|
+
@response = response
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def body
|
|
14
|
+
@response.body
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_xml
|
|
18
|
+
REXML::Document.new(body)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def get_element(xpath, xml_element)
|
|
22
|
+
xml = self.to_xml
|
|
23
|
+
xml.elements.each(xpath) do |element|
|
|
24
|
+
@value = element.elements[xml_element].text
|
|
25
|
+
end
|
|
26
|
+
return @value
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def code
|
|
30
|
+
@response.code
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def success
|
|
34
|
+
if @response.code.eql? '200'
|
|
35
|
+
return true
|
|
36
|
+
else
|
|
37
|
+
return false
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: pay_with_amazon
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Amazon Payments
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2015-04-07 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Login and Pay with Amazon Ruby SDK
|
|
14
|
+
email:
|
|
15
|
+
executables: []
|
|
16
|
+
extensions: []
|
|
17
|
+
extra_rdoc_files: []
|
|
18
|
+
files:
|
|
19
|
+
- LICENSE
|
|
20
|
+
- NOTICE
|
|
21
|
+
- README.md
|
|
22
|
+
- lib/pay_with_amazon.rb
|
|
23
|
+
- lib/pay_with_amazon/client.rb
|
|
24
|
+
- lib/pay_with_amazon/ipn_handler.rb
|
|
25
|
+
- lib/pay_with_amazon/response.rb
|
|
26
|
+
- lib/pay_with_amazon/version.rb
|
|
27
|
+
homepage: https://github.com/amzn/login-and-pay-with-amazon-sdk-ruby
|
|
28
|
+
licenses:
|
|
29
|
+
- Apache License, Version 2.0
|
|
30
|
+
metadata: {}
|
|
31
|
+
post_install_message:
|
|
32
|
+
rdoc_options: []
|
|
33
|
+
require_paths:
|
|
34
|
+
- lib
|
|
35
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: 2.0.0
|
|
40
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
41
|
+
requirements:
|
|
42
|
+
- - ">="
|
|
43
|
+
- !ruby/object:Gem::Version
|
|
44
|
+
version: '0'
|
|
45
|
+
requirements: []
|
|
46
|
+
rubyforge_project:
|
|
47
|
+
rubygems_version: 2.4.5
|
|
48
|
+
signing_key:
|
|
49
|
+
specification_version: 4
|
|
50
|
+
summary: Login and Pay with Amazon Ruby SDK
|
|
51
|
+
test_files: []
|
|
52
|
+
has_rdoc:
|