braintree 2.30.0 → 2.30.2

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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +1 -1
  3. data/lib/braintree.rb +5 -1
  4. data/lib/braintree/client_token.rb +18 -0
  5. data/lib/braintree/client_token_gateway.rb +30 -0
  6. data/lib/braintree/configuration.rb +29 -0
  7. data/lib/braintree/credit_card.rb +4 -0
  8. data/lib/braintree/credit_card_gateway.rb +9 -1
  9. data/lib/braintree/customer.rb +4 -0
  10. data/lib/braintree/gateway.rb +4 -0
  11. data/lib/braintree/http.rb +13 -1
  12. data/lib/braintree/sha256_digest.rb +13 -0
  13. data/lib/braintree/signature_service.rb +19 -0
  14. data/lib/braintree/subscription_gateway.rb +2 -0
  15. data/lib/braintree/successful_result.rb +4 -6
  16. data/lib/braintree/transaction_gateway.rb +1 -1
  17. data/lib/braintree/transparent_redirect_gateway.rb +3 -8
  18. data/lib/braintree/version.rb +1 -1
  19. data/lib/braintree/webhook_notification_gateway.rb +11 -5
  20. data/lib/braintree/webhook_testing_gateway.rb +13 -13
  21. data/spec/httpsd.pid +1 -1
  22. data/spec/integration/braintree/client_api/client_token_spec.rb +143 -0
  23. data/spec/integration/braintree/client_api/spec_helper.rb +80 -0
  24. data/spec/integration/braintree/credit_card_spec.rb +99 -0
  25. data/spec/integration/braintree/customer_spec.rb +24 -0
  26. data/spec/integration/braintree/subscription_spec.rb +40 -1
  27. data/spec/integration/braintree/transaction_search_spec.rb +2 -2
  28. data/spec/integration/braintree/transaction_spec.rb +27 -5
  29. data/spec/unit/braintree/client_token_spec.rb +37 -0
  30. data/spec/unit/braintree/configuration_spec.rb +30 -0
  31. data/spec/unit/braintree/credit_card_spec.rb +2 -0
  32. data/spec/unit/braintree/customer_spec.rb +2 -0
  33. data/spec/unit/braintree/digest_spec.rb +14 -0
  34. data/spec/unit/braintree/http_spec.rb +19 -0
  35. data/spec/unit/braintree/sha256_digest_spec.rb +11 -0
  36. data/spec/unit/braintree/signature_service_spec.rb +23 -0
  37. data/spec/unit/braintree/successful_result_spec.rb +7 -7
  38. data/spec/unit/braintree/transparent_redirect_spec.rb +8 -1
  39. data/spec/unit/braintree/webhook_notification_spec.rb +58 -4
  40. metadata +126 -121
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 8344ccd108c80b288f1aa06fe5ba464b7b30fc72
4
+ data.tar.gz: 1f31b8e13d92045c7f195764aff85bf54d0e822f
5
+ SHA512:
6
+ metadata.gz: 1433ff6de818797a42e80d011fb76cbb8365e7b83f2071c2c8f8f9855bc1f9acee30c3a133f0d804453e1695345bcdd44c74248438730876017e67dac1279b9f
7
+ data.tar.gz: c2c7febb1f2827454eb232ed952e03500d0fb720dec58234cdbfe36b0c72b387065cc5fb8b968928f19788ff18a82497bbbeb1acf670598b2eb5d9a61ec71e07
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2009-2010 Braintree Payment Solutions
1
+ Copyright (c) 2009-2014 Braintree, a division of PayPal, Inc.
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person
4
4
  obtaining a copy of this software and associated documentation
@@ -22,10 +22,12 @@ require "braintree/modification"
22
22
 
23
23
  require "braintree/add_on"
24
24
  require "braintree/add_on_gateway"
25
- require "braintree/address/country_names"
26
25
  require "braintree/address"
26
+ require "braintree/address/country_names"
27
27
  require "braintree/address_gateway"
28
28
  require "braintree/advanced_search"
29
+ require "braintree/client_token"
30
+ require "braintree/client_token_gateway"
29
31
  require "braintree/configuration"
30
32
  require "braintree/credit_card"
31
33
  require "braintree/credit_card_gateway"
@@ -56,6 +58,8 @@ require "braintree/plan_gateway"
56
58
  require "braintree/settlement_batch_summary"
57
59
  require "braintree/settlement_batch_summary_gateway"
58
60
  require "braintree/resource_collection"
61
+ require "braintree/sha256_digest"
62
+ require "braintree/signature_service"
59
63
  require "braintree/subscription"
60
64
  require "braintree/subscription_gateway"
61
65
  require "braintree/subscription_search"
@@ -0,0 +1,18 @@
1
+ require 'json'
2
+
3
+ module Braintree
4
+ module ClientToken
5
+ def self.generate(options={})
6
+ _validate_options(options)
7
+ Configuration.gateway.client_token.generate(options)
8
+ end
9
+
10
+ def self._validate_options(options)
11
+ [:make_default, :fail_on_duplicate_payment_method, :verify_card].each do |credit_card_option|
12
+ if options[credit_card_option]
13
+ raise ArgumentError.new("cannot specify #{credit_card_option} without a customer_id") unless options[:customer_id]
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,30 @@
1
+ module Braintree
2
+ class ClientTokenGateway
3
+ def initialize(gateway)
4
+ @gateway = gateway
5
+ @config = gateway.config
6
+ end
7
+
8
+ def generate(options={})
9
+ params = nil
10
+ if options
11
+ Util.verify_keys(ClientTokenGateway._generate_signature, options)
12
+ params = {:client_token => options}
13
+ end
14
+ result = @config.http.post("/client_token", params)
15
+
16
+ if result[:client_token]
17
+ result[:client_token][:value]
18
+ else
19
+ raise ArgumentError, result[:api_error_response][:message]
20
+ end
21
+ end
22
+
23
+ def self._generate_signature # :nodoc:
24
+ [
25
+ :customer_id, :proxy_merchant_id,
26
+ {:options => [:make_default, :verify_card, :fail_on_duplicate_payment_method]}
27
+ ]
28
+ end
29
+ end
30
+ end
@@ -48,6 +48,14 @@ module Braintree
48
48
  @logger ||= _default_logger
49
49
  end
50
50
 
51
+ def self.signature_service
52
+ instantiate.signature_service
53
+ end
54
+
55
+ def self.sha256_signature_service
56
+ instantiate.sha256_signature_service
57
+ end
58
+
51
59
  def initialize(options = {})
52
60
  [:endpoint, :environment, :public_key, :private_key, :custom_user_agent, :logger].each do |attr|
53
61
  instance_variable_set "@#{attr}", options[attr]
@@ -110,6 +118,19 @@ module Braintree
110
118
  end
111
119
  end
112
120
 
121
+ def auth_url
122
+ case @environment
123
+ when :development
124
+ "http://auth.venmo.dev:9292"
125
+ when :production
126
+ "https://auth.venmo.com"
127
+ when :qa
128
+ "https://auth.venmo.qa2.braintreegateway.com"
129
+ when :sandbox
130
+ "https://auth.venmo.sandbox.braintreegateway.com"
131
+ end
132
+ end
133
+
113
134
  def ssl? # :nodoc:
114
135
  case @environment
115
136
  when :development
@@ -133,5 +154,13 @@ module Braintree
133
154
  def inspect
134
155
  super.gsub(/@private_key=\".*\"/, '@private_key="[FILTERED]"')
135
156
  end
157
+
158
+ def signature_service
159
+ @signature_service ||= SignatureService.new(@private_key)
160
+ end
161
+
162
+ def sha256_signature_service
163
+ @sha256_signature_service ||= SignatureService.new(@private_key, SHA256Digest)
164
+ end
136
165
  end
137
166
  end
@@ -94,6 +94,10 @@ module Braintree
94
94
  Configuration.gateway.credit_card.find(token)
95
95
  end
96
96
 
97
+ def self.from_nonce(nonce)
98
+ Configuration.gateway.credit_card.from_nonce(nonce)
99
+ end
100
+
97
101
  # See http://www.braintreepayments.com/docs/ruby/transactions/create_from_vault
98
102
  def self.sale(token, transaction_attributes)
99
103
  Configuration.gateway.transaction.sale(transaction_attributes.merge(:payment_method_token => token))
@@ -48,6 +48,14 @@ module Braintree
48
48
  raise NotFoundError, "payment method with token #{token.inspect} not found"
49
49
  end
50
50
 
51
+ def from_nonce(nonce)
52
+ raise ArgumentError if nonce.nil? || nonce.to_s.strip == ""
53
+ response = @config.http.get "/payment_methods/from_nonce/#{nonce}"
54
+ CreditCard._new(@gateway, response[:credit_card])
55
+ rescue NotFoundError
56
+ raise NotFoundError, "nonce #{nonce.inspect} locked, consumed, or not found"
57
+ end
58
+
51
59
  def update(token, attributes)
52
60
  Util.verify_keys(CreditCardGateway._update_signature, attributes)
53
61
  _do_update(:put, "/payment_methods/#{token}", :credit_card => attributes)
@@ -80,7 +88,7 @@ module Braintree
80
88
  signature = [
81
89
  :billing_address_id, :cardholder_name, :cvv, :device_session_id, :expiration_date,
82
90
  :expiration_month, :expiration_year, :number, :token, :venmo_sdk_payment_method_code,
83
- :device_data, :fraud_merchant_id,
91
+ :device_data, :fraud_merchant_id, :payment_method_nonce,
84
92
  {:options => options},
85
93
  {:billing_address => billing_address_params}
86
94
  ]
@@ -202,5 +202,9 @@ module Braintree
202
202
  :created_at, :updated_at
203
203
  ]
204
204
  end
205
+
206
+ def self._now_timestamp # :nodoc:
207
+ Time.now.to_i
208
+ end
205
209
  end
206
210
  end
@@ -21,6 +21,10 @@ module Braintree
21
21
  AddressGateway.new(self)
22
22
  end
23
23
 
24
+ def client_token
25
+ ClientTokenGateway.new(self)
26
+ end
27
+
24
28
  def credit_card
25
29
  CreditCardGateway.new(self)
26
30
  end
@@ -14,7 +14,8 @@ module Braintree
14
14
  end
15
15
  end
16
16
 
17
- def get(path)
17
+ def get(_path, query_params={})
18
+ path = _path + _build_query_string(query_params)
18
19
  response = _http_do Net::HTTP::Get, path
19
20
  if response.code.to_i == 200 || response.code.to_i == 422
20
21
  Xml.hash_from_xml(_body(response))
@@ -46,6 +47,17 @@ module Braintree
46
47
  Braintree::Xml.hash_to_xml params
47
48
  end
48
49
 
50
+ def _build_query_string(params)
51
+ if params.empty?
52
+ ""
53
+ else
54
+ "?" + params.map do |x, y|
55
+ raise(ArgumentError, "Nested hashes aren't supported in query parameters") if y.respond_to?(:to_hash)
56
+ "#{x}=#{y}"
57
+ end.join("&")
58
+ end
59
+ end
60
+
49
61
  def _http_do(http_verb, path, body = nil)
50
62
  connection = Net::HTTP.new(@config.server, @config.port)
51
63
  connection.read_timeout = 60
@@ -0,0 +1,13 @@
1
+ module Braintree
2
+ module SHA256Digest # :nodoc:
3
+ def self.hexdigest(private_key, string)
4
+ _hmac(private_key, string)
5
+ end
6
+
7
+ def self._hmac(key, message)
8
+ key_digest = ::Digest::SHA256.digest(key)
9
+ sha256 = OpenSSL::Digest::Digest.new("sha256")
10
+ OpenSSL::HMAC.hexdigest(sha256, key_digest, message.to_s)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ module Braintree
2
+ class SignatureService
3
+ attr_reader :key
4
+
5
+ def initialize(key, digest=Braintree::Digest)
6
+ @key = key
7
+ @digest = digest
8
+ end
9
+
10
+ def sign(data)
11
+ query_string = Util.hash_to_query_string(data)
12
+ "#{hash(query_string)}|#{query_string}"
13
+ end
14
+
15
+ def hash(data)
16
+ @digest.hexdigest(@key, data)
17
+ end
18
+ end
19
+ end
@@ -60,6 +60,7 @@ module Braintree
60
60
  :never_expires,
61
61
  :number_of_billing_cycles,
62
62
  :payment_method_token,
63
+ :payment_method_nonce,
63
64
  :plan_id,
64
65
  :price,
65
66
  :trial_duration,
@@ -77,6 +78,7 @@ module Braintree
77
78
  :never_expires,
78
79
  :number_of_billing_cycles,
79
80
  :payment_method_token,
81
+ :payment_method_nonce,
80
82
  :plan_id,
81
83
  :price,
82
84
  {:options => [
@@ -3,14 +3,12 @@ module Braintree
3
3
  class SuccessfulResult
4
4
  include BaseModule
5
5
 
6
+ attr_reader :address, :credit_card, :customer, :merchant_account, :settlement_batch_summary, :subscription, :new_transaction, :transaction
7
+
6
8
  def initialize(attributes = {}) # :nodoc:
7
9
  @attrs = attributes.keys
8
- singleton_class.class_eval do
9
- attributes.each do |key, value|
10
- define_method key do
11
- value
12
- end
13
- end
10
+ attributes.each do |key, value|
11
+ instance_variable_set("@#{key}", value)
14
12
  end
15
13
  end
16
14
 
@@ -117,7 +117,7 @@ module Braintree
117
117
  :amount, :customer_id, :merchant_account_id, :order_id, :channel, :payment_method_token,
118
118
  :purchase_order_number, :recurring, :shipping_address_id, :type, :tax_amount, :tax_exempt,
119
119
  :venmo_sdk_payment_method_code, :device_session_id, :service_fee_amount, :device_data, :fraud_merchant_id,
120
- :billing_address_id,
120
+ :billing_address_id, :payment_method_nonce,
121
121
  {:credit_card => [:token, :cardholder_name, :cvv, :expiration_date, :expiration_month, :expiration_year, :number]},
122
122
  {:customer => [:id, :company, :email, :fax, :first_name, :last_name, :phone, :website]},
123
123
  {
@@ -51,7 +51,7 @@ module Braintree
51
51
 
52
52
  query_strings_without_hash = [query_string_without_hash, encoded_query_string_without_hash, decoded_query_string_without_hash]
53
53
 
54
- if query_strings_without_hash.any? { |query_string| _hash(query_string) == params[:hash] }
54
+ if query_strings_without_hash.any? { |query_string| @config.signature_service.hash(query_string) == params[:hash] }
55
55
  params
56
56
  else
57
57
  raise ForgedQueryString
@@ -92,17 +92,12 @@ module Braintree
92
92
 
93
93
  def _data(params) # :nodoc:
94
94
  raise ArgumentError, "expected params to contain :redirect_url" unless params[:redirect_url]
95
- tr_data_segment = Util.hash_to_query_string(params.merge(
95
+
96
+ @config.signature_service.sign(params.merge(
96
97
  :api_version => @config.api_version,
97
98
  :time => Time.now.utc.strftime("%Y%m%d%H%M%S"),
98
99
  :public_key => @config.public_key
99
100
  ))
100
- tr_data_hash = _hash(tr_data_segment)
101
- "#{tr_data_hash}|#{tr_data_segment}"
102
- end
103
-
104
- def _hash(string) # :nodoc:
105
- ::Braintree::Digest.hexdigest(@config.private_key, string)
106
101
  end
107
102
  end
108
103
  end
@@ -2,7 +2,7 @@ module Braintree
2
2
  module Version
3
3
  Major = 2
4
4
  Minor = 30
5
- Tiny = 0
5
+ Tiny = 2
6
6
 
7
7
  String = "#{Major}.#{Minor}.#{Tiny}"
8
8
  end
@@ -6,6 +6,9 @@ module Braintree
6
6
  end
7
7
 
8
8
  def parse(signature_string, payload)
9
+ if payload =~ /[^A-Za-z0-9+=\/\n]/
10
+ raise InvalidSignature, "payload contains illegal characters"
11
+ end
9
12
  _verify_signature(signature_string, payload)
10
13
  attributes = Xml.hash_from_xml(Base64.decode64(payload))
11
14
  WebhookNotification._new(@gateway, attributes[:notification])
@@ -25,12 +28,15 @@ module Braintree
25
28
  end
26
29
  end
27
30
 
28
- def _verify_signature(signature, payload)
29
- public_key, signature = _matching_signature_pair(signature)
30
- payload_signature = Braintree::Digest.hexdigest(@config.private_key, payload)
31
+ def _verify_signature(signature_string, payload)
32
+ public_key, signature = _matching_signature_pair(signature_string)
33
+ raise InvalidSignature, 'no matching public key' if public_key.nil?
31
34
 
32
- raise InvalidSignature if public_key.nil?
33
- raise InvalidSignature unless Braintree::Digest.secure_compare(signature, payload_signature)
35
+ signature_matches = [payload, payload + "\n"].any? do |payload|
36
+ payload_signature = Braintree::Digest.hexdigest(@config.private_key, payload)
37
+ Braintree::Digest.secure_compare(signature, payload_signature)
38
+ end
39
+ raise InvalidSignature, 'signature does not match payload - one has been modified' unless signature_matches
34
40
  end
35
41
  end
36
42
  end
@@ -65,31 +65,31 @@ module Braintree
65
65
  def _partner_merchant_connected_sample_xml(data)
66
66
 
67
67
  <<-XML
68
- <partner_merchant>
69
- <merchant_public_id>public_id</merchant_public_id>
70
- <public_key>public_key</public_key>
71
- <private_key>private_key</private_key>
72
- <partner_merchant_id>abc123</partner_merchant_id>
73
- <client_side_encryption_key>cse_key</client_side_encryption_key>
74
- </partner_merchant>
68
+ <partner-merchant>
69
+ <merchant-public-id>public_id</merchant-public-id>
70
+ <public-key>public_key</public-key>
71
+ <private-key>private_key</private-key>
72
+ <partner-merchant-id>abc123</partner-merchant-id>
73
+ <client-side-encryption-key>cse_key</client-side-encryption-key>
74
+ </partner-merchant>
75
75
  XML
76
76
  end
77
77
 
78
78
  def _partner_merchant_disconnected_sample_xml(data)
79
79
 
80
80
  <<-XML
81
- <partner_merchant>
82
- <partner_merchant_id>abc123</partner_merchant_id>
83
- </partner_merchant>
81
+ <partner-merchant>
82
+ <partner-merchant-id>abc123</partner-merchant-id>
83
+ </partner-merchant>
84
84
  XML
85
85
  end
86
86
 
87
87
  def _partner_merchant_declined_sample_xml(data)
88
88
 
89
89
  <<-XML
90
- <partner_merchant>
91
- <partner_merchant_id>abc123</partner_merchant_id>
92
- </partner_merchant>
90
+ <partner-merchant>
91
+ <partner-merchant-id>abc123</partner-merchant-id>
92
+ </partner-merchant>
93
93
  XML
94
94
  end
95
95
 
@@ -1 +1 @@
1
- 15069
1
+ 5195
@@ -0,0 +1,143 @@
1
+ require File.expand_path(File.dirname(__FILE__) + "/../../spec_helper")
2
+ require File.expand_path(File.dirname(__FILE__) + "/spec_helper")
3
+
4
+ describe Braintree::ClientToken do
5
+
6
+ describe "self.generate" do
7
+ it "generates a fingerprint that the gateway accepts" do
8
+ config = Braintree::Configuration.instantiate
9
+ client_token = Braintree::ClientToken.generate
10
+ http = ClientApiHttp.new(
11
+ config,
12
+ :authorization_fingerprint => JSON.parse(client_token)["authorizationFingerprint"],
13
+ :shared_customer_identifier => "fake_identifier",
14
+ :shared_customer_identifier_type => "testing"
15
+ )
16
+
17
+ response = http.get_cards
18
+
19
+ response.code.should == "200"
20
+ end
21
+
22
+ it "raises ArgumentError on invalid parameters (422)" do
23
+ expect do
24
+ Braintree::ClientToken.generate(:options => {:make_default => true})
25
+ end.to raise_error(ArgumentError)
26
+ end
27
+
28
+ it "can pass verify_card" do
29
+ config = Braintree::Configuration.instantiate
30
+ result = Braintree::Customer.create
31
+ client_token = Braintree::ClientToken.generate(
32
+ :customer_id => result.customer.id,
33
+ :options => {
34
+ :verify_card => true
35
+ }
36
+ )
37
+
38
+ http = ClientApiHttp.new(
39
+ config,
40
+ :authorization_fingerprint => JSON.parse(client_token)["authorizationFingerprint"],
41
+ :shared_customer_identifier => "fake_identifier",
42
+ :shared_customer_identifier_type => "testing"
43
+ )
44
+
45
+ response = http.add_card(
46
+ :credit_card => {
47
+ :number => "4000111111111115",
48
+ :expiration_month => "11",
49
+ :expiration_year => "2099"
50
+ }
51
+ )
52
+
53
+ response.code.should == "422"
54
+ end
55
+
56
+ it "can pass make_default" do
57
+ config = Braintree::Configuration.instantiate
58
+ result = Braintree::Customer.create
59
+ customer_id = result.customer.id
60
+ client_token = Braintree::ClientToken.generate(
61
+ :customer_id => customer_id,
62
+ :options => {
63
+ :make_default => true
64
+ }
65
+ )
66
+
67
+ http = ClientApiHttp.new(
68
+ config,
69
+ :authorization_fingerprint => JSON.parse(client_token)["authorizationFingerprint"],
70
+ :shared_customer_identifier => "fake_identifier",
71
+ :shared_customer_identifier_type => "testing"
72
+ )
73
+
74
+ response = http.add_card(
75
+ :credit_card => {
76
+ :number => "4111111111111111",
77
+ :expiration_month => "11",
78
+ :expiration_year => "2099"
79
+ }
80
+ )
81
+
82
+ response.code.should == "201"
83
+
84
+ response = http.add_card(
85
+ :credit_card => {
86
+ :number => "4005519200000004",
87
+ :expiration_month => "11",
88
+ :expiration_year => "2099"
89
+ }
90
+ )
91
+
92
+ response.code.should == "201"
93
+
94
+ customer = Braintree::Customer.find(customer_id)
95
+ customer.credit_cards.select { |c| c.bin == "400551" }[0].should be_default
96
+ end
97
+
98
+ it "can pass fail_on_duplicate_payment_method" do
99
+ config = Braintree::Configuration.instantiate
100
+ result = Braintree::Customer.create
101
+ customer_id = result.customer.id
102
+ client_token = Braintree::ClientToken.generate(
103
+ :customer_id => customer_id
104
+ )
105
+
106
+ http = ClientApiHttp.new(
107
+ config,
108
+ :authorization_fingerprint => JSON.parse(client_token)["authorizationFingerprint"],
109
+ :shared_customer_identifier => "fake_identifier",
110
+ :shared_customer_identifier_type => "testing"
111
+ )
112
+
113
+ response = http.add_card(
114
+ :credit_card => {
115
+ :number => "4111111111111111",
116
+ :expiration_month => "11",
117
+ :expiration_year => "2099"
118
+ }
119
+ )
120
+
121
+ response.code.should == "201"
122
+
123
+ client_token = Braintree::ClientToken.generate(
124
+ :customer_id => customer_id,
125
+ :options => {
126
+ :fail_on_duplicate_payment_method => true
127
+ }
128
+ )
129
+
130
+ http.fingerprint = JSON.parse(client_token)["authorizationFingerprint"]
131
+
132
+ response = http.add_card(
133
+ :credit_card => {
134
+ :number => "4111111111111111",
135
+ :expiration_month => "11",
136
+ :expiration_year => "2099"
137
+ }
138
+ )
139
+
140
+ response.code.should == "422"
141
+ end
142
+ end
143
+ end