boomerang 0.0.1
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.
- data/.gitignore +4 -0
- data/.rvmrc +1 -0
- data/README.markdown +103 -0
- data/Rakefile +6 -0
- data/boomerang.gemspec +30 -0
- data/data/ca-bundle.crt +7989 -0
- data/lib/boomerang.rb +132 -0
- data/lib/boomerang/api_call.rb +66 -0
- data/lib/boomerang/errors.rb +23 -0
- data/lib/boomerang/response.rb +43 -0
- data/lib/boomerang/signature.rb +77 -0
- data/lib/boomerang/utilities.rb +45 -0
- metadata +80 -0
data/lib/boomerang.rb
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
require "erb"
|
|
2
|
+
require "net/https"
|
|
3
|
+
require "openssl"
|
|
4
|
+
require "rexml/document"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
require "boomerang/errors"
|
|
8
|
+
require "boomerang/utilities"
|
|
9
|
+
require "boomerang/signature"
|
|
10
|
+
require "boomerang/api_call"
|
|
11
|
+
require "boomerang/response"
|
|
12
|
+
|
|
13
|
+
class Boomerang
|
|
14
|
+
VERSION = "0.0.1"
|
|
15
|
+
ENDPOINTS = { cbui: "https://authorize.payments.amazon.com/" +
|
|
16
|
+
"cobranded-ui/actions/start",
|
|
17
|
+
cbui_sandbox: "https://authorize.payments-sandbox.amazon.com/" +
|
|
18
|
+
"cobranded-ui/actions/start",
|
|
19
|
+
fps: "https://fps.amazonaws.com",
|
|
20
|
+
fps_sandbox: "https://fps.sandbox.amazonaws.com" }
|
|
21
|
+
PIPELINES = %w[ SingleUse MultiUse Recurring Recipient SetupPrepaid
|
|
22
|
+
SetupPostpaid EditToken ]
|
|
23
|
+
|
|
24
|
+
def initialize(access_key_id, secret_access_key, use_sandbox)
|
|
25
|
+
@access_key_id = access_key_id
|
|
26
|
+
@secret_access_key = secret_access_key
|
|
27
|
+
@use_sandbox = use_sandbox
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def use_sandbox?
|
|
31
|
+
@use_sandbox
|
|
32
|
+
end
|
|
33
|
+
alias_method :using_sandbox?, :use_sandbox?
|
|
34
|
+
|
|
35
|
+
def cbui_form(pipeline, parameters)
|
|
36
|
+
submit_tag = parameters.delete(:submit_tag) || %Q{<input type="submit">}
|
|
37
|
+
pipeline = Utilities.CamelCase(pipeline)
|
|
38
|
+
parameters = Utilities.camelCase(parameters).merge(
|
|
39
|
+
"version" => "2009-01-09",
|
|
40
|
+
"pipelineName" => pipeline
|
|
41
|
+
)
|
|
42
|
+
fail ArgumentError, "parameters must be a Hash" unless parameters.is_a? Hash
|
|
43
|
+
unless PIPELINES.include? pipeline
|
|
44
|
+
choices = "#{PIPELINES[0..-2].join(', ')}, or #{PIPELINES[-1]}"
|
|
45
|
+
fail ArgumentError, "pipline must be one of #{choices}"
|
|
46
|
+
end
|
|
47
|
+
unless parameters["returnUrl"]
|
|
48
|
+
fail ArgumentError, "returnUrl is a required parameter"
|
|
49
|
+
end
|
|
50
|
+
required_fields = case pipeline
|
|
51
|
+
when "Recipient"
|
|
52
|
+
%w[callerReference recipientPaysFee]
|
|
53
|
+
when "Recurring"
|
|
54
|
+
%w[callerReference recurringPeriod transactionAmount]
|
|
55
|
+
end
|
|
56
|
+
required_fields.each do |required_field|
|
|
57
|
+
unless parameters[required_field]
|
|
58
|
+
fail ArgumentError, "#{required_field} is a required parameter"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
url = ENDPOINTS[use_sandbox? ? :cbui_sandbox : :cbui]
|
|
63
|
+
signature = Signature.new("GET", url, parameters)
|
|
64
|
+
signature.sign(@access_key_id, @secret_access_key)
|
|
65
|
+
|
|
66
|
+
form = %Q{<form action="#{url}" method="GET">}
|
|
67
|
+
signature.signed_fields.each do |name, value|
|
|
68
|
+
form << %Q{<input type="hidden" } +
|
|
69
|
+
%Q{name="#{ERB::Util.h name}" value="#{ERB::Util.h value}">}
|
|
70
|
+
end
|
|
71
|
+
form << %Q{#{submit_tag}</form>}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def pay(parameters)
|
|
75
|
+
%w[marketplace_fixed_fee transaction_amount].each do |amount|
|
|
76
|
+
if dollars = parameters.delete(amount.to_sym)
|
|
77
|
+
parameters["#{amount}.currency_code"] = "USD"
|
|
78
|
+
parameters["#{amount}.value"] = dollars
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
parameters = Utilities.CamelCase(parameters)
|
|
82
|
+
%w[ CallerReference SenderTokenId TransactionAmount.CurrencyCode
|
|
83
|
+
TransactionAmount.Value ].each do |required_field|
|
|
84
|
+
unless parameters[required_field]
|
|
85
|
+
fail ArgumentError, "#{required_field} is a required parameter"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
call = APICall.new( ENDPOINTS[use_sandbox? ? :fps_sandbox : :fps],
|
|
90
|
+
:pay,
|
|
91
|
+
parameters )
|
|
92
|
+
call.sign(@access_key_id, @secret_access_key)
|
|
93
|
+
|
|
94
|
+
Response.new(call.response)
|
|
95
|
+
.parse( "/xmlns:PayResponse/",
|
|
96
|
+
"PayResult/TransactionId",
|
|
97
|
+
"PayResult/TransactionStatus",
|
|
98
|
+
"ResponseMetadata/RequestId" )
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def get_transaction_status(transaction_id)
|
|
102
|
+
call = APICall.new( ENDPOINTS[use_sandbox? ? :fps_sandbox : :fps],
|
|
103
|
+
:get_transaction_status,
|
|
104
|
+
transaction_id: transaction_id )
|
|
105
|
+
call.sign(@access_key_id, @secret_access_key)
|
|
106
|
+
|
|
107
|
+
Response.new(call.response)
|
|
108
|
+
.parse( "/xmlns:GetTransactionStatusResponse/",
|
|
109
|
+
"GetTransactionStatusResult/TransactionId",
|
|
110
|
+
"GetTransactionStatusResult/TransactionStatus",
|
|
111
|
+
"GetTransactionStatusResult/CallerReference",
|
|
112
|
+
"GetTransactionStatusResult/StatusCode",
|
|
113
|
+
"GetTransactionStatusResult/StatusMessage",
|
|
114
|
+
"ResponseMetadata/RequestId" )
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def verify_signature?(url, parameters)
|
|
118
|
+
parameters = parameters.is_a?(Hash) ? Utilities.build_query(parameters) :
|
|
119
|
+
parameters.to_s
|
|
120
|
+
|
|
121
|
+
call = APICall.new( ENDPOINTS[use_sandbox? ? :fps_sandbox : :fps],
|
|
122
|
+
:verify_signature,
|
|
123
|
+
url_end_point: url,
|
|
124
|
+
http_parameters: parameters )
|
|
125
|
+
|
|
126
|
+
parsed = Response.new(call.response)
|
|
127
|
+
.parse( "/xmlns:VerifySignatureResponse/",
|
|
128
|
+
"VerifySignatureResult/VerificationStatus",
|
|
129
|
+
"ResponseMetadata/RequestId" )
|
|
130
|
+
parsed[:verification_status] == "Success"
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
class Boomerang
|
|
2
|
+
class APICall
|
|
3
|
+
CA_PATH = File.join(File.dirname(__FILE__), *%w[.. .. data ca-bundle.crt])
|
|
4
|
+
|
|
5
|
+
def initialize(host, action, parameters)
|
|
6
|
+
@host = host
|
|
7
|
+
@parameters = Utilities.CamelCase(parameters)
|
|
8
|
+
.merge( "Action" => Utilities.CamelCase(action),
|
|
9
|
+
"Version" => "2008-09-17" )
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def sign(access_key_id, secret_access_key)
|
|
13
|
+
signature = Signature.new("GET", @host, @parameters)
|
|
14
|
+
signature.sign(access_key_id, secret_access_key)
|
|
15
|
+
@parameters = signature.signed_fields
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def response
|
|
19
|
+
url = "#{@host}/?#{Utilities.build_query(@parameters)}"
|
|
20
|
+
uri = URI.parse(url)
|
|
21
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
22
|
+
http.use_ssl = true
|
|
23
|
+
http.ca_file = CA_PATH
|
|
24
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
25
|
+
http.verify_depth = 5
|
|
26
|
+
|
|
27
|
+
begin
|
|
28
|
+
response = http.start { |session|
|
|
29
|
+
get = Net::HTTP::Get.new("#{uri.path}?#{uri.query}")
|
|
30
|
+
if (response = session.request(get)).is_a? Net::HTTPSuccess
|
|
31
|
+
begin
|
|
32
|
+
response.body
|
|
33
|
+
rescue StandardError => error
|
|
34
|
+
fail wrap_error( "HTTP",
|
|
35
|
+
"#{error.message} (#{error.class.name})",
|
|
36
|
+
http_response: response,
|
|
37
|
+
original_error: error )
|
|
38
|
+
end
|
|
39
|
+
else
|
|
40
|
+
fail wrap_error( "HTTP",
|
|
41
|
+
"#{response.message} (#{response.class.name})",
|
|
42
|
+
http_response: response )
|
|
43
|
+
end
|
|
44
|
+
}
|
|
45
|
+
rescue Errors::HTTPError
|
|
46
|
+
fail # pass through already wrapped errors
|
|
47
|
+
rescue StandardError => error
|
|
48
|
+
fail wrap_error( "Connection",
|
|
49
|
+
"#{error.message} (#{error.class.name})",
|
|
50
|
+
original_error: error )
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
#######
|
|
55
|
+
private
|
|
56
|
+
#######
|
|
57
|
+
|
|
58
|
+
def wrap_error(base, message, additional_fields)
|
|
59
|
+
wrapped_error = Errors.const_get("#{base}Error").new(message)
|
|
60
|
+
additional_fields.each do |field_name, field_data|
|
|
61
|
+
wrapped_error.send("#{field_name}=", field_data)
|
|
62
|
+
end
|
|
63
|
+
wrapped_error
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
class Boomerang
|
|
2
|
+
module Errors
|
|
3
|
+
class BoomerangError < RuntimeError
|
|
4
|
+
# do nothing: just creating a base error type
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
class ConnectionError < BoomerangError
|
|
8
|
+
attr_accessor :original_error
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class HTTPError < BoomerangError
|
|
12
|
+
attr_accessor :http_response, :original_error
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class AWSError < BoomerangError
|
|
16
|
+
attr_accessor :other_errors, :request_id
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.const_missing(error_name) # :nodoc:
|
|
20
|
+
const_set(error_name, Class.new(AWSError))
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
class Boomerang
|
|
2
|
+
class Response
|
|
3
|
+
def initialize(body)
|
|
4
|
+
@body = body
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def parse(prefix, *elements)
|
|
8
|
+
xml = REXML::Document.new(@body)
|
|
9
|
+
parse_and_fail_with_errors_if_any(xml)
|
|
10
|
+
parse_response(xml, prefix, elements)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
#######
|
|
14
|
+
private
|
|
15
|
+
#######
|
|
16
|
+
|
|
17
|
+
def parse_and_fail_with_errors_if_any(xml)
|
|
18
|
+
errors = [ ]
|
|
19
|
+
xml.elements.each("/Response/Errors/Error") do |error|
|
|
20
|
+
if (code = error.elements["Code"]) and
|
|
21
|
+
(message = error.elements["Message"])
|
|
22
|
+
errors << Errors.const_get(code.text).new(message.text)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
unless errors.empty?
|
|
26
|
+
first_error = errors.first
|
|
27
|
+
first_error.other_errors = Array(errors[1..-1])
|
|
28
|
+
if node = xml.elements["/Response/RequestID"]
|
|
29
|
+
first_error.request_id = node.text
|
|
30
|
+
end
|
|
31
|
+
fail first_error
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def parse_response(xml, prefix, elements)
|
|
36
|
+
Hash[ elements.map { |field|
|
|
37
|
+
if node = xml.elements["#{prefix}#{field}"]
|
|
38
|
+
[Utilities.snake_case(field[/\w+\z/]).to_sym, node.text]
|
|
39
|
+
end
|
|
40
|
+
}.compact ]
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
class Boomerang
|
|
2
|
+
class Signature
|
|
3
|
+
SHA1_ALGORITHM = OpenSSL::Digest::Digest.new("sha1")
|
|
4
|
+
SHA256_ALGORITHM = OpenSSL::Digest::Digest.new("sha256")
|
|
5
|
+
|
|
6
|
+
def initialize(http_verb, url, parameters, algorithm = SHA256_ALGORITHM)
|
|
7
|
+
@fps = !!(url =~ /\bfps\b/)
|
|
8
|
+
@http_verb = http_verb
|
|
9
|
+
@url = URI.parse(url)
|
|
10
|
+
@parameters = parameters.merge(
|
|
11
|
+
Utilities.camel_case(
|
|
12
|
+
{ :signature_version => "2",
|
|
13
|
+
:signature_method => algorithm == SHA256_ALGORITHM ?
|
|
14
|
+
"HmacSHA256" :
|
|
15
|
+
"HmacSHA1" },
|
|
16
|
+
fps?
|
|
17
|
+
)
|
|
18
|
+
)
|
|
19
|
+
if fps?
|
|
20
|
+
@parameters["Timestamp"] = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
21
|
+
end
|
|
22
|
+
@algorithm = algorithm
|
|
23
|
+
@signature = nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def sign(access_key_id, secret_access_key)
|
|
27
|
+
@parameters[ fps? ? "AWSAccessKeyId" :
|
|
28
|
+
"callerKey" ] = access_key_id
|
|
29
|
+
@signature = hash(secret_access_key)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def signed_fields
|
|
33
|
+
@parameters.merge(Utilities.camel_case(:signature, fps?) => @signature)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
#######
|
|
37
|
+
private
|
|
38
|
+
#######
|
|
39
|
+
|
|
40
|
+
def fps?
|
|
41
|
+
@fps
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def canonicalized_query_string
|
|
45
|
+
@parameters.keys
|
|
46
|
+
.map(&Utilities.method(:utf8))
|
|
47
|
+
.sort
|
|
48
|
+
.map { |name| [
|
|
49
|
+
Utilities.url_encode(name),
|
|
50
|
+
Utilities.url_encode(Utilities.utf8(@parameters[name]))
|
|
51
|
+
].join("=") }
|
|
52
|
+
.join("&")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def value_of_host_header_in_lowercase
|
|
56
|
+
@url.host
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def http_request_uri
|
|
60
|
+
path = @url.path
|
|
61
|
+
path.nil? || path.empty? ? "/" : path
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def string_to_sign
|
|
65
|
+
[ @http_verb,
|
|
66
|
+
value_of_host_header_in_lowercase,
|
|
67
|
+
http_request_uri,
|
|
68
|
+
canonicalized_query_string ].join("\n")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def hash(secret_access_key)
|
|
72
|
+
[ OpenSSL::HMAC.digest( @algorithm,
|
|
73
|
+
secret_access_key,
|
|
74
|
+
string_to_sign ) ].pack("m").chomp
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
class Boomerang
|
|
2
|
+
module Utilities
|
|
3
|
+
extend self
|
|
4
|
+
|
|
5
|
+
def camel_case(hash_or_string, first_cap = true)
|
|
6
|
+
if hash_or_string.is_a? Hash
|
|
7
|
+
Hash[hash_or_string.map { |k, v| [camel_case(k, first_cap), v]}]
|
|
8
|
+
else
|
|
9
|
+
string = hash_or_string.to_s
|
|
10
|
+
string = string.gsub(/(\A|\.)[a-z]/) { $&.upcase } if first_cap
|
|
11
|
+
string.gsub(/_([a-z])/) { $1.upcase }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
alias_method :CamelCase, :camel_case
|
|
15
|
+
def camelCase(hash_or_string, first_cap = false)
|
|
16
|
+
camel_case(hash_or_string, first_cap)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def snake_case(hash_or_string)
|
|
20
|
+
if hash_or_string.is_a? Hash
|
|
21
|
+
Hash[hash_or_string.map { |k, v| [snake_case(k), v]}]
|
|
22
|
+
else
|
|
23
|
+
hash_or_string.to_s
|
|
24
|
+
.gsub(/([a-z])([A-Z])/) { "#{$1}_#{$2.downcase}" }
|
|
25
|
+
.downcase
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def utf8(string)
|
|
30
|
+
string.to_s.encode(Encoding::UTF_8)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def url_encode(string)
|
|
34
|
+
string.chars
|
|
35
|
+
.map { |char| char =~ /\A[-A-Za-z0-9_.~]\z/ ?
|
|
36
|
+
char :
|
|
37
|
+
char.bytes.map { |byte| "%%%02X" % byte }.join }
|
|
38
|
+
.join
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def build_query(parameters)
|
|
42
|
+
parameters.map { |k, v| "#{url_encode k}=#{url_encode v}" }.join("&")
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: boomerang
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
prerelease: false
|
|
5
|
+
segments:
|
|
6
|
+
- 0
|
|
7
|
+
- 0
|
|
8
|
+
- 1
|
|
9
|
+
version: 0.0.1
|
|
10
|
+
platform: ruby
|
|
11
|
+
authors:
|
|
12
|
+
- James Edward Gray II
|
|
13
|
+
autorequire:
|
|
14
|
+
bindir: bin
|
|
15
|
+
cert_chain: []
|
|
16
|
+
|
|
17
|
+
date: 2011-03-06 00:00:00 -06:00
|
|
18
|
+
default_executable:
|
|
19
|
+
dependencies: []
|
|
20
|
+
|
|
21
|
+
description: A library for working with Amazon.com's FPS API. The code is especially intended for "marketplace applications."
|
|
22
|
+
email:
|
|
23
|
+
- james@graysoftinc.com
|
|
24
|
+
executables: []
|
|
25
|
+
|
|
26
|
+
extensions: []
|
|
27
|
+
|
|
28
|
+
extra_rdoc_files: []
|
|
29
|
+
|
|
30
|
+
files:
|
|
31
|
+
- .gitignore
|
|
32
|
+
- .rvmrc
|
|
33
|
+
- README.markdown
|
|
34
|
+
- Rakefile
|
|
35
|
+
- boomerang.gemspec
|
|
36
|
+
- data/ca-bundle.crt
|
|
37
|
+
- lib/boomerang.rb
|
|
38
|
+
- lib/boomerang/api_call.rb
|
|
39
|
+
- lib/boomerang/errors.rb
|
|
40
|
+
- lib/boomerang/response.rb
|
|
41
|
+
- lib/boomerang/signature.rb
|
|
42
|
+
- lib/boomerang/utilities.rb
|
|
43
|
+
has_rdoc: true
|
|
44
|
+
homepage: https://github.com/okrb/boomerang
|
|
45
|
+
licenses: []
|
|
46
|
+
|
|
47
|
+
post_install_message:
|
|
48
|
+
rdoc_options: []
|
|
49
|
+
|
|
50
|
+
require_paths:
|
|
51
|
+
- lib
|
|
52
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
53
|
+
none: false
|
|
54
|
+
requirements:
|
|
55
|
+
- - ~>
|
|
56
|
+
- !ruby/object:Gem::Version
|
|
57
|
+
segments:
|
|
58
|
+
- 1
|
|
59
|
+
- 9
|
|
60
|
+
- 2
|
|
61
|
+
version: 1.9.2
|
|
62
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
63
|
+
none: false
|
|
64
|
+
requirements:
|
|
65
|
+
- - ~>
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
segments:
|
|
68
|
+
- 1
|
|
69
|
+
- 3
|
|
70
|
+
- 7
|
|
71
|
+
version: 1.3.7
|
|
72
|
+
requirements: []
|
|
73
|
+
|
|
74
|
+
rubyforge_project:
|
|
75
|
+
rubygems_version: 1.3.7
|
|
76
|
+
signing_key:
|
|
77
|
+
specification_version: 3
|
|
78
|
+
summary: A Ruby library wrapping Amazon.com's FPS API.
|
|
79
|
+
test_files: []
|
|
80
|
+
|