bunq-client 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.ruby-version +2 -1
- data/.travis.yml +2 -2
- data/README.md +11 -0
- data/bunq-client.gemspec +8 -6
- data/lib/bunq/attachment_public_content.rb +19 -0
- data/lib/bunq/card.rb +15 -0
- data/lib/bunq/cards.rb +11 -0
- data/lib/bunq/client.rb +91 -13
- data/lib/bunq/device_servers.rb +12 -2
- data/lib/bunq/encryptor.rb +53 -0
- data/lib/bunq/errors.rb +33 -0
- data/lib/bunq/installations.rb +1 -1
- data/lib/bunq/monetary_account.rb +4 -0
- data/lib/bunq/paginated.rb +13 -3
- data/lib/bunq/payment.rb +13 -0
- data/lib/bunq/resource.rb +56 -39
- data/lib/bunq/session_servers.rb +1 -1
- data/lib/bunq/signature.rb +13 -4
- data/lib/bunq/user.rb +10 -0
- data/lib/bunq/user_person.rb +17 -0
- data/lib/bunq/version.rb +1 -1
- metadata +35 -13
- data/lib/bunq/unexpected_response.rb +0 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f43869cbdc6e1d596f972aac0752ca7ed25fe6884814ea592a7e34e97fae6948
|
4
|
+
data.tar.gz: 38a49c91d8e233921b8d21b1e5852e01acfff630157abe0dd3bfa4f715f4a10f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a853e3ae7694c1bfb5a41aa735c4942ff015c8614602f6efd45d9a9aa08300ee1e89da4ad2ff3f354d82ef4ba519f10229badd1338dd9730d61ec8d8b2cb475d
|
7
|
+
data.tar.gz: ac8ee43d4bb61f30ce45937b7b90af82087cccd0d248396d27decdcbcdfae6584f4bf40f5c434135cc9ac7d248ebafdd9f44039557d59a433b8124ab1a06e4d2
|
data/.ruby-version
CHANGED
@@ -1 +1,2 @@
|
|
1
|
-
2.3
|
1
|
+
2.5.3
|
2
|
+
|
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -137,6 +137,17 @@ Bunq.client.me_as_user.monetary_accounts.index.each do |monetary_account|
|
|
137
137
|
end
|
138
138
|
```
|
139
139
|
|
140
|
+
## Session caching
|
141
|
+
|
142
|
+
By default, each `Bunq.client` creates a new session. If you want to share a session between multiple
|
143
|
+
`Bunq.client`s, use the following configuration:
|
144
|
+
|
145
|
+
```
|
146
|
+
Bunq.configure do |config|
|
147
|
+
config.session_cache = Bunq::ThreadSafeSessionCache.new
|
148
|
+
end
|
149
|
+
```
|
150
|
+
|
140
151
|
## Development
|
141
152
|
|
142
153
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/bunq-client.gemspec
CHANGED
@@ -10,11 +10,13 @@ Gem::Specification.new do |spec|
|
|
10
10
|
'Lars Vonk',
|
11
11
|
'Bob Forma',
|
12
12
|
'Derek Kraan',
|
13
|
+
'Mike van Diepen',
|
13
14
|
]
|
14
15
|
spec.email = [
|
15
16
|
'lars.vonk@gmail.com',
|
16
17
|
'bforma@zilverline.com',
|
17
|
-
'
|
18
|
+
'derek.kraan@gmail.com',
|
19
|
+
'mvdiepen@zilverline.com',
|
18
20
|
]
|
19
21
|
|
20
22
|
spec.summary = %q{Ruby client for the bunq public API.}
|
@@ -22,7 +24,6 @@ Gem::Specification.new do |spec|
|
|
22
24
|
spec.homepage = "https://github.com/jorttbv/bunq-client"
|
23
25
|
spec.license = "MIT"
|
24
26
|
|
25
|
-
|
26
27
|
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
27
28
|
f.match(%r{^(test|spec|features)/})
|
28
29
|
end
|
@@ -31,11 +32,12 @@ Gem::Specification.new do |spec|
|
|
31
32
|
spec.require_paths = ["lib"]
|
32
33
|
|
33
34
|
spec.add_runtime_dependency 'rest-client', '~> 2.0'
|
35
|
+
spec.add_runtime_dependency 'thread_safe', '~> 0.3.6'
|
34
36
|
|
35
|
-
spec.add_development_dependency "bundler", "~> 1.
|
36
|
-
spec.add_development_dependency "rake", "~>
|
37
|
-
spec.add_development_dependency "rspec", "~> 3.
|
38
|
-
spec.add_development_dependency "webmock", "~>
|
37
|
+
spec.add_development_dependency "bundler", "~> 1.16"
|
38
|
+
spec.add_development_dependency "rake", "~> 12.3"
|
39
|
+
spec.add_development_dependency "rspec", "~> 3.7"
|
40
|
+
spec.add_development_dependency "webmock", "~> 3.2.1"
|
39
41
|
spec.add_development_dependency "rspec-json_expectations", "~> 2.1"
|
40
42
|
spec.add_development_dependency "codecov", "~> 0.1.10"
|
41
43
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Bunq
|
2
|
+
##
|
3
|
+
# https://doc.bunq.com/api/1/call/attachment-public-content
|
4
|
+
class AttachmentPublicContent
|
5
|
+
def initialize(client, id)
|
6
|
+
@resource = Bunq::Resource.new(client, "/v1/attachment-public/#{id}/content")
|
7
|
+
end
|
8
|
+
|
9
|
+
##
|
10
|
+
# https://doc.bunq.com/api/1/call/attachment-public-content/method/list
|
11
|
+
# Returns the raw content of a public attachment with given ID.
|
12
|
+
# The raw content is the binary representation of a file.
|
13
|
+
def show
|
14
|
+
@resource.with_session do
|
15
|
+
@resource.get { |response| response.body }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/bunq/card.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
module Bunq
|
2
|
+
class Card
|
3
|
+
def initialize(parent_resource, id)
|
4
|
+
@resource = parent_resource.append("/card/#{id}")
|
5
|
+
end
|
6
|
+
|
7
|
+
def show
|
8
|
+
@resource.with_session { @resource.get }['Response']
|
9
|
+
end
|
10
|
+
|
11
|
+
def update(card)
|
12
|
+
@resource.with_session { @resource.put(card, encrypt: true) }['Response']
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/bunq/cards.rb
ADDED
data/lib/bunq/client.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'openssl'
|
2
2
|
require 'base64'
|
3
|
+
require 'thread_safe'
|
3
4
|
|
4
5
|
require_relative './version'
|
5
6
|
require_relative './resource'
|
@@ -7,20 +8,24 @@ require_relative './resource'
|
|
7
8
|
require_relative './installations'
|
8
9
|
require_relative './installation'
|
9
10
|
require_relative './device_servers'
|
11
|
+
require_relative './encryptor'
|
10
12
|
require_relative './session_servers'
|
11
13
|
require_relative './user'
|
12
14
|
require_relative './user_company'
|
15
|
+
require_relative './user_person'
|
13
16
|
require_relative './monetary_account'
|
14
17
|
require_relative './monetary_accounts'
|
18
|
+
require_relative './payment'
|
15
19
|
require_relative './payments'
|
16
20
|
require_relative './signature'
|
21
|
+
require_relative './attachment_public_content.rb'
|
17
22
|
|
18
23
|
##
|
19
24
|
# Usage
|
20
25
|
#
|
21
26
|
# Bunq.configure do |config|
|
22
27
|
# config.api_key = 'YOUR_APIKEY'
|
23
|
-
# config.installation_token = 'YOUR_INSTALLATION_TOKEN'
|
28
|
+
# config.installation_token = 'YOUR_INSTALLATION_TOKEN'
|
24
29
|
# config.private_key = 'YOUR PRIVATE KEY'
|
25
30
|
# config.server_public_key = 'SERVER PUBLIC KEY'
|
26
31
|
# end
|
@@ -38,8 +43,10 @@ module Bunq
|
|
38
43
|
yield(configuration)
|
39
44
|
|
40
45
|
configuration.base_url = Configuration::SANDBOX_BASE_URL if configuration.sandbox
|
46
|
+
end
|
41
47
|
|
42
|
-
|
48
|
+
def reset_configuration
|
49
|
+
self.configuration = nil
|
43
50
|
end
|
44
51
|
|
45
52
|
##
|
@@ -49,13 +56,43 @@ module Bunq
|
|
49
56
|
fail "No configuration! Call Bunq.configure first." unless configuration
|
50
57
|
Client.new(configuration)
|
51
58
|
end
|
59
|
+
end
|
52
60
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
61
|
+
class NoSessionCache
|
62
|
+
def get(&block)
|
63
|
+
block.call if block_given?
|
64
|
+
end
|
65
|
+
|
66
|
+
def clear
|
67
|
+
# no-op
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
##
|
72
|
+
# A thread-safe session cache that can hold one (the current) session.
|
73
|
+
#
|
74
|
+
# Usage:
|
75
|
+
#
|
76
|
+
# Bunq.configure do |config|
|
77
|
+
# config.session_cache = Bunq::ThreadSafeSessionCache.new
|
78
|
+
# end
|
79
|
+
#
|
80
|
+
# After this, all +Bunq.client+ calls will use the same session. When the session times out,
|
81
|
+
# a new one is started automatically.
|
82
|
+
#
|
83
|
+
class ThreadSafeSessionCache
|
84
|
+
CACHE_KEY = 'CURRENT_BUNQ_SESSION'
|
85
|
+
|
86
|
+
def initialize
|
87
|
+
clear
|
88
|
+
end
|
89
|
+
|
90
|
+
def get(&block)
|
91
|
+
@cache.fetch_or_store(CACHE_KEY) { block.call if block_given? }
|
92
|
+
end
|
93
|
+
|
94
|
+
def clear
|
95
|
+
@cache = ThreadSafe::Cache.new
|
59
96
|
end
|
60
97
|
end
|
61
98
|
|
@@ -63,13 +100,15 @@ module Bunq
|
|
63
100
|
# Configuration object for connecting to the bunq api
|
64
101
|
#
|
65
102
|
class Configuration
|
66
|
-
SANDBOX_BASE_URL = 'https://
|
103
|
+
SANDBOX_BASE_URL = 'https://public-api.sandbox.bunq.com'
|
67
104
|
PRODUCTION_BASE_URL = 'https://api.bunq.com'
|
68
105
|
|
69
106
|
DEFAULT_LANGUAGE = 'nl_NL'
|
70
107
|
DEFAULT_REGION = 'nl_NL'
|
71
108
|
DEFAULT_GEOLOCATION = '0 0 0 0 000'
|
72
109
|
DEFAULT_USER_AGENT = "bunq ruby client #{Bunq::VERSION}"
|
110
|
+
DEFAULT_TIMEOUT = 60
|
111
|
+
DEFAULT_SESSION_CACHE = NoSessionCache.new
|
73
112
|
|
74
113
|
# Base url for the bunq api. Defaults to +PRODUCTION_BASE_URL+
|
75
114
|
attr_accessor :base_url,
|
@@ -100,8 +139,13 @@ module Bunq
|
|
100
139
|
# The private key for signing the request
|
101
140
|
:private_key,
|
102
141
|
# The public key of this installation for verifying the response
|
103
|
-
:server_public_key
|
104
|
-
|
142
|
+
:server_public_key,
|
143
|
+
# Timeout in seconds to wait for bunq api. Defaults to +DEFAULT_TIMEOUT+
|
144
|
+
:timeout,
|
145
|
+
# Cache to retrieve current session from. Defaults to +DEFAULT_SESSION_CACHE+,
|
146
|
+
# which will create a new session per `Bunq.client` instance.
|
147
|
+
# See +ThreadSafeSessionCache+ for more advanced use.
|
148
|
+
:session_cache
|
105
149
|
|
106
150
|
def initialize
|
107
151
|
@sandbox = false
|
@@ -111,6 +155,8 @@ module Bunq
|
|
111
155
|
@geolocation = DEFAULT_GEOLOCATION
|
112
156
|
@user_agent = DEFAULT_USER_AGENT
|
113
157
|
@disable_response_signature_verification = false
|
158
|
+
@timeout = DEFAULT_TIMEOUT
|
159
|
+
@session_cache = DEFAULT_SESSION_CACHE
|
114
160
|
end
|
115
161
|
end
|
116
162
|
|
@@ -119,11 +165,11 @@ module Bunq
|
|
119
165
|
#
|
120
166
|
# An instance of a +Client+ can be obtained via +Bunq.client+
|
121
167
|
class Client
|
122
|
-
|
123
168
|
attr_accessor :current_session
|
124
169
|
attr_reader :configuration
|
125
170
|
|
126
171
|
def initialize(configuration)
|
172
|
+
fail ArgumentError.new('configuration is required') unless configuration
|
127
173
|
@configuration = configuration
|
128
174
|
end
|
129
175
|
|
@@ -151,6 +197,20 @@ module Bunq
|
|
151
197
|
Bunq::UserCompany.new(self, id)
|
152
198
|
end
|
153
199
|
|
200
|
+
def user_person(id)
|
201
|
+
Bunq::UserPerson.new(self, id)
|
202
|
+
end
|
203
|
+
|
204
|
+
# Returns the +Bunq::AttachmentPublicContent+ represented by the given id
|
205
|
+
def attachment_public_content(id)
|
206
|
+
with_session { Bunq::AttachmentPublicContent.new(self, id) }
|
207
|
+
end
|
208
|
+
|
209
|
+
# Returns the +Bunq::UserPerson+ represented by the +Bunq::Configuration.api_key+
|
210
|
+
def me_as_user_person
|
211
|
+
with_session { user_person(current_session_user_id) }
|
212
|
+
end
|
213
|
+
|
154
214
|
# Returns the +Bunq::UserCompany+ represented by the +Bunq::Configuration.api_key+
|
155
215
|
def me_as_user_company
|
156
216
|
with_session { user_company(current_session_user_id) }
|
@@ -162,12 +222,30 @@ module Bunq
|
|
162
222
|
end
|
163
223
|
|
164
224
|
def ensure_session!
|
165
|
-
@current_session ||=
|
225
|
+
@current_session ||= configuration.session_cache.get { create_session }
|
226
|
+
end
|
227
|
+
|
228
|
+
def create_session
|
229
|
+
session_servers.create
|
166
230
|
end
|
167
231
|
|
168
232
|
def with_session(&block)
|
233
|
+
retries ||= 0
|
169
234
|
ensure_session!
|
170
235
|
block.call
|
236
|
+
rescue UnauthorisedResponse => e
|
237
|
+
configuration.session_cache.clear
|
238
|
+
@current_session = nil
|
239
|
+
retry if (retries += 1) < 2
|
240
|
+
raise e
|
241
|
+
end
|
242
|
+
|
243
|
+
def signature
|
244
|
+
@signature ||= Signature.new(configuration.private_key, configuration.server_public_key)
|
245
|
+
end
|
246
|
+
|
247
|
+
def encryptor
|
248
|
+
@encryptor ||= Encryptor.new(configuration.server_public_key)
|
171
249
|
end
|
172
250
|
|
173
251
|
def headers
|
data/lib/bunq/device_servers.rb
CHANGED
@@ -13,10 +13,20 @@ module Bunq
|
|
13
13
|
|
14
14
|
##
|
15
15
|
# https://doc.bunq.com/api/1/call/device-server/method/post
|
16
|
-
|
16
|
+
#
|
17
|
+
# You can add a wildcard IP by passing an array of the current IP,
|
18
|
+
# and the `*` character. E.g.: ['1.2.3.4', '*'].
|
19
|
+
#
|
20
|
+
# @param description [String] The description of this device server.
|
21
|
+
# @param permitted_ips [Array|nil] Array of permitted IP addresses.
|
22
|
+
def create(description, permitted_ips: nil)
|
17
23
|
fail ArgumentError.new('description is required') unless description
|
24
|
+
fail 'Cannot create session, please add the api_key to your configuration' unless @client.configuration.api_key
|
25
|
+
|
26
|
+
params = { description: description, secret: @client.configuration.api_key }
|
27
|
+
params[:permitted_ips] = permitted_ips if permitted_ips
|
18
28
|
|
19
|
-
@resource.post(
|
29
|
+
@resource.post(params)['Response']
|
20
30
|
end
|
21
31
|
|
22
32
|
##
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Bunq
|
2
|
+
class Encryptor
|
3
|
+
HEADER_CLIENT_ENCRYPTION_HMAC = 'X-Bunq-Client-Encryption-Hmac'
|
4
|
+
HEADER_CLIENT_ENCRYPTION_IV = 'X-Bunq-Client-Encryption-Iv'
|
5
|
+
HEADER_CLIENT_ENCRYPTION_KEY = 'X-Bunq-Client-Encryption-Key'
|
6
|
+
AES_ENCRYPTION_METHOD = 'aes-256-cbc'
|
7
|
+
HMAC_ALGORITHM = 'sha1'
|
8
|
+
|
9
|
+
def initialize(server_public_key)
|
10
|
+
fail ArgumentError.new('server_public_key is mandatory') unless server_public_key
|
11
|
+
|
12
|
+
@server_public_key = OpenSSL::PKey::RSA.new(server_public_key)
|
13
|
+
end
|
14
|
+
|
15
|
+
def encrypt(body)
|
16
|
+
headers = {}
|
17
|
+
|
18
|
+
iv, key, encrypted_body = encrypt_body(body)
|
19
|
+
|
20
|
+
headers[HEADER_CLIENT_ENCRYPTION_IV] = Base64.strict_encode64(iv)
|
21
|
+
|
22
|
+
encrypted_key = server_public_key.public_encrypt(key)
|
23
|
+
headers[HEADER_CLIENT_ENCRYPTION_KEY] = Base64.strict_encode64(encrypted_key)
|
24
|
+
|
25
|
+
digest = hmac(key, iv + encrypted_body)
|
26
|
+
headers[HEADER_CLIENT_ENCRYPTION_HMAC] = Base64.strict_encode64(digest)
|
27
|
+
|
28
|
+
[encrypted_body, headers]
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
attr_reader :server_public_key
|
34
|
+
|
35
|
+
def encrypt_body(body)
|
36
|
+
cipher = OpenSSL::Cipher.new(AES_ENCRYPTION_METHOD)
|
37
|
+
cipher.encrypt
|
38
|
+
|
39
|
+
iv = cipher.random_iv
|
40
|
+
key = cipher.random_key
|
41
|
+
|
42
|
+
encrypted_body = cipher.update(body) + cipher.final
|
43
|
+
|
44
|
+
[iv, key, encrypted_body]
|
45
|
+
end
|
46
|
+
|
47
|
+
def hmac(key, content)
|
48
|
+
hmac = OpenSSL::HMAC.new(key, OpenSSL::Digest.new(HMAC_ALGORITHM))
|
49
|
+
hmac << content
|
50
|
+
hmac.digest
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/lib/bunq/errors.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
module Bunq
|
2
|
+
class ResponseError < StandardError
|
3
|
+
attr_reader :code
|
4
|
+
attr_reader :headers
|
5
|
+
attr_reader :body
|
6
|
+
|
7
|
+
def initialize(msg = "Response error", code: nil, headers: nil, body: nil)
|
8
|
+
@code = code
|
9
|
+
@headers = headers || {}
|
10
|
+
@body = body
|
11
|
+
super("#{msg}: #{body}")
|
12
|
+
end
|
13
|
+
|
14
|
+
# Returns the parsed body if it is a JSON document, nil otherwise.
|
15
|
+
# @param opts [Hash] Optional options that are passed to `JSON.parse`.
|
16
|
+
def parsed_body(opts = {})
|
17
|
+
JSON.parse(@body, opts) if @body && @headers['content-type'] && @headers['content-type'].include?('application/json')
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns an array of errors returned from the API, or nil if no errors are returned.
|
21
|
+
# @return [Array|nil]
|
22
|
+
def errors
|
23
|
+
json = parsed_body
|
24
|
+
json ? json['Error'] : nil
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class UnexpectedResponse < ResponseError; end
|
29
|
+
class AbsentResponseSignature < ResponseError; end
|
30
|
+
class TooManyRequestsResponse < ResponseError; end
|
31
|
+
class UnauthorisedResponse < ResponseError; end
|
32
|
+
class Timeout < StandardError; end
|
33
|
+
end
|
data/lib/bunq/installations.rb
CHANGED
@@ -7,7 +7,7 @@ module Bunq
|
|
7
7
|
def create(public_key)
|
8
8
|
fail ArgumentError.new('public_key is required') unless public_key
|
9
9
|
|
10
|
-
@resource.post({client_public_key: public_key}, true)['Response']
|
10
|
+
@resource.post({client_public_key: public_key}, skip_verify: true)['Response']
|
11
11
|
end
|
12
12
|
|
13
13
|
def index
|
data/lib/bunq/paginated.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require_relative '
|
1
|
+
require_relative 'errors'
|
2
2
|
|
3
3
|
module Bunq
|
4
4
|
class MissingPaginationObject < UnexpectedResponse; end
|
@@ -40,12 +40,22 @@ module Bunq
|
|
40
40
|
pagination = result['Pagination']
|
41
41
|
fail MissingPaginationObject unless pagination
|
42
42
|
|
43
|
-
last_page = !pagination[
|
44
|
-
next_params = params.merge(
|
43
|
+
last_page = !pagination[paging_url(params)]
|
44
|
+
next_params = params.merge(:"#{paging_id(params)}" => param(paging_id(params), pagination[paging_url(params)])) unless last_page
|
45
45
|
end
|
46
46
|
end
|
47
47
|
end
|
48
48
|
|
49
|
+
def paging_url(params)
|
50
|
+
return 'newer_url' if params[:newer_id]
|
51
|
+
'older_url'
|
52
|
+
end
|
53
|
+
|
54
|
+
def paging_id(params)
|
55
|
+
return 'newer_id' if params[:newer_id]
|
56
|
+
'older_id'
|
57
|
+
end
|
58
|
+
|
49
59
|
def param(name, url)
|
50
60
|
CGI.parse(URI(url).query)[name]&.first
|
51
61
|
end
|
data/lib/bunq/payment.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
module Bunq
|
2
|
+
# https://doc.bunq.com/api/1/call/payment
|
3
|
+
class Payment
|
4
|
+
def initialize(parent_resource, id)
|
5
|
+
@resource = parent_resource.append("/payment/#{id}")
|
6
|
+
end
|
7
|
+
|
8
|
+
# https://doc.bunq.com/api/1/call/payment/method/get
|
9
|
+
def show
|
10
|
+
@resource.with_session { @resource.get }['Response']
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
data/lib/bunq/resource.rb
CHANGED
@@ -1,5 +1,4 @@
|
|
1
|
-
require_relative '
|
2
|
-
require_relative 'unexpected_response'
|
1
|
+
require_relative 'errors'
|
3
2
|
require 'restclient'
|
4
3
|
require 'json'
|
5
4
|
|
@@ -11,74 +10,89 @@ module Bunq
|
|
11
10
|
def initialize(client, path)
|
12
11
|
@client = client
|
13
12
|
@path = path
|
14
|
-
@resource = RestClient::Resource.new(
|
15
|
-
"#{client.configuration.base_url}#{path}",
|
16
|
-
{
|
17
|
-
headers: client.headers
|
18
|
-
}.tap do |x|
|
19
|
-
if client.configuration.sandbox
|
20
|
-
x[:user] = client.configuration.sandbox_user
|
21
|
-
x[:password] = client.configuration.sandbox_password
|
22
|
-
end
|
23
|
-
end
|
24
|
-
)
|
25
13
|
end
|
26
14
|
|
27
15
|
def get(params = {}, &block)
|
28
|
-
|
16
|
+
resource.get({params: params}.merge(bunq_request_headers('GET', params))) do |response, request, result|
|
29
17
|
verify_and_handle_response(response, request, result, &block)
|
30
18
|
end
|
19
|
+
rescue RestClient::Exceptions::Timeout
|
20
|
+
raise Bunq::Timeout
|
31
21
|
end
|
32
22
|
|
33
23
|
|
34
|
-
def post(payload, skip_verify
|
35
|
-
|
36
|
-
if
|
37
|
-
|
24
|
+
def post(payload, skip_verify: false, encrypt: false, &block)
|
25
|
+
body = JSON.generate(payload)
|
26
|
+
body, headers = client.encryptor.encrypt(body) if encrypt
|
27
|
+
|
28
|
+
headers = bunq_request_headers('POST', NO_PARAMS, body, headers || {})
|
29
|
+
|
30
|
+
resource.post(body, headers) do |response, request, result|
|
31
|
+
if skip_verify
|
38
32
|
handle_response(response, request, result, &block)
|
39
|
-
|
40
|
-
else
|
41
|
-
@resource.post(json, bunq_request_headers('POST', NO_PARAMS, json)) do |response, request, result|
|
33
|
+
else
|
42
34
|
verify_and_handle_response(response, request, result, &block)
|
43
35
|
end
|
44
36
|
end
|
37
|
+
rescue RestClient::Exceptions::Timeout
|
38
|
+
raise Bunq::Timeout
|
45
39
|
end
|
46
40
|
|
47
|
-
def put(payload, &block)
|
48
|
-
|
49
|
-
|
41
|
+
def put(payload, encrypt: false, &block)
|
42
|
+
body = JSON.generate(payload)
|
43
|
+
body, headers = client.encryptor.encrypt(body) if encrypt
|
44
|
+
|
45
|
+
headers = bunq_request_headers('PUT', NO_PARAMS, body, headers || {})
|
46
|
+
|
47
|
+
resource.put(body, headers) do |response, request, result|
|
50
48
|
verify_and_handle_response(response, request, result, &block)
|
51
49
|
end
|
50
|
+
rescue RestClient::Exceptions::Timeout
|
51
|
+
raise Bunq::Timeout
|
52
52
|
end
|
53
53
|
|
54
54
|
def append(path)
|
55
55
|
Bunq::Resource.new(client, @path + path)
|
56
56
|
end
|
57
57
|
|
58
|
-
def ensure_session!
|
59
|
-
client.ensure_session!
|
60
|
-
end
|
61
|
-
|
62
58
|
def with_session(&block)
|
63
59
|
client.with_session(&block)
|
64
60
|
end
|
65
61
|
|
66
62
|
private
|
67
63
|
|
68
|
-
attr_reader :client
|
64
|
+
attr_reader :client, :path
|
69
65
|
|
70
|
-
def
|
71
|
-
|
66
|
+
def resource
|
67
|
+
RestClient::Resource.new(
|
68
|
+
"#{client.configuration.base_url}#{path}",
|
69
|
+
{
|
70
|
+
headers: client.headers,
|
71
|
+
timeout: client.configuration.timeout,
|
72
|
+
}.tap do |x|
|
73
|
+
if client.configuration.sandbox
|
74
|
+
x[:user] = client.configuration.sandbox_user
|
75
|
+
x[:password] = client.configuration.sandbox_password
|
76
|
+
end
|
77
|
+
end
|
78
|
+
)
|
79
|
+
end
|
80
|
+
|
81
|
+
def bunq_request_headers(verb, params, payload = nil, headers = {})
|
82
|
+
headers['X-Bunq-Client-Request-Id'] = SecureRandom.uuid
|
83
|
+
|
84
|
+
unless @path.end_with?('/installation') && verb == 'POST'
|
85
|
+
headers['X-Bunq-Client-Signature'] = sign_request(verb, params, headers, payload)
|
86
|
+
end
|
72
87
|
|
73
|
-
|
74
|
-
request_id_header.merge('X-Bunq-Client-Signature' => sign_request(verb, params, request_id_header, payload))
|
88
|
+
headers
|
75
89
|
end
|
76
90
|
|
77
|
-
def sign_request(verb, params,
|
78
|
-
|
91
|
+
def sign_request(verb, params, headers, payload = nil)
|
92
|
+
client.signature.create(
|
79
93
|
verb,
|
80
94
|
encode_params(@path, params),
|
81
|
-
|
95
|
+
resource.headers.merge(headers),
|
82
96
|
payload
|
83
97
|
)
|
84
98
|
end
|
@@ -89,18 +103,21 @@ module Bunq
|
|
89
103
|
end
|
90
104
|
|
91
105
|
def verify_and_handle_response(response, request, result, &block)
|
92
|
-
|
106
|
+
client.signature.verify!(response) unless client.configuration.disable_response_signature_verification
|
93
107
|
handle_response(response, request, result, &block)
|
94
108
|
end
|
95
109
|
|
96
110
|
def handle_response(response, _request, _result, &block)
|
97
|
-
|
98
|
-
when 200, 201
|
111
|
+
if response.code == 200 || response.code == 201
|
99
112
|
if block_given?
|
100
113
|
yield(response)
|
101
114
|
else
|
102
115
|
JSON.parse(response.body)
|
103
116
|
end
|
117
|
+
elsif (response.code == 409 && Bunq.configuration.sandbox) || response.code == 429
|
118
|
+
fail TooManyRequestsResponse.new(code: response.code, headers: response.raw_headers, body: response.body)
|
119
|
+
elsif response.code == 401
|
120
|
+
fail UnauthorisedResponse.new(code: response.code, headers: response.raw_headers, body: response.body)
|
104
121
|
else
|
105
122
|
fail UnexpectedResponse.new(code: response.code, headers: response.raw_headers, body: response.body)
|
106
123
|
end
|
data/lib/bunq/session_servers.rb
CHANGED
@@ -10,7 +10,7 @@ module Bunq
|
|
10
10
|
##
|
11
11
|
# https://doc.bunq.com/api/1/call/session-server/method/post
|
12
12
|
def create
|
13
|
-
fail 'Cannot create session, please
|
13
|
+
fail 'Cannot create session, please add the api_key to your configuration' unless @api_key
|
14
14
|
@resource.post(secret: @api_key)['Response']
|
15
15
|
end
|
16
16
|
end
|
data/lib/bunq/signature.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require_relative '
|
1
|
+
require_relative 'errors'
|
2
2
|
|
3
3
|
module Bunq
|
4
4
|
class Signature
|
@@ -27,13 +27,18 @@ module Bunq
|
|
27
27
|
end
|
28
28
|
|
29
29
|
def verify!(response)
|
30
|
+
return if skip_signature_check(response.code)
|
31
|
+
|
30
32
|
sorted_bunq_headers = response.raw_headers.select(&method(:verifiable_header?)).sort.to_h.map { |k, v| "#{k.to_s.split('-').map(&:capitalize).join('-')}: #{v.first}" }
|
31
33
|
data = %Q{#{response.code}\n#{sorted_bunq_headers.join("\n")}\n\n#{response.body}}
|
32
34
|
|
33
|
-
signature_headers = response.raw_headers.find { |k, _| k.to_s.downcase == BUNQ_SERVER_SIGNATURE_RESPONSE_HEADER }
|
34
|
-
fail
|
35
|
+
signature_headers = response.raw_headers.find { |k, _| k.to_s.downcase == BUNQ_SERVER_SIGNATURE_RESPONSE_HEADER }
|
36
|
+
fail AbsentResponseSignature.new(code: response.code, headers: response.raw_headers, body: response.body) unless signature_headers
|
37
|
+
|
38
|
+
signature_headers_value = signature_headers[1]
|
39
|
+
fail AbsentResponseSignature.new(code: response.code, headers: response.raw_headers, body: response.body) unless signature_headers_value
|
35
40
|
|
36
|
-
signature = Base64.strict_decode64(
|
41
|
+
signature = Base64.strict_decode64(signature_headers_value.first)
|
37
42
|
fail UnexpectedResponse.new(code: response.code, headers: response.raw_headers, body: response.body) unless server_public_key.verify(digest, signature, data)
|
38
43
|
end
|
39
44
|
|
@@ -63,5 +68,9 @@ module Bunq
|
|
63
68
|
_header_name = header_name.to_s.downcase
|
64
69
|
_header_name.start_with?(BUNQ_HEADER_PREFIX) && _header_name != BUNQ_SERVER_SIGNATURE_RESPONSE_HEADER
|
65
70
|
end
|
71
|
+
|
72
|
+
def skip_signature_check(responseCode)
|
73
|
+
(Bunq::configuration.sandbox && responseCode == 409) || responseCode == 429
|
74
|
+
end
|
66
75
|
end
|
67
76
|
end
|
data/lib/bunq/user.rb
CHANGED
@@ -4,6 +4,8 @@ require_relative 'monetary_accounts'
|
|
4
4
|
require_relative 'draft_share_invite_bank'
|
5
5
|
require_relative 'draft_share_invite_banks'
|
6
6
|
require_relative 'certificate_pinned'
|
7
|
+
require_relative 'card'
|
8
|
+
require_relative 'cards'
|
7
9
|
|
8
10
|
module Bunq
|
9
11
|
class User
|
@@ -31,6 +33,14 @@ module Bunq
|
|
31
33
|
Bunq::CertificatePinned.new(@resource)
|
32
34
|
end
|
33
35
|
|
36
|
+
def card(id)
|
37
|
+
Bunq::Card.new(@resource, id)
|
38
|
+
end
|
39
|
+
|
40
|
+
def cards
|
41
|
+
Bunq::Cards.new(@resource)
|
42
|
+
end
|
43
|
+
|
34
44
|
def show
|
35
45
|
@resource.with_session { @resource.get }['Response']
|
36
46
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require_relative 'resource'
|
2
|
+
|
3
|
+
module Bunq
|
4
|
+
class UserPerson
|
5
|
+
def initialize(client, id)
|
6
|
+
@resource = Bunq::Resource.new(client, "/v1/user-person/#{id}")
|
7
|
+
end
|
8
|
+
|
9
|
+
def show
|
10
|
+
@resource.with_session { @resource.get }['Response']
|
11
|
+
end
|
12
|
+
|
13
|
+
def update(attributes)
|
14
|
+
@resource.with_session { @resource.put(attributes) }['Response']
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/bunq/version.rb
CHANGED
metadata
CHANGED
@@ -1,16 +1,17 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bunq-client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Lars Vonk
|
8
8
|
- Bob Forma
|
9
9
|
- Derek Kraan
|
10
|
+
- Mike van Diepen
|
10
11
|
autorequire:
|
11
12
|
bindir: exe
|
12
13
|
cert_chain: []
|
13
|
-
date:
|
14
|
+
date: 2019-06-03 00:00:00.000000000 Z
|
14
15
|
dependencies:
|
15
16
|
- !ruby/object:Gem::Dependency
|
16
17
|
name: rest-client
|
@@ -26,62 +27,76 @@ dependencies:
|
|
26
27
|
- - "~>"
|
27
28
|
- !ruby/object:Gem::Version
|
28
29
|
version: '2.0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: thread_safe
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
requirements:
|
34
|
+
- - "~>"
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: 0.3.6
|
37
|
+
type: :runtime
|
38
|
+
prerelease: false
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - "~>"
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 0.3.6
|
29
44
|
- !ruby/object:Gem::Dependency
|
30
45
|
name: bundler
|
31
46
|
requirement: !ruby/object:Gem::Requirement
|
32
47
|
requirements:
|
33
48
|
- - "~>"
|
34
49
|
- !ruby/object:Gem::Version
|
35
|
-
version: '1.
|
50
|
+
version: '1.16'
|
36
51
|
type: :development
|
37
52
|
prerelease: false
|
38
53
|
version_requirements: !ruby/object:Gem::Requirement
|
39
54
|
requirements:
|
40
55
|
- - "~>"
|
41
56
|
- !ruby/object:Gem::Version
|
42
|
-
version: '1.
|
57
|
+
version: '1.16'
|
43
58
|
- !ruby/object:Gem::Dependency
|
44
59
|
name: rake
|
45
60
|
requirement: !ruby/object:Gem::Requirement
|
46
61
|
requirements:
|
47
62
|
- - "~>"
|
48
63
|
- !ruby/object:Gem::Version
|
49
|
-
version: '
|
64
|
+
version: '12.3'
|
50
65
|
type: :development
|
51
66
|
prerelease: false
|
52
67
|
version_requirements: !ruby/object:Gem::Requirement
|
53
68
|
requirements:
|
54
69
|
- - "~>"
|
55
70
|
- !ruby/object:Gem::Version
|
56
|
-
version: '
|
71
|
+
version: '12.3'
|
57
72
|
- !ruby/object:Gem::Dependency
|
58
73
|
name: rspec
|
59
74
|
requirement: !ruby/object:Gem::Requirement
|
60
75
|
requirements:
|
61
76
|
- - "~>"
|
62
77
|
- !ruby/object:Gem::Version
|
63
|
-
version: '3.
|
78
|
+
version: '3.7'
|
64
79
|
type: :development
|
65
80
|
prerelease: false
|
66
81
|
version_requirements: !ruby/object:Gem::Requirement
|
67
82
|
requirements:
|
68
83
|
- - "~>"
|
69
84
|
- !ruby/object:Gem::Version
|
70
|
-
version: '3.
|
85
|
+
version: '3.7'
|
71
86
|
- !ruby/object:Gem::Dependency
|
72
87
|
name: webmock
|
73
88
|
requirement: !ruby/object:Gem::Requirement
|
74
89
|
requirements:
|
75
90
|
- - "~>"
|
76
91
|
- !ruby/object:Gem::Version
|
77
|
-
version:
|
92
|
+
version: 3.2.1
|
78
93
|
type: :development
|
79
94
|
prerelease: false
|
80
95
|
version_requirements: !ruby/object:Gem::Requirement
|
81
96
|
requirements:
|
82
97
|
- - "~>"
|
83
98
|
- !ruby/object:Gem::Version
|
84
|
-
version:
|
99
|
+
version: 3.2.1
|
85
100
|
- !ruby/object:Gem::Dependency
|
86
101
|
name: rspec-json_expectations
|
87
102
|
requirement: !ruby/object:Gem::Requirement
|
@@ -114,7 +129,8 @@ description: Ruby client for the bunq public API. Extracted from www.jortt.nl
|
|
114
129
|
email:
|
115
130
|
- lars.vonk@gmail.com
|
116
131
|
- bforma@zilverline.com
|
117
|
-
-
|
132
|
+
- derek.kraan@gmail.com
|
133
|
+
- mvdiepen@zilverline.com
|
118
134
|
executables: []
|
119
135
|
extensions: []
|
120
136
|
extra_rdoc_files: []
|
@@ -129,25 +145,31 @@ files:
|
|
129
145
|
- README.md
|
130
146
|
- Rakefile
|
131
147
|
- bunq-client.gemspec
|
148
|
+
- lib/bunq/attachment_public_content.rb
|
132
149
|
- lib/bunq/bunq.rb
|
150
|
+
- lib/bunq/card.rb
|
151
|
+
- lib/bunq/cards.rb
|
133
152
|
- lib/bunq/certificate_pinned.rb
|
134
153
|
- lib/bunq/client.rb
|
135
154
|
- lib/bunq/device_servers.rb
|
136
155
|
- lib/bunq/draft_share_invite_bank.rb
|
137
156
|
- lib/bunq/draft_share_invite_banks.rb
|
157
|
+
- lib/bunq/encryptor.rb
|
158
|
+
- lib/bunq/errors.rb
|
138
159
|
- lib/bunq/installation.rb
|
139
160
|
- lib/bunq/installations.rb
|
140
161
|
- lib/bunq/monetary_account.rb
|
141
162
|
- lib/bunq/monetary_accounts.rb
|
142
163
|
- lib/bunq/paginated.rb
|
164
|
+
- lib/bunq/payment.rb
|
143
165
|
- lib/bunq/payments.rb
|
144
166
|
- lib/bunq/qr_code_content.rb
|
145
167
|
- lib/bunq/resource.rb
|
146
168
|
- lib/bunq/session_servers.rb
|
147
169
|
- lib/bunq/signature.rb
|
148
|
-
- lib/bunq/unexpected_response.rb
|
149
170
|
- lib/bunq/user.rb
|
150
171
|
- lib/bunq/user_company.rb
|
172
|
+
- lib/bunq/user_person.rb
|
151
173
|
- lib/bunq/version.rb
|
152
174
|
homepage: https://github.com/jorttbv/bunq-client
|
153
175
|
licenses:
|
@@ -169,7 +191,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
169
191
|
version: '0'
|
170
192
|
requirements: []
|
171
193
|
rubyforge_project:
|
172
|
-
rubygems_version: 2.
|
194
|
+
rubygems_version: 2.7.6
|
173
195
|
signing_key:
|
174
196
|
specification_version: 4
|
175
197
|
summary: Ruby client for the bunq public API.
|