stripe 2.7.0 → 2.8.0
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.
- 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
|