stripe 2.7.0 → 2.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/History.txt +4 -0
- data/README.md +13 -0
- data/VERSION +1 -1
- data/lib/stripe.rb +1 -0
- data/lib/stripe/errors.rb +11 -0
- data/lib/stripe/util.rb +12 -0
- data/lib/stripe/version.rb +1 -1
- data/lib/stripe/webhook.rb +79 -0
- data/test/stripe/webhook_test.rb +92 -0
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 747667dd050969e755253ea2c5328142ebff0dd2
|
4
|
+
data.tar.gz: f60cfc0497a8b0044811283e893a667755f3f6cd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b405457d2c0615be9e9fdf1ea20229b95365fb0a615d2f1b508fe0b4466c8842e7a7469317208e477280df0eb51d280e1e82928d5465de54fdca7e786df44faa
|
7
|
+
data.tar.gz: 8718ad60ec44fd3af67bfb5b8567c56a35141ce14937f6a88ed7d455084ed491cb93a3d2fb45c31d2297d0324667ba8d43517aea65b87c72aea7c28240ed9863
|
data/History.txt
CHANGED
data/README.md
CHANGED
@@ -122,6 +122,19 @@ an intermittent network problem:
|
|
122
122
|
[Idempotency keys][idempotency-keys] are added to requests to guarantee that
|
123
123
|
retries are safe.
|
124
124
|
|
125
|
+
### Configuring Timeouts
|
126
|
+
|
127
|
+
Open and read timeouts are configurable:
|
128
|
+
|
129
|
+
```java
|
130
|
+
Stripe.open_timeout = 30 // in seconds
|
131
|
+
Stripe.read_timeout = 80
|
132
|
+
```
|
133
|
+
|
134
|
+
Please take care to set conservative read timeouts. Some API requests can take
|
135
|
+
some time, and a short timeout increases the likelihood of a problem within our
|
136
|
+
servers.
|
137
|
+
|
125
138
|
### Writing a Plugin
|
126
139
|
|
127
140
|
If you're writing a plugin that uses the library, we'd appreciate it if you
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.
|
1
|
+
2.8.0
|
data/lib/stripe.rb
CHANGED
data/lib/stripe/errors.rb
CHANGED
@@ -89,4 +89,15 @@ module Stripe
|
|
89
89
|
# back off on request rate.
|
90
90
|
class RateLimitError < StripeError
|
91
91
|
end
|
92
|
+
|
93
|
+
# SignatureVerificationError is raised when the signature verification for a
|
94
|
+
# webhook fails
|
95
|
+
class SignatureVerificationError < StripeError
|
96
|
+
attr_accessor :sig_header
|
97
|
+
|
98
|
+
def initialize(message, sig_header, http_body: nil)
|
99
|
+
super(message, http_body: http_body)
|
100
|
+
@sig_header = sig_header
|
101
|
+
end
|
102
|
+
end
|
92
103
|
end
|
data/lib/stripe/util.rb
CHANGED
@@ -256,5 +256,17 @@ module Stripe
|
|
256
256
|
end
|
257
257
|
end
|
258
258
|
end
|
259
|
+
|
260
|
+
# Constant time string comparison to prevent timing attacks
|
261
|
+
# Code borrowed from ActiveSupport
|
262
|
+
def self.secure_compare(a, b)
|
263
|
+
return false unless a.bytesize == b.bytesize
|
264
|
+
|
265
|
+
l = a.unpack "C#{a.bytesize}"
|
266
|
+
|
267
|
+
res = 0
|
268
|
+
b.each_byte { |byte| res |= byte ^ l.shift }
|
269
|
+
res == 0
|
270
|
+
end
|
259
271
|
end
|
260
272
|
end
|
data/lib/stripe/version.rb
CHANGED
@@ -0,0 +1,79 @@
|
|
1
|
+
module Stripe
|
2
|
+
module Webhook
|
3
|
+
DEFAULT_TOLERANCE = 300
|
4
|
+
|
5
|
+
# Initializes an Event object from a JSON payload.
|
6
|
+
#
|
7
|
+
# This may raise JSON::ParserError if the payload is not valid JSON, or
|
8
|
+
# SignatureVerificationError if the signature verification fails.
|
9
|
+
def self.construct_event(payload, sig_header, secret, tolerance: DEFAULT_TOLERANCE)
|
10
|
+
data = JSON.parse(payload, symbolize_names: true)
|
11
|
+
event = Event.construct_from(data)
|
12
|
+
|
13
|
+
Signature.verify_header(payload, sig_header, secret, tolerance: tolerance)
|
14
|
+
|
15
|
+
event
|
16
|
+
end
|
17
|
+
|
18
|
+
module Signature
|
19
|
+
EXPECTED_SCHEME = 'v1'
|
20
|
+
|
21
|
+
def self.compute_signature(payload, secret)
|
22
|
+
OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), secret, payload)
|
23
|
+
end
|
24
|
+
private_class_method :compute_signature
|
25
|
+
|
26
|
+
# Extracts the timestamp and the signature(s) with the desired scheme
|
27
|
+
# from the header
|
28
|
+
def self.get_timestamp_and_signatures(header, scheme)
|
29
|
+
list_items = header.split(/,\s*/).map { |i| i.split('=', 2) }
|
30
|
+
timestamp = Integer(list_items.select { |i| i[0] == 't' }[0][1])
|
31
|
+
signatures = list_items.select { |i| i[0] == scheme }.map { |i| i[1] }
|
32
|
+
[timestamp, signatures]
|
33
|
+
end
|
34
|
+
private_class_method :get_timestamp_and_signatures
|
35
|
+
|
36
|
+
# Verifies the signature header for a given payload.
|
37
|
+
#
|
38
|
+
# Raises a SignatureVerificationError in the following cases:
|
39
|
+
# - the header does not match the expected format
|
40
|
+
# - no signatures found with the expected scheme
|
41
|
+
# - no signatures matching the expected signature
|
42
|
+
# - a tolerance is provided and the timestamp is not within the
|
43
|
+
# tolerance
|
44
|
+
#
|
45
|
+
# Returns true otherwise
|
46
|
+
def self.verify_header(payload, header, secret, tolerance: nil)
|
47
|
+
begin
|
48
|
+
timestamp, signatures = get_timestamp_and_signatures(header, EXPECTED_SCHEME)
|
49
|
+
rescue
|
50
|
+
raise SignatureVerificationError.new(
|
51
|
+
"Unable to extract timestamp and signatures from header",
|
52
|
+
header, http_body: payload)
|
53
|
+
end
|
54
|
+
|
55
|
+
if signatures.empty?
|
56
|
+
raise SignatureVerificationError.new(
|
57
|
+
"No signatures found with expected scheme #{EXPECTED_SCHEME}",
|
58
|
+
header, http_body: payload)
|
59
|
+
end
|
60
|
+
|
61
|
+
signed_payload = "#{timestamp}.#{payload}"
|
62
|
+
expected_sig = compute_signature(signed_payload, secret)
|
63
|
+
unless signatures.any? {|s| Util.secure_compare(expected_sig, s)}
|
64
|
+
raise SignatureVerificationError.new(
|
65
|
+
"No signatures found matching the expected signature for payload",
|
66
|
+
header, http_body: payload)
|
67
|
+
end
|
68
|
+
|
69
|
+
if tolerance && timestamp < Time.now.to_f - tolerance
|
70
|
+
raise SignatureVerificationError.new(
|
71
|
+
"Timestamp outside the tolerance zone (#{Time.at(timestamp)})",
|
72
|
+
header, http_body: payload)
|
73
|
+
end
|
74
|
+
|
75
|
+
true
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require File.expand_path('../../test_helper', __FILE__)
|
2
|
+
|
3
|
+
module Stripe
|
4
|
+
class WebhookTest < Test::Unit::TestCase
|
5
|
+
EVENT_PAYLOAD = '''{
|
6
|
+
"id": "evt_test_webhook",
|
7
|
+
"object": "event"
|
8
|
+
}'''
|
9
|
+
SECRET = 'whsec_test_secret'
|
10
|
+
|
11
|
+
def generate_header(opts={})
|
12
|
+
opts[:timestamp] ||= Time.now.to_i
|
13
|
+
opts[:payload] ||= EVENT_PAYLOAD
|
14
|
+
opts[:secret] ||= SECRET
|
15
|
+
opts[:scheme] ||= Stripe::Webhook::Signature::EXPECTED_SCHEME
|
16
|
+
opts[:signature] ||= Stripe::Webhook::Signature.send(:compute_signature, "#{opts[:timestamp]}.#{opts[:payload]}", opts[:secret])
|
17
|
+
"t=#{opts[:timestamp]},#{opts[:scheme]}=#{opts[:signature]}"
|
18
|
+
end
|
19
|
+
|
20
|
+
context ".construct_event" do
|
21
|
+
should "return an Event instance from a valid JSON payload and valid signature header" do
|
22
|
+
header = generate_header
|
23
|
+
event = Stripe::Webhook.construct_event(EVENT_PAYLOAD, header, SECRET)
|
24
|
+
assert event.kind_of?(Stripe::Event)
|
25
|
+
end
|
26
|
+
|
27
|
+
should "raise a JSON::ParserError from an invalid JSON payload" do
|
28
|
+
assert_raises JSON::ParserError do
|
29
|
+
payload = 'this is not valid JSON'
|
30
|
+
header = generate_header(payload: payload)
|
31
|
+
Stripe::Webhook.construct_event(payload, header, SECRET)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
should "raise a SignatureVerificationError from a valid JSON payload and an invalid signature header" do
|
36
|
+
header = 'bad_header'
|
37
|
+
assert_raises Stripe::SignatureVerificationError do
|
38
|
+
Stripe::Webhook.construct_event(EVENT_PAYLOAD, header, SECRET)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context ".verify_signature_header" do
|
44
|
+
should "raise a SignatureVerificationError when the header does not have the expected format" do
|
45
|
+
header = 'i\'m not even a real signature header'
|
46
|
+
e = assert_raises(Stripe::SignatureVerificationError) do
|
47
|
+
Stripe::Webhook::Signature.verify_header(EVENT_PAYLOAD, header, 'secret')
|
48
|
+
end
|
49
|
+
assert_match("Unable to extract timestamp and signatures from header", e.message)
|
50
|
+
end
|
51
|
+
|
52
|
+
should "raise a SignatureVerificationError when there are no signatures with the expected scheme" do
|
53
|
+
header = generate_header(scheme: 'v0')
|
54
|
+
e = assert_raises(Stripe::SignatureVerificationError) do
|
55
|
+
Stripe::Webhook::Signature.verify_header(EVENT_PAYLOAD, header, 'secret')
|
56
|
+
end
|
57
|
+
assert_match("No signatures found with expected scheme", e.message)
|
58
|
+
end
|
59
|
+
|
60
|
+
should "raise a SignatureVerificationError when there are no valid signatures for the payload" do
|
61
|
+
header = generate_header(signature: 'bad_signature')
|
62
|
+
e = assert_raises(Stripe::SignatureVerificationError) do
|
63
|
+
Stripe::Webhook::Signature.verify_header(EVENT_PAYLOAD, header, 'secret')
|
64
|
+
end
|
65
|
+
assert_match("No signatures found matching the expected signature for payload", e.message)
|
66
|
+
end
|
67
|
+
|
68
|
+
should "raise a SignatureVerificationError when the timestamp is not within the tolerance" do
|
69
|
+
header = generate_header(timestamp: Time.now.to_i - 15)
|
70
|
+
e = assert_raises(Stripe::SignatureVerificationError) do
|
71
|
+
Stripe::Webhook::Signature.verify_header(EVENT_PAYLOAD, header, SECRET, tolerance: 10)
|
72
|
+
end
|
73
|
+
assert_match("Timestamp outside the tolerance zone", e.message)
|
74
|
+
end
|
75
|
+
|
76
|
+
should "return true when the header contains a valid signature and the timestamp is within the tolerance" do
|
77
|
+
header = generate_header
|
78
|
+
assert(Stripe::Webhook::Signature.verify_header(EVENT_PAYLOAD, header, SECRET, tolerance: 10))
|
79
|
+
end
|
80
|
+
|
81
|
+
should "return true when the header contains at least one valid signature" do
|
82
|
+
header = generate_header + ",v1=bad_signature"
|
83
|
+
assert(Stripe::Webhook::Signature.verify_header(EVENT_PAYLOAD, header, SECRET, tolerance: 10))
|
84
|
+
end
|
85
|
+
|
86
|
+
should "return true when the header contains a valid signature and the timestamp is off but no tolerance is provided" do
|
87
|
+
header = generate_header(timestamp: 12345)
|
88
|
+
assert(Stripe::Webhook::Signature.verify_header(EVENT_PAYLOAD, header, SECRET))
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: stripe
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stripe
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-04-
|
11
|
+
date: 2017-04-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: faraday
|
@@ -97,6 +97,7 @@ files:
|
|
97
97
|
- lib/stripe/transfer.rb
|
98
98
|
- lib/stripe/util.rb
|
99
99
|
- lib/stripe/version.rb
|
100
|
+
- lib/stripe/webhook.rb
|
100
101
|
- openapi/fixtures.json
|
101
102
|
- openapi/fixtures.yaml
|
102
103
|
- openapi/spec2.json
|
@@ -146,6 +147,7 @@ files:
|
|
146
147
|
- test/stripe/three_d_secure_test.rb
|
147
148
|
- test/stripe/transfer_test.rb
|
148
149
|
- test/stripe/util_test.rb
|
150
|
+
- test/stripe/webhook_test.rb
|
149
151
|
- test/stripe_test.rb
|
150
152
|
- test/test_data.rb
|
151
153
|
- test/test_helper.rb
|
@@ -218,6 +220,7 @@ test_files:
|
|
218
220
|
- test/stripe/three_d_secure_test.rb
|
219
221
|
- test/stripe/transfer_test.rb
|
220
222
|
- test/stripe/util_test.rb
|
223
|
+
- test/stripe/webhook_test.rb
|
221
224
|
- test/stripe_test.rb
|
222
225
|
- test/test_data.rb
|
223
226
|
- test/test_helper.rb
|