blockchyp 2.0.0.pre.alpha7

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.
@@ -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,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'blockchyp_client'
4
+
5
+ module CardType
6
+ CREDIT = 0
7
+ DEBIT = 1
8
+ EBT = 2
9
+ BLOCKCHAIN_GIFT = 3
10
+ end
11
+
12
+ module SignatureFormat
13
+ NONE = ''
14
+ PNG = 'png'
15
+ JPG = 'jpg'
16
+ GIF = 'gif'
17
+ end
18
+
19
+ module PromptType
20
+ AMOUNT = 'amount'
21
+ EMAIL = 'email'
22
+ PHONE_NUMBER = 'phone'
23
+ CUSTOMER_NUMBER = 'customer-number'
24
+ REWARDS_NUMBER = 'rewards-number'
25
+ end
26
+
27
+ module BlockChyp
28
+ # the main autogenerated blockchyp client
29
+ class BlockChyp < BlockChypClient
30
+
31
+ def heartbeat(test)
32
+ gateway_get('/api/heartbeat', test)
33
+ end
34
+
35
+ # Executes a standard direct preauth and capture.
36
+ def charge(request)
37
+ route_terminal_request(request, '/api/charge', '/api/charge', 'POST')
38
+ end
39
+
40
+ # Executes a preauthorization intended to be captured later.
41
+ def preauth(request)
42
+ route_terminal_request(request, '/api/preauth', '/api/preauth', 'POST')
43
+ end
44
+
45
+ # Tests connectivity with a payment terminal.
46
+ def ping(request)
47
+ route_terminal_request(request, '/api/test', '/api/terminal-test', 'POST')
48
+ end
49
+
50
+ # Checks the remaining balance on a payment method.
51
+ def balance(request)
52
+ route_terminal_request(request, '/api/balance', '/api/balance', 'POST')
53
+ end
54
+
55
+ # Clears the line item display and any in progress transaction.
56
+ def clear(request)
57
+ route_terminal_request(request, '/api/clear', '/api/terminal-clear', 'POST')
58
+ end
59
+
60
+ # Prompts the user to accept terms and conditions.
61
+ def terms_and_conditions(request)
62
+ route_terminal_request(request, '/api/tc', '/api/terminal-tc', 'POST')
63
+ end
64
+
65
+ # Appends items to an existing transaction display Subtotal, Tax, and
66
+ # Total are overwritten by the request. Items with the same description
67
+ # are combined into groups.
68
+ def update_transaction_display(request)
69
+ route_terminal_request(request, '/api/txdisplay', '/api/terminal-txdisplay', 'PUT')
70
+ end
71
+
72
+ # Displays a new transaction on the terminal.
73
+ def new_transaction_display(request)
74
+ route_terminal_request(request, '/api/txdisplay', '/api/terminal-txdisplay', 'POST')
75
+ end
76
+
77
+ # Asks the consumer text based question.
78
+ def text_prompt(request)
79
+ route_terminal_request(request, '/api/text-prompt', '/api/text-prompt', 'POST')
80
+ end
81
+
82
+ # Asks the consumer a yes/no question.
83
+ def boolean_prompt(request)
84
+ route_terminal_request(request, '/api/boolean-prompt', '/api/boolean-prompt', 'POST')
85
+ end
86
+
87
+ # Displays a short message on the terminal.
88
+ def message(request)
89
+ route_terminal_request(request, '/api/message', '/api/message', 'POST')
90
+ end
91
+
92
+ # Executes a refund.
93
+ def refund(request)
94
+ route_terminal_request(request, '/api/refund', '/api/refund', 'POST')
95
+ end
96
+
97
+ # Adds a new payment method to the token vault.
98
+ def enroll(request)
99
+ route_terminal_request(request, '/api/enroll', '/api/enroll', 'POST')
100
+ end
101
+
102
+ # Activates or recharges a gift card.
103
+ def gift_activate(request)
104
+ route_terminal_request(request, '/api/gift-activate', '/api/gift-activate', 'POST')
105
+ end
106
+
107
+ # Executes a manual time out reversal.
108
+ #
109
+ # We love time out reversals. Don't be afraid to use them whenever a
110
+ # request to a BlockChyp terminal times out. You have up to two minutes to
111
+ # reverse any transaction. The only caveat is that you must assign
112
+ # transactionRef values when you build the original request. Otherwise, we
113
+ # have no real way of knowing which transaction you're trying to reverse
114
+ # because we may not have assigned it an id yet. And if we did assign it
115
+ # an id, you wouldn't know what it is because your request to the terminal
116
+ # timed out before you got a response.
117
+ def reverse(request)
118
+ gateway_request('/api/reverse', request, 'POST')
119
+ end
120
+
121
+ # Captures a preauthorization.
122
+ def capture(request)
123
+ gateway_request('/api/capture', request, 'POST')
124
+ end
125
+
126
+ # Closes the current credit card batch.
127
+ def close_batch(request)
128
+ gateway_request('/api/close-batch', request, 'POST')
129
+ end
130
+
131
+ # Discards a previous preauth transaction.
132
+ def void(request)
133
+ gateway_request('/api/void', request, 'POST')
134
+ end
135
+
136
+ end
137
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockChyp
4
+ VERSION = '2.0.0.pre.alpha7'
5
+ end
@@ -0,0 +1,334 @@
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 gateway_get(path, test)
50
+ path = resolve_gateway_url(path, test)
51
+ puts 'GET: ' + path
52
+ uri = URI(path)
53
+ req = Net::HTTP::Get.new(uri)
54
+ headers = generate_gateway_headers
55
+ headers.each do |key, value|
56
+ req[key] = value
57
+ end
58
+ res = Net::HTTP.new(uri.host, uri.port).start do |inner_http|
59
+ inner_http.request(req)
60
+ end
61
+ if res.is_a?(Net::HTTPSuccess)
62
+ JSON.parse(res.body)
63
+ else
64
+ raise res.message
65
+ end
66
+ end
67
+
68
+ def generate_gateway_headers
69
+ nonce = CryptoUtils.generate_nonce
70
+ tsp = CryptoUtils.timestamp
71
+
72
+ sig = compute_hmac(tsp, nonce)
73
+
74
+ {
75
+ 'Nonce' => nonce,
76
+ 'Timestamp' => tsp,
77
+ 'Authorization' => 'Dual ' + bearer_token + ':' + api_key + ':' + sig
78
+ }
79
+ end
80
+
81
+ def compute_hmac(tsp, nonce)
82
+ canonical_string = api_key + bearer_token + tsp + nonce
83
+
84
+ OpenSSL::HMAC.hexdigest('SHA256', CryptoUtils.hex2bin(signing_key), canonical_string)
85
+ end
86
+
87
+ def resolve_gateway_url(path, test)
88
+ url = if test
89
+ test_gateway_host
90
+ else
91
+ gateway_host
92
+ end
93
+
94
+ url + path
95
+ end
96
+
97
+ def generate_error_response(msg)
98
+ [
99
+ 'success' => false,
100
+ 'error' => msg,
101
+ 'responseDescription' => msg
102
+ ]
103
+ end
104
+
105
+ def route_terminal_request(request, terminal_path, gateway_path, method)
106
+ if request['terminalName'].nil?
107
+ return gateway_request(gateway_path, request, method)
108
+ end
109
+
110
+ route = resolve_terminal_route(request['terminalName'])
111
+ if !route
112
+ return generate_error_response('Unkown Terminal')
113
+ elsif route['cloudRelayEnabled']
114
+ return gateway_request(gateway_path, request, method)
115
+ end
116
+
117
+ terminal_request(route, terminal_path, request, method, true)
118
+ end
119
+
120
+ def terminal_request(route, path, request, method, open_retry)
121
+ url = resolve_terminal_url(route, path)
122
+
123
+ tx_creds = route['transientCredentials']
124
+
125
+ terminal_request = {
126
+ 'apiKey' => tx_creds['apiKey'],
127
+ 'bearerToken' => tx_creds['bearerToken'],
128
+ 'signingKey' => tx_creds['signingKey'],
129
+ 'request' => request
130
+ }
131
+
132
+ puts method + ': ' + url
133
+ puts terminal_request.to_json
134
+
135
+ uri = URI(url)
136
+ req = if method == 'PUT'
137
+ Net::HTTP::Put.new(uri)
138
+ else
139
+ Net::HTTP::Post.new(uri)
140
+ end
141
+ json = terminal_request.to_json
142
+ req['Content-Type'] = 'application/json'
143
+ req['Content-Length'] = json.length
144
+ req.body = json
145
+ http = Net::HTTP.new(uri.host, uri.port)
146
+ http.open_timeout = terminal_connect_timeout
147
+ http.read_timeout = terminal_timeout
148
+ begin
149
+ res = http.start do |inner_http|
150
+ inner_http.request(req)
151
+ end
152
+ rescue Net::OpenTimeout, Errno::EHOSTDOWN, Errno::EHOSTUNREACH, Errno::ETIMEDOUT, Errno::ENETUNREACH
153
+ if open_retry
154
+ evict(route['terminalName'])
155
+ route = resolve_terminal_route(route['terminalName'])
156
+ return terminal_request(route, path, request, method, false)
157
+ end
158
+ end
159
+ if res.is_a?(Net::HTTPSuccess)
160
+ puts 'Response: ' + res.body
161
+ JSON.parse(res.body)
162
+ else
163
+ raise res.message
164
+ end
165
+ end
166
+
167
+ def resolve_terminal_url(route, path)
168
+ url = if https
169
+ 'https://'
170
+ else
171
+ 'http://'
172
+ end
173
+ url += route['ipAddress']
174
+ port = if https
175
+ ':8443'
176
+ else
177
+ ':8080'
178
+ end
179
+ url + port + path
180
+ end
181
+
182
+ def gateway_request(path, request, method)
183
+ url = resolve_gateway_url(path, request['test'])
184
+
185
+ puts method + ': ' + url
186
+ puts request.to_json
187
+
188
+ uri = URI(url)
189
+ req = if method == 'PUT'
190
+ Net::HTTP::Put.new(uri)
191
+ else
192
+ Net::HTTP::Post.new(uri)
193
+ end
194
+ json = request.to_json
195
+ req['Content-Type'] = 'application/json'
196
+ req['Content-Length'] = json.length
197
+ headers = generate_gateway_headers
198
+ headers.each do |key, value|
199
+ req[key] = value
200
+ end
201
+ req.body = json
202
+ res = Net::HTTP.new(uri.host, uri.port).start do |http|
203
+ http.request(req)
204
+ end
205
+ if res.is_a?(Net::HTTPSuccess)
206
+ puts 'Response: ' + res.body
207
+ JSON.parse(res.body)
208
+ else
209
+ raise res.message
210
+ end
211
+ end
212
+
213
+ attr_accessor :routeCache
214
+
215
+ def resolve_terminal_route(terminal_name)
216
+ route = route_cache_get(terminal_name, false)
217
+
218
+ if route.nil?
219
+ route = request_route_from_gateway(terminal_name)
220
+ if route.nil?
221
+ return route
222
+ end
223
+
224
+ ttl = Time.now.utc + (route_cache_ttl * 60)
225
+ route_cache_entry = {}
226
+ route_cache_entry['route'] = route
227
+ route_cache_entry['ttl'] = ttl
228
+ @route_cache[api_key + terminal_name] = route_cache_entry
229
+ update_offline_cache(route_cache_entry)
230
+ end
231
+ route
232
+ end
233
+
234
+ def update_offline_cache(route_cache_entry)
235
+ if offline_cache_enabled
236
+ offline_cache = read_offline_cache
237
+ offline_entry = route_cache_entry.clone
238
+ route = route_cache_entry['route'].clone
239
+ tx_creds = route['transientCredentials'].clone
240
+ tx_creds['apiKey'] = encrypt(tx_creds['api_key'])
241
+ tx_creds['bearerToken'] = encrypt(tx_creds['bearerToken'])
242
+ tx_creds['signingKey'] = encrypt(tx_creds['signingKey'])
243
+ route['transientCredentials'] = tx_creds
244
+ offline_entry['route'] = route
245
+ offline_cache[apiKey + route['terminalName']] = offline_entry
246
+ File.write(route_cache_location, offline_cache.to_json)
247
+ end
248
+ end
249
+
250
+ def encrypt(plain_text)
251
+ cipher = OpenSSL::Cipher::AES256.new(:CBC)
252
+ cipher.encrypt
253
+ cipher.key = derive_offline_key
254
+ iv = cipher.random_iv
255
+
256
+ Base64.encode64(iv) + ':' + Base64.encode64(cipher.update(plain_text) + cipher.final)
257
+ end
258
+
259
+ def decrypt(cipher_text)
260
+ tokens = cipher_text.split(':')
261
+
262
+ iv = Base64.decode64(tokens[0])
263
+ cp = Base64.decode64(tokens[1])
264
+
265
+ decipher = OpenSSL::Cipher::AES256.new(:CBC)
266
+ decipher.decrypt
267
+ decipher.key = derive_offline_key
268
+ decipher.iv = iv
269
+
270
+ decipher.update(cp) + decipher.final
271
+ end
272
+
273
+ def derive_offline_key
274
+ Digest::SHA256.digest offline_fixed_key + signing_key
275
+ end
276
+
277
+ def request_route_from_gateway(terminal_name)
278
+ route = gateway_get('/api/terminal-route?terminal=' + CGI.escape(terminal_name), false)
279
+ if !route.nil? && !route['ipAddress'].empty?
280
+ route['exists'] = true
281
+ end
282
+ route
283
+ end
284
+
285
+ def evict(terminal_name)
286
+ route_cache.delete(api_key + terminal_name)
287
+
288
+ offline_cache = read_offline_cache
289
+ offline_cache.delete(api_key + terminal_name)
290
+ File.write(route_cache_location, route_cache.to_json)
291
+ end
292
+
293
+ def route_cache_get(terminal_name, stale)
294
+ route_cache_entry = @route_cache[api_key + terminal_name]
295
+
296
+ if route_cache_entry.nil? && offline_cache_enabled
297
+ offline_cache = read_offline_cache
298
+ route_cache_entry = offline_cache[@api_key + terminal_name]
299
+ end
300
+
301
+ if route_cache_entry
302
+ route = route_cache_entry['route']
303
+ tx_creds = route['transientCredentials']
304
+ tx_creds['apiKey'] = decrypt(tx_creds['apiKey'])
305
+ tx_creds['bearerToken'] = decrypt(tx_creds['bearerToken'])
306
+ tx_creds['signingKey'] = decrypt(tx_creds['signingKey'])
307
+ route['transientCredentials'] = tx_creds
308
+ routeCacheEntry['route'] = route
309
+ end
310
+
311
+ if route_cache_entry
312
+ now = Time.new
313
+ ttl = Time.parse(route_cache_entry['ttl'])
314
+ if stale || now < ttl
315
+ puts 'Cache Hit ' + route_cache_entry.to_json
316
+ route_cache_entry['route']
317
+ end
318
+ end
319
+ end
320
+
321
+ def read_offline_cache
322
+ puts route_cache_location
323
+
324
+ if File.file?(route_cache_location)
325
+
326
+ config_file = File.open(route_cache_location)
327
+ content = config_file.read
328
+
329
+ return JSON.parse(content)
330
+ end
331
+ {}
332
+ end
333
+ end
334
+ end