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.
@@ -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
+