braintree 2.30.0 → 2.30.2

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