blockchyp 2.2.1

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/Makefile +71 -0
  3. data/README.md +997 -0
  4. data/Rakefile +39 -0
  5. data/lib/blockchyp.rb +190 -0
  6. data/lib/blockchyp/version.rb +5 -0
  7. data/lib/blockchyp_client.rb +346 -0
  8. data/lib/crypto_utils.rb +25 -0
  9. data/test/boolean_prompt_test.rb +43 -0
  10. data/test/capture_signature_test.rb +40 -0
  11. data/test/gateway_timeout_test.rb +40 -0
  12. data/test/heartbeat_test.rb +27 -0
  13. data/test/new_transaction_display_test.rb +78 -0
  14. data/test/pan_charge_test.rb +53 -0
  15. data/test/pan_enroll_test.rb +51 -0
  16. data/test/pan_preauth_test.rb +52 -0
  17. data/test/simple_batch_close_test.rb +48 -0
  18. data/test/simple_capture_test.rb +47 -0
  19. data/test/simple_gift_activate_test.rb +42 -0
  20. data/test/simple_message_test.rb +40 -0
  21. data/test/simple_ping_test.rb +39 -0
  22. data/test/simple_refund_test.rb +48 -0
  23. data/test/simple_reversal_test.rb +48 -0
  24. data/test/simple_void_test.rb +48 -0
  25. data/test/terminal_charge_test.rb +51 -0
  26. data/test/terminal_clear_test.rb +39 -0
  27. data/test/terminal_ebt_balance_test.rb +41 -0
  28. data/test/terminal_ebt_charge_test.rb +53 -0
  29. data/test/terminal_enroll_test.rb +50 -0
  30. data/test/terminal_gift_card_balance_test.rb +40 -0
  31. data/test/terminal_keyed_charge_test.rb +52 -0
  32. data/test/terminal_manual_ebt_charge_test.rb +54 -0
  33. data/test/terminal_preauth_test.rb +51 -0
  34. data/test/terminal_status_test.rb +39 -0
  35. data/test/terminal_timeout_test.rb +39 -0
  36. data/test/terms_and_conditions_test.rb +44 -0
  37. data/test/test_helper.rb +65 -0
  38. data/test/text_prompt_test.rb +41 -0
  39. data/test/update_transaction_display_test.rb +78 -0
  40. metadata +81 -0
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+
5
+ begin
6
+ Bundler.setup(:default)
7
+ rescue Bundler::BundlerError => e
8
+ warn e.message
9
+ warn 'Run `bundle install` to install missing gems'
10
+ exit e.status_code
11
+ end
12
+
13
+ require 'rake'
14
+ require 'rake/testtask'
15
+
16
+ task(default: %i[test lint])
17
+
18
+ Rake::TestTask.new(:test) do |t|
19
+ t.libs << '.' << 'lib' << 'test'
20
+ t.test_files = FileList['test/*_test.rb']
21
+ t.verbose = false
22
+ end
23
+
24
+ task :lint do
25
+ if RUBY_ENGINE == 'ruby'
26
+ require 'rubocop/rake_task'
27
+ RuboCop::RakeTask.new
28
+ end
29
+ end
30
+
31
+ task(gem: :build)
32
+ task :build do
33
+ system 'gem build blockchyp.gemspec'
34
+ end
35
+
36
+ task publish: :build do
37
+ system "gem push blockchyp-#{BlockChyp::VERSION}.gem"
38
+ system "rm blockchyp-#{BlockChyp::VERSION}.gem"
39
+ end
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2019 BlockChyp, Inc. All rights reserved. Use of this code is
4
+ # governed by a license that can be found in the LICENSE file.
5
+ #
6
+ # This file was generated automatically. Changes to this file will be lost
7
+ # every time the code is regenerated.
8
+
9
+ require_relative 'blockchyp_client'
10
+
11
+ module CardType
12
+ CREDIT = 0
13
+ DEBIT = 1
14
+ EBT = 2
15
+ BLOCKCHAIN_GIFT = 3
16
+ end
17
+
18
+ module SignatureFormat
19
+ NONE = ''
20
+ PNG = 'png'
21
+ JPG = 'jpg'
22
+ GIF = 'gif'
23
+ end
24
+
25
+ module PromptType
26
+ AMOUNT = 'amount'
27
+ EMAIL = 'email'
28
+ PHONE_NUMBER = 'phone'
29
+ CUSTOMER_NUMBER = 'customer-number'
30
+ REWARDS_NUMBER = 'rewards-number'
31
+ FIRST_NAME = 'first-name'
32
+ LAST_NAME = 'last-name'
33
+ end
34
+
35
+ module AVSResponse
36
+ NOT_APPLICABLE = ''
37
+ NOT_SUPPORTED = 'not_supported'
38
+ RETRY = 'retry'
39
+ NO_MATCH = 'no_match'
40
+ ADDRESS_MATCH = 'address_match'
41
+ POSTAL_CODE_MATCH = 'zip_match'
42
+ ADDRESS_AND_POSTAL_CODE_MATCH = 'match'
43
+ end
44
+
45
+ module BlockChyp
46
+ # the main autogenerated blockchyp client
47
+ class BlockChyp < BlockChypClient
48
+
49
+ def heartbeat(test)
50
+ gateway_request('GET', '/api/heartbeat', {'test' => test})
51
+ end
52
+
53
+ # Executes a standard direct preauth and capture.
54
+ def charge(request)
55
+ route_terminal_request('POST', '/api/charge', '/api/charge', request)
56
+ end
57
+
58
+ # Executes a preauthorization intended to be captured later.
59
+ def preauth(request)
60
+ route_terminal_request('POST', '/api/preauth', '/api/preauth', request)
61
+ end
62
+
63
+ # Tests connectivity with a payment terminal.
64
+ def ping(request)
65
+ route_terminal_request('POST', '/api/test', '/api/terminal-test', request)
66
+ end
67
+
68
+ # Checks the remaining balance on a payment method.
69
+ def balance(request)
70
+ route_terminal_request('POST', '/api/balance', '/api/balance', request)
71
+ end
72
+
73
+ # Clears the line item display and any in progress transaction.
74
+ def clear(request)
75
+ route_terminal_request('POST', '/api/clear', '/api/terminal-clear', request)
76
+ end
77
+
78
+ # Prompts the user to accept terms and conditions.
79
+ def terms_and_conditions(request)
80
+ route_terminal_request('POST', '/api/tc', '/api/terminal-tc', request)
81
+ end
82
+
83
+ # Appends items to an existing transaction display. Subtotal, Tax, and
84
+ # Total are overwritten by the request. Items with the same description
85
+ # are combined into groups.
86
+ def update_transaction_display(request)
87
+ route_terminal_request('PUT', '/api/txdisplay', '/api/terminal-txdisplay', request)
88
+ end
89
+
90
+ # Displays a new transaction on the terminal.
91
+ def new_transaction_display(request)
92
+ route_terminal_request('POST', '/api/txdisplay', '/api/terminal-txdisplay', request)
93
+ end
94
+
95
+ # Asks the consumer a text based question.
96
+ def text_prompt(request)
97
+ route_terminal_request('POST', '/api/text-prompt', '/api/text-prompt', request)
98
+ end
99
+
100
+ # Asks the consumer a yes/no question.
101
+ def boolean_prompt(request)
102
+ route_terminal_request('POST', '/api/boolean-prompt', '/api/boolean-prompt', request)
103
+ end
104
+
105
+ # Displays a short message on the terminal.
106
+ def message(request)
107
+ route_terminal_request('POST', '/api/message', '/api/message', request)
108
+ end
109
+
110
+ # Executes a refund.
111
+ def refund(request)
112
+ route_terminal_request('POST', '/api/refund', '/api/refund', request)
113
+ end
114
+
115
+ # Adds a new payment method to the token vault.
116
+ def enroll(request)
117
+ route_terminal_request('POST', '/api/enroll', '/api/enroll', request)
118
+ end
119
+
120
+ # Activates or recharges a gift card.
121
+ def gift_activate(request)
122
+ route_terminal_request('POST', '/api/gift-activate', '/api/gift-activate', request)
123
+ end
124
+
125
+ # Returns the current status of a terminal.
126
+ def terminal_status(request)
127
+ route_terminal_request('POST', '/api/terminal-status', '/api/terminal-status', request)
128
+ end
129
+
130
+ # Captures and returns a signature.
131
+ def capture_signature(request)
132
+ route_terminal_request('POST', '/api/capture-signature', '/api/capture-signature', request)
133
+ end
134
+
135
+ # Executes a manual time out reversal.
136
+ #
137
+ # We love time out reversals. Don't be afraid to use them whenever a
138
+ # request to a BlockChyp terminal times out. You have up to two minutes to
139
+ # reverse any transaction. The only caveat is that you must assign
140
+ # transactionRef values when you build the original request. Otherwise, we
141
+ # have no real way of knowing which transaction you're trying to reverse
142
+ # because we may not have assigned it an id yet. And if we did assign it
143
+ # an id, you wouldn't know what it is because your request to the terminal
144
+ # timed out before you got a response.
145
+ def reverse(request)
146
+ gateway_request('POST', '/api/reverse', request)
147
+ end
148
+
149
+ # Captures a preauthorization.
150
+ def capture(request)
151
+ gateway_request('POST', '/api/capture', request)
152
+ end
153
+
154
+ # Closes the current credit card batch.
155
+ def close_batch(request)
156
+ gateway_request('POST', '/api/close-batch', request)
157
+ end
158
+
159
+ # Discards a previous preauth transaction.
160
+ def void(request)
161
+ gateway_request('POST', '/api/void', request)
162
+ end
163
+
164
+ # Updates or creates a customer record.
165
+ def update_customer(request)
166
+ gateway_request('POST', '/api/update-customer', request)
167
+ end
168
+
169
+ # Retrieves a customer by id.
170
+ def customer(request)
171
+ gateway_request('POST', '/api/customer', request)
172
+ end
173
+
174
+ # Searches the customer database.
175
+ def customer_search(request)
176
+ gateway_request('POST', '/api/customer-search', request)
177
+ end
178
+
179
+ # Retrieves the current status of a transaction.
180
+ def transaction_status(request)
181
+ gateway_request('POST', '/api/tx-status', request)
182
+ end
183
+
184
+ # Creates and send a payment link to a customer.
185
+ def send_payment_link(request)
186
+ gateway_request('POST', '/api/send-payment-link', request)
187
+ end
188
+
189
+ end
190
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockChyp
4
+ VERSION = '2.2.1'
5
+ end
@@ -0,0 +1,346 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'cgi'
5
+ require 'digest'
6
+ require 'json'
7
+ require 'net/http'
8
+ require 'openssl'
9
+ require 'time'
10
+ require 'tmpdir'
11
+ require 'uri'
12
+
13
+ require_relative 'crypto_utils'
14
+
15
+ module BlockChyp
16
+ # base class for the blockchyp generated blockchyp client
17
+ class BlockChypClient
18
+ def initialize(api_key, bearer_token, signing_key)
19
+ @api_key = api_key
20
+ @bearer_token = bearer_token
21
+ @signing_key = signing_key
22
+ @gateway_host = 'https://api.blockchyp.com'
23
+ @test_gateway_host = 'https://test.blockchyp.com'
24
+ @https = false
25
+ @route_cache_location = File.join(Dir.tmpdir, '.blockchyp_route')
26
+ @route_cache_ttl = 60
27
+ @gateway_timeout = 20
28
+ @terminal_timeout = 120
29
+ @terminal_connect_timeout = 5
30
+ @offline_cache_enabled = true
31
+ @route_cache = {}
32
+ @offline_fixed_key = 'cb22789c9d5c344a10e0474f134db39e25eb3bbf5a1b1a5e89b507f15ea9519c'
33
+ end
34
+
35
+ attr_reader :api_key
36
+ attr_reader :bearer_token
37
+ attr_reader :signing_key
38
+ attr_reader :offline_fixed_key
39
+ attr_accessor :gateway_host
40
+ attr_accessor :test_gateway_host
41
+ attr_accessor :https
42
+ attr_accessor :route_cache_ttl
43
+ attr_accessor :gateway_timeout
44
+ attr_accessor :terminal_timeout
45
+ attr_accessor :offline_cache_enabled
46
+ attr_accessor :terminal_connect_timeout
47
+ attr_accessor :route_cache_location
48
+
49
+ def generate_gateway_headers
50
+ nonce = CryptoUtils.generate_nonce
51
+ tsp = CryptoUtils.timestamp
52
+
53
+ sig = compute_hmac(tsp, nonce)
54
+
55
+ {
56
+ 'Nonce' => nonce,
57
+ 'Timestamp' => tsp,
58
+ 'Authorization' => 'Dual ' + bearer_token + ':' + api_key + ':' + sig
59
+ }
60
+ end
61
+
62
+ def compute_hmac(tsp, nonce)
63
+ canonical_string = api_key + bearer_token + tsp + nonce
64
+
65
+ OpenSSL::HMAC.hexdigest('SHA256', CryptoUtils.hex2bin(signing_key), canonical_string)
66
+ end
67
+
68
+ def resolve_gateway_uri(path, request)
69
+ url = if request.nil? || !request['test']
70
+ gateway_host
71
+ else
72
+ test_gateway_host
73
+ end
74
+
75
+ URI.parse(url + path)
76
+ end
77
+
78
+ def generate_error_response(msg)
79
+ [
80
+ 'success' => false,
81
+ 'error' => msg,
82
+ 'responseDescription' => msg
83
+ ]
84
+ end
85
+
86
+ def route_terminal_request(method, terminal_path, gateway_path, request)
87
+ if request['terminalName'].nil?
88
+ return gateway_request(method, gateway_path, request)
89
+ end
90
+
91
+ route = resolve_terminal_route(request['terminalName'])
92
+ if !route
93
+ return generate_error_response('Unkown Terminal')
94
+ elsif route['cloudRelayEnabled']
95
+ return gateway_request(method, gateway_path, request, relay: true)
96
+ end
97
+
98
+ terminal_request(method, route, terminal_path, request, true)
99
+ end
100
+
101
+ def terminal_request(method, route, path, request, open_retry)
102
+ uri = resolve_terminal_uri(route, path)
103
+ http = Net::HTTP.new(uri.host, uri.port)
104
+ http.use_ssl = uri.instance_of?(URI::HTTPS)
105
+ timeout = get_timeout(request, terminal_timeout)
106
+ http.open_timeout = timeout
107
+ http.read_timeout = timeout
108
+
109
+ tx_creds = route['transientCredentials']
110
+
111
+ wrapped_request = {
112
+ 'apiKey' => tx_creds['apiKey'],
113
+ 'bearerToken' => tx_creds['bearerToken'],
114
+ 'signingKey' => tx_creds['signingKey'],
115
+ 'request' => request
116
+ }
117
+
118
+ req = get_http_request(method, uri)
119
+
120
+ req['User-Agent'] = user_agent
121
+ json = wrapped_request.to_json
122
+ req['Content-Type'] = 'application/json'
123
+ req['Content-Length'] = json.length
124
+ req.body = json
125
+
126
+ begin
127
+ response = http.request(req)
128
+ rescue Net::OpenTimeout, Errno::EHOSTDOWN, Errno::EHOSTUNREACH, Errno::ETIMEDOUT, Errno::ENETUNREACH
129
+ if open_retry
130
+ evict(route['terminalName'])
131
+ route = resolve_terminal_route(route['terminalName'])
132
+ return terminal_request(method, route, path, request, false)
133
+ end
134
+ raise
135
+ end
136
+ if response.is_a?(Net::HTTPSuccess)
137
+ JSON.parse(response.body)
138
+ else
139
+ raise response.message
140
+ end
141
+ end
142
+
143
+ def get_http_request(method, uri)
144
+ case method
145
+ when 'GET'
146
+ Net::HTTP::Get.new(uri.request_uri)
147
+ when 'PUT'
148
+ Net::HTTP::Put.new(uri.request_uri)
149
+ when 'POST'
150
+ Net::HTTP::Post.new(uri.request_uri)
151
+ end
152
+ end
153
+
154
+ def resolve_terminal_uri(route, path)
155
+ url = if https
156
+ 'https://'
157
+ else
158
+ 'http://'
159
+ end
160
+ url += route['ipAddress']
161
+ port = if https
162
+ ':8443'
163
+ else
164
+ ':8080'
165
+ end
166
+ URI.parse(url + port + path)
167
+ end
168
+
169
+ def gateway_request(method, path, request = nil, relay = false)
170
+ uri = resolve_gateway_uri(path, request)
171
+ http = Net::HTTP.new(uri.host, uri.port)
172
+ http.use_ssl = uri.instance_of?(URI::HTTPS)
173
+ timeout = get_timeout(request, relay ? terminal_timeout : gateway_timeout)
174
+ http.open_timeout = timeout
175
+ http.read_timeout = timeout
176
+
177
+ req = get_http_request(method, uri)
178
+
179
+ req['User-Agent'] = user_agent
180
+ unless request.nil?
181
+ json = request.to_json
182
+ req['Content-Type'] = 'application/json'
183
+ req['Content-Length'] = json.length
184
+ req.body = json
185
+ end
186
+
187
+ headers = generate_gateway_headers
188
+ headers.each do |key, value|
189
+ req[key] = value
190
+ end
191
+
192
+ response = http.request(req)
193
+
194
+ if response.is_a?(Net::HTTPSuccess)
195
+ JSON.parse(response.body)
196
+ else
197
+ raise response.message
198
+ end
199
+ end
200
+
201
+ attr_accessor :routeCache
202
+
203
+ def resolve_terminal_route(terminal_name)
204
+ route = route_cache_get(terminal_name, false)
205
+
206
+ if route.nil?
207
+ route = request_route_from_gateway(terminal_name)
208
+ if route.nil?
209
+ return route
210
+ end
211
+
212
+ ttl = Time.now.utc + (route_cache_ttl * 60)
213
+ route_cache_entry = {}
214
+ route_cache_entry['route'] = route
215
+ route_cache_entry['ttl'] = ttl
216
+ @route_cache[api_key + terminal_name] = route_cache_entry
217
+ update_offline_cache(route_cache_entry)
218
+ end
219
+ route
220
+ end
221
+
222
+ def update_offline_cache(route_cache_entry)
223
+ if offline_cache_enabled
224
+ offline_cache = read_offline_cache
225
+ offline_entry = route_cache_entry.clone
226
+ route = route_cache_entry['route'].clone
227
+ tx_creds = route['transientCredentials'].clone
228
+ tx_creds['apiKey'] = encrypt(tx_creds['apiKey'])
229
+ tx_creds['bearerToken'] = encrypt(tx_creds['bearerToken'])
230
+ tx_creds['signingKey'] = encrypt(tx_creds['signingKey'])
231
+ route['transientCredentials'] = tx_creds
232
+ offline_entry['route'] = route
233
+ offline_cache[api_key + route['terminalName']] = offline_entry
234
+ File.write(route_cache_location, offline_cache.to_json)
235
+ end
236
+ end
237
+
238
+ def encrypt(plain_text)
239
+ return plain_text if plain_text.nil? || plain_text.empty?
240
+
241
+ cipher = OpenSSL::Cipher::AES256.new(:CBC)
242
+ cipher.encrypt
243
+ cipher.key = derive_offline_key
244
+ iv = cipher.random_iv
245
+
246
+ Base64.encode64(iv) + ':' + Base64.encode64(cipher.update(plain_text) + cipher.final)
247
+ end
248
+
249
+ def decrypt(cipher_text)
250
+ return cipher_text if cipher_text.nil? || cipher_text.empty?
251
+
252
+ tokens = cipher_text.split(':')
253
+
254
+ return cipher_text if tokens[0].nil? || tokens[1].nil?
255
+
256
+ iv = Base64.decode64(tokens[0])
257
+ cp = Base64.decode64(tokens[1])
258
+
259
+ decipher = OpenSSL::Cipher::AES256.new(:CBC)
260
+ decipher.decrypt
261
+ decipher.key = derive_offline_key
262
+ decipher.iv = iv
263
+
264
+ decipher.update(cp) + decipher.final
265
+ end
266
+
267
+ def derive_offline_key
268
+ Digest::SHA256.digest offline_fixed_key + signing_key
269
+ end
270
+
271
+ def request_route_from_gateway(terminal_name)
272
+ route = gateway_request('GET', '/api/terminal-route?terminal=' + CGI.escape(terminal_name))
273
+ if !route.nil? && !route['ipAddress'].empty?
274
+ route['exists'] = true
275
+ end
276
+ route
277
+ end
278
+
279
+ def evict(terminal_name)
280
+ route_cache.delete(api_key + terminal_name)
281
+
282
+ offline_cache = read_offline_cache
283
+ offline_cache.delete(api_key + terminal_name)
284
+ File.write(route_cache_location, route_cache.to_json)
285
+ end
286
+
287
+ def route_cache_get(terminal_name, stale)
288
+ route_cache_entry = @route_cache[api_key + terminal_name]
289
+
290
+ if route_cache_entry.nil? && offline_cache_enabled
291
+ offline_cache = read_offline_cache
292
+ route_cache_entry = offline_cache[@api_key + terminal_name]
293
+ end
294
+
295
+ if route_cache_entry
296
+ route = route_cache_entry['route']
297
+ tx_creds = route['transientCredentials']
298
+ tx_creds['apiKey'] = decrypt(tx_creds['apiKey'])
299
+ tx_creds['bearerToken'] = decrypt(tx_creds['bearerToken'])
300
+ tx_creds['signingKey'] = decrypt(tx_creds['signingKey'])
301
+ route['transientCredentials'] = tx_creds
302
+ route_cache_entry['route'] = route
303
+ end
304
+
305
+ if route_cache_entry
306
+ now = Time.new
307
+ raw_ttl = route_cache_entry['ttl']
308
+ if raw_ttl.instance_of?(Time)
309
+ ttl = raw_ttl
310
+ else
311
+ ttl = Time.parse(route_cache_entry['ttl'])
312
+ end
313
+ if stale || now < ttl
314
+ route_cache_entry['route']
315
+ end
316
+ end
317
+ end
318
+
319
+ def read_offline_cache
320
+ if File.file?(route_cache_location)
321
+
322
+ config_file = File.open(route_cache_location)
323
+ content = config_file.read
324
+
325
+ return JSON.parse(content)
326
+ end
327
+ {}
328
+ end
329
+
330
+ def user_agent
331
+ if defined? VERSION
332
+ "BlockChyp-Ruby/#{VERSION}"
333
+ else
334
+ "BlockChyp-Ruby"
335
+ end
336
+ end
337
+
338
+ def get_timeout(request, default)
339
+ if request.nil? || request['timeout'].nil? || request['timeout'].zero?
340
+ return default
341
+ end
342
+
343
+ request['timeout']
344
+ end
345
+ end
346
+ end