blest 0.0.1 → 0.1.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/README.md +64 -111
- data/lib/blest.rb +808 -143
- data/spec/blest_spec.rb +219 -0
- metadata +3 -2
data/lib/blest.rb
CHANGED
@@ -1,14 +1,347 @@
|
|
1
1
|
require 'socket'
|
2
2
|
require 'json'
|
3
|
+
require 'date'
|
4
|
+
require 'concurrent'
|
5
|
+
require 'securerandom'
|
6
|
+
require 'net/http'
|
3
7
|
|
4
|
-
|
5
|
-
|
6
|
-
|
8
|
+
|
9
|
+
|
10
|
+
class Router
|
11
|
+
attr_reader :routes
|
12
|
+
|
13
|
+
def initialize(options = nil)
|
14
|
+
@middleware = []
|
15
|
+
@afterware = []
|
16
|
+
@timeout = 5000
|
17
|
+
@introspection = false
|
18
|
+
@routes = {}
|
19
|
+
|
20
|
+
if options
|
21
|
+
@timeout = options['timeout'] || options[:timeout] || 5000
|
22
|
+
@introspection = options['introspection'] || options[:introspection] || false
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def before(&handler)
|
27
|
+
unless handler.is_a?(Proc)
|
28
|
+
raise ArgumentError, 'Before handlers should be procs'
|
29
|
+
end
|
30
|
+
|
31
|
+
arg_count = handler.arity
|
32
|
+
if arg_count <= 2
|
33
|
+
@middleware.push(handler)
|
34
|
+
else
|
35
|
+
raise ArgumentError, 'Before handlers should have at most three arguments'
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def after(&handler)
|
40
|
+
unless handler.is_a?(Proc)
|
41
|
+
raise ArgumentError, 'After handlers should be procs'
|
42
|
+
end
|
43
|
+
|
44
|
+
arg_count = handler.arity
|
45
|
+
if arg_count <= 2
|
46
|
+
@afterware.push(handler)
|
47
|
+
else
|
48
|
+
raise ArgumentError, 'After handlers should have at most two arguments'
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def route(route, &handler)
|
53
|
+
route_error = validate_route(route)
|
54
|
+
raise ArgumentError, route_error if route_error
|
55
|
+
raise ArgumentError, 'Route already exists' if @routes.key?(route)
|
56
|
+
raise ArgumentError, 'Handler should be a function' unless handler.respond_to?(:call)
|
57
|
+
|
58
|
+
@routes[route] = {
|
59
|
+
handler: [*@middleware, handler, *@afterware],
|
60
|
+
description: nil,
|
61
|
+
parameters: nil,
|
62
|
+
result: nil,
|
63
|
+
visible: @introspection,
|
64
|
+
validate: false,
|
65
|
+
timeout: @timeout
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
def describe(route, config)
|
70
|
+
raise ArgumentError, 'Route does not exist' unless @routes.key?(route)
|
71
|
+
raise ArgumentError, 'Configuration should be an object' unless config.is_a?(Hash)
|
72
|
+
|
73
|
+
if config.key?('description')
|
74
|
+
raise ArgumentError, 'Description should be a str' if !config['description'].nil? && !config['description'].is_a?(String)
|
75
|
+
@routes[route]['description'] = config['description']
|
76
|
+
end
|
77
|
+
|
78
|
+
if config.key?('parameters')
|
79
|
+
raise ArgumentError, 'Parameters should be a dict' if !config['parameters'].nil? && !config['parameters'].is_a?(Hash)
|
80
|
+
@routes[route]['parameters'] = config['parameters']
|
81
|
+
end
|
82
|
+
|
83
|
+
if config.key?('result')
|
84
|
+
raise ArgumentError, 'Result should be a dict' if !config['result'].nil? && !config['result'].is_a?(Hash)
|
85
|
+
@routes[route]['result'] = config['result']
|
86
|
+
end
|
87
|
+
|
88
|
+
if config.key?('visible')
|
89
|
+
raise ArgumentError, 'Visible should be True or False' if !config['visible'].nil? && ![true, false].include?(config['visible'])
|
90
|
+
@routes[route]['visible'] = config['visible']
|
91
|
+
end
|
92
|
+
|
93
|
+
if config.key?('validate')
|
94
|
+
raise ArgumentError, 'Validate should be True or False' if !config['validate'].nil? && ![true, false].include?(config['validate'])
|
95
|
+
@routes[route]['validate'] = config['validate']
|
96
|
+
end
|
97
|
+
|
98
|
+
if config.key?('timeout')
|
99
|
+
raise ArgumentError, 'Timeout should be a positive int' if !config['timeout'].nil? && (!config['timeout'].is_a?(Integer) || config['timeout'] <= 0)
|
100
|
+
@routes[route]['timeout'] = config['timeout']
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def merge(router)
|
105
|
+
raise ArgumentError, 'Router is required' unless router.is_a?(Router)
|
106
|
+
|
107
|
+
new_routes = router.routes.keys
|
108
|
+
existing_routes = @routes.keys
|
109
|
+
|
110
|
+
raise ArgumentError, 'No routes to merge' if new_routes.empty?
|
111
|
+
|
112
|
+
new_routes.each do |route|
|
113
|
+
if existing_routes.include?(route)
|
114
|
+
raise ArgumentError, 'Cannot merge duplicate routes: ' + route
|
115
|
+
else
|
116
|
+
@routes[route] = {
|
117
|
+
**router.routes[route],
|
118
|
+
handler: @middleware + router.routes[route][:handler] + @afterware,
|
119
|
+
timeout: router.routes[route][:timeout] || @timeout
|
120
|
+
}
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def namespace(prefix, router)
|
126
|
+
raise ArgumentError, 'Router is required' unless router.is_a?(Router)
|
127
|
+
|
128
|
+
prefix_error = validate_route(prefix)
|
129
|
+
raise ArgumentError, prefix_error if prefix_error
|
130
|
+
|
131
|
+
new_routes = router.routes.keys
|
132
|
+
existing_routes = @routes.keys
|
133
|
+
|
134
|
+
raise ArgumentError, 'No routes to namespace' if new_routes.empty?
|
135
|
+
|
136
|
+
new_routes.each do |route|
|
137
|
+
ns_route = "#{prefix}/#{route}"
|
138
|
+
if existing_routes.include?(ns_route)
|
139
|
+
raise ArgumentError, 'Cannot merge duplicate routes: ' + ns_route
|
140
|
+
else
|
141
|
+
@routes[ns_route] = {
|
142
|
+
**router.routes[route],
|
143
|
+
handler: @middleware + router.routes[route][:handler] + @afterware,
|
144
|
+
timeout: router.routes[route].fetch('timeout', @timeout)
|
145
|
+
}
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def handle(request, context = {})
|
151
|
+
handle_request(@routes, request, context)
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
155
|
+
|
156
|
+
|
157
|
+
|
158
|
+
class Blest < Router
|
159
|
+
@options = nil
|
160
|
+
@errorhandler = nil
|
161
|
+
|
162
|
+
def initialize(options = nil)
|
163
|
+
@options = options
|
164
|
+
super(options)
|
165
|
+
end
|
166
|
+
|
167
|
+
def errorhandler(&errorhandler)
|
168
|
+
@errorhandler = errorhandler
|
169
|
+
puts 'The errorhandler method is not currently used'
|
170
|
+
end
|
171
|
+
|
172
|
+
def listen(*args)
|
173
|
+
request_handler = create_request_handler(@routes)
|
174
|
+
server = create_http_server(request_handler, @options)
|
175
|
+
server.call(*args)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
|
180
|
+
|
181
|
+
class HttpClient
|
182
|
+
attr_reader :queue, :futures
|
183
|
+
attr_accessor :url, :max_batch_size, :buffer_delay, :headers
|
184
|
+
|
185
|
+
def initialize(url, max_batch_size = 25, buffer_delay = 10, headers = {})
|
186
|
+
@url = url
|
187
|
+
@max_batch_size = max_batch_size
|
188
|
+
@buffer_delay = buffer_delay
|
189
|
+
@headers = headers
|
190
|
+
@queue = Queue.new
|
191
|
+
@futures = {}
|
192
|
+
@lock = Mutex.new
|
193
|
+
end
|
194
|
+
|
195
|
+
def request(route, parameters=nil, selector=nil)
|
196
|
+
uuid = SecureRandom.uuid
|
197
|
+
future = Concurrent::Promises.resolvable_future
|
198
|
+
@lock.synchronize do
|
199
|
+
@futures[uuid] = future
|
200
|
+
end
|
201
|
+
|
202
|
+
@queue.push({ uuid: uuid, data: [uuid, route, parameters, selector] })
|
203
|
+
process_timeout()
|
204
|
+
future
|
205
|
+
end
|
206
|
+
|
207
|
+
private
|
208
|
+
|
209
|
+
def process_timeout
|
210
|
+
Thread.new do
|
211
|
+
sleep @buffer_delay / 1000.0
|
212
|
+
process
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def process
|
217
|
+
until @queue.empty?
|
218
|
+
batch = []
|
219
|
+
batch << @queue.pop until batch.length >= @max_batch_size || @queue.empty?
|
220
|
+
|
221
|
+
unless batch.empty?
|
222
|
+
response = send_batch(batch)
|
223
|
+
process_response(response)
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
def send_batch(batch)
|
229
|
+
uri = URI(@url)
|
230
|
+
path = uri.path
|
231
|
+
path = '/' if uri.path.empty?
|
232
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
233
|
+
http.use_ssl = true if uri.scheme == 'https'
|
234
|
+
|
235
|
+
request = Net::HTTP::Post.new(path, @headers.merge({ 'Accept' => 'application/json', 'Content-Type' => 'application/json' }))
|
236
|
+
request.body = JSON.generate(batch.map { |item| item[:data] })
|
237
|
+
|
238
|
+
http.request(request)
|
239
|
+
end
|
240
|
+
|
241
|
+
def process_response(response)
|
242
|
+
if response.is_a?(Net::HTTPSuccess)
|
243
|
+
|
244
|
+
results = JSON.parse(response.body)
|
245
|
+
results.each do |result|
|
246
|
+
uuid = result[0]
|
247
|
+
data = result[2]
|
248
|
+
error = result[3]
|
249
|
+
future = nil
|
250
|
+
|
251
|
+
@lock.synchronize do
|
252
|
+
future = @futures.delete(uuid)
|
253
|
+
end
|
254
|
+
|
255
|
+
if future
|
256
|
+
if error
|
257
|
+
future.reject(error)
|
258
|
+
else
|
259
|
+
future.fulfill(data)
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
else
|
265
|
+
@lock.synchronize do
|
266
|
+
future = @futures.delete(uuid)
|
267
|
+
end
|
268
|
+
|
269
|
+
if future
|
270
|
+
error_message = "HTTP Error: #{response.code} - #{response.message}"
|
271
|
+
future.reject(error_message)
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
|
278
|
+
|
279
|
+
def get_value(hash, key)
|
280
|
+
if hash.key?(key.to_sym)
|
281
|
+
hash[key.to_sym]
|
282
|
+
elsif hash.key?(key.to_s)
|
283
|
+
hash[key.to_s]
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
|
288
|
+
|
289
|
+
class HttpServer
|
290
|
+
attr_reader :url, :host, :port, :cors, :headers
|
291
|
+
|
292
|
+
def initialize(request_handler, options = {})
|
293
|
+
unless request_handler.is_a?(Router)
|
294
|
+
raise ArgumentError, "request_handler must be an instance of Router class"
|
295
|
+
end
|
296
|
+
@request_handler = request_handler
|
297
|
+
if options
|
298
|
+
options_error = validate_server_options(options)
|
299
|
+
raise ArgumentError, options_error if options_error
|
300
|
+
else
|
301
|
+
@options = {}
|
302
|
+
end
|
303
|
+
|
304
|
+
@url = options && get_value(options, :url) || '/'
|
305
|
+
@host = options && get_value(options, :host) || 'localhost'
|
306
|
+
@port = options && get_value(options, :port) || 8080
|
307
|
+
@cors = options && get_value(options, :cors) || false
|
308
|
+
cors_default = cors == true ? '*' : cors || ''
|
309
|
+
|
310
|
+
@headers = {
|
311
|
+
'access-control-allow-origin' => options && get_value(options, :accessControlAllowOrigin) || cors_default,
|
312
|
+
'content-security-policy' => options && get_value(options, :contentSecurityPolicy) || "default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests",
|
313
|
+
'cross-origin-opener-policy' => options && get_value(options, :crossOriginOpenerPolicy) || 'same-origin',
|
314
|
+
'cross-origin-resource-policy' => options && get_value(options, :crossOriginResourcePolicy) || 'same-origin',
|
315
|
+
'origin-agent-cluster' => options && get_value(options, :originAgentCluster) || '?1',
|
316
|
+
'referrer-policy' => options && get_value(options, :referrerPolicy) || 'no-referrer',
|
317
|
+
'strict-transport-security' => options && get_value(options, :strictTransportSecurity) || 'max-age=1555200 includeSubDomains',
|
318
|
+
'x-content-type-options' => options && get_value(options, :xContentTypeOptions) || 'nosniff',
|
319
|
+
'x-dns-prefetch-control' => options && get_value(options, :xDnsPrefetchOptions) || 'off',
|
320
|
+
'x-download-options' => options && get_value(options, :xDownloadOptions) || 'noopen',
|
321
|
+
'x-frame-options' => options && get_value(options, :xFrameOptions) || 'SAMEORIGIN',
|
322
|
+
'x-permitted-cross-domain-policies' => options && get_value(options, :xPermittedCrossDomainPolicies) || 'none',
|
323
|
+
'x-xss-protection' => options && get_value(options, :xXssProtection) || '0'
|
324
|
+
}
|
325
|
+
end
|
326
|
+
|
327
|
+
def listen()
|
328
|
+
server = TCPServer.new(@host, @port)
|
329
|
+
puts "Server listening on port #{@port}"
|
330
|
+
|
331
|
+
loop do
|
332
|
+
client = server.accept
|
333
|
+
Thread.new { handle_http_request(client, @request_handler, @http_headers) }
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
private
|
338
|
+
|
339
|
+
def parse_http_request(request_line)
|
7
340
|
method, path, _ = request_line.split(' ')
|
8
341
|
{ method: method, path: path }
|
9
342
|
end
|
10
343
|
|
11
|
-
def
|
344
|
+
def parse_http_headers(client)
|
12
345
|
headers = {}
|
13
346
|
|
14
347
|
while (line = client.gets.chomp)
|
@@ -21,7 +354,7 @@ def create_http_server(request_handler, options = nil)
|
|
21
354
|
headers
|
22
355
|
end
|
23
356
|
|
24
|
-
def
|
357
|
+
def parse_post_body(client, content_length)
|
25
358
|
body = ''
|
26
359
|
|
27
360
|
while content_length > 0
|
@@ -33,7 +366,7 @@ def create_http_server(request_handler, options = nil)
|
|
33
366
|
body
|
34
367
|
end
|
35
368
|
|
36
|
-
def
|
369
|
+
def build_http_response(status, headers, body)
|
37
370
|
response = "HTTP/1.1 #{status}\r\n"
|
38
371
|
headers.each { |key, value| response += "#{key}: #{value}\r\n" }
|
39
372
|
response += "\r\n"
|
@@ -41,30 +374,24 @@ def create_http_server(request_handler, options = nil)
|
|
41
374
|
response
|
42
375
|
end
|
43
376
|
|
44
|
-
def
|
377
|
+
def handle_http_request(client, request_handler, http_headers)
|
45
378
|
request_line = client.gets
|
46
379
|
return unless request_line
|
47
380
|
|
48
|
-
request =
|
49
|
-
|
50
|
-
headers = parse_headers(client)
|
381
|
+
request = parse_http_request(request_line)
|
51
382
|
|
52
|
-
|
53
|
-
'Access-Control-Allow-Origin' => '*',
|
54
|
-
'Access-Control-Allow-Methods' => 'POST, OPTIONS',
|
55
|
-
'Access-Control-Allow-Headers' => 'Content-Type'
|
56
|
-
}
|
383
|
+
headers = parse_http_headers(client)
|
57
384
|
|
58
385
|
if request[:path] != '/'
|
59
|
-
response =
|
386
|
+
response = build_http_response('404 Not Found', http_headers, '')
|
60
387
|
client.print(response)
|
61
388
|
elsif request[:method] == 'OPTIONS'
|
62
|
-
response =
|
389
|
+
response = build_http_response('204 No Content', http_headers, '')
|
63
390
|
client.print(response)
|
64
391
|
elsif request[:method] == 'POST'
|
65
392
|
|
66
393
|
content_length = headers['Content-Length'].to_i
|
67
|
-
body =
|
394
|
+
body = parse_post_body(client, content_length)
|
68
395
|
|
69
396
|
begin
|
70
397
|
json_data = JSON.parse(body)
|
@@ -72,7 +399,7 @@ def create_http_server(request_handler, options = nil)
|
|
72
399
|
'headers' => headers
|
73
400
|
}
|
74
401
|
|
75
|
-
response_headers =
|
402
|
+
response_headers = http_headers.merge({
|
76
403
|
'Content-Type' => 'application/json'
|
77
404
|
})
|
78
405
|
|
@@ -80,202 +407,540 @@ def create_http_server(request_handler, options = nil)
|
|
80
407
|
|
81
408
|
if error
|
82
409
|
response_json = error.to_json
|
83
|
-
response =
|
410
|
+
response = build_http_response('500 Internal Server Error', response_headers, response_json)
|
84
411
|
client.print response
|
85
412
|
elsif result
|
86
413
|
response_json = result.to_json
|
87
|
-
response =
|
414
|
+
response = build_http_response('200 OK', response_headers, response_json)
|
88
415
|
client.print response
|
89
416
|
else
|
90
|
-
response =
|
417
|
+
response = build_http_response('500 Internal Server Error', response_headers, { 'message' => 'Request handler failed to return a result' }.to_json)
|
91
418
|
client.print response
|
92
419
|
end
|
93
420
|
|
94
421
|
rescue JSON::ParserError
|
95
|
-
response =
|
422
|
+
response = build_http_response('400 Bad Request', http_headers, '')
|
96
423
|
end
|
97
424
|
|
98
425
|
else
|
99
|
-
response =
|
426
|
+
response = build_http_response('405 Method Not Allowed', http_headers, '')
|
100
427
|
client.print(response)
|
101
428
|
end
|
102
429
|
client.close()
|
103
430
|
end
|
104
431
|
|
105
|
-
|
432
|
+
end
|
433
|
+
|
106
434
|
|
107
|
-
port = options&.fetch(:port, 8080) if options.is_a?(Hash)
|
108
|
-
port ||= 8080
|
109
435
|
|
110
|
-
server = TCPServer.new('localhost', 8080)
|
111
|
-
puts "Server listening on port #{port}"
|
112
436
|
|
113
|
-
loop do
|
114
|
-
client = server.accept
|
115
|
-
Thread.new { handle_request(client, request_handler) }
|
116
|
-
end
|
117
437
|
|
118
|
-
end
|
119
438
|
|
120
|
-
return run
|
121
|
-
end
|
122
439
|
|
123
|
-
def
|
440
|
+
def create_http_server(request_handler, options = nil)
|
124
441
|
if options
|
125
|
-
|
442
|
+
options_error = validate_server_options(options)
|
443
|
+
raise ArgumentError, options_error if options_error
|
444
|
+
end
|
445
|
+
|
446
|
+
url = options && get_value(options, :url) || '/'
|
447
|
+
port = options && get_value(options, :port) || 8080
|
448
|
+
cors = options && get_value(options, :cors) || false
|
449
|
+
cors_default = cors == true ? '*' : cors || ''
|
450
|
+
|
451
|
+
http_headers = {
|
452
|
+
'access-control-allow-origin' => options && get_value(options, :accessControlAllowOrigin) || cors_default,
|
453
|
+
'content-security-policy' => options && get_value(options, :contentSecurityPolicy) || "default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests",
|
454
|
+
'cross-origin-opener-policy' => options && get_value(options, :crossOriginOpenerPolicy) || 'same-origin',
|
455
|
+
'cross-origin-resource-policy' => options && get_value(options, :crossOriginResourcePolicy) || 'same-origin',
|
456
|
+
'origin-agent-cluster' => options && get_value(options, :originAgentCluster) || '?1',
|
457
|
+
'referrer-policy' => options && get_value(options, :referrerPolicy) || 'no-referrer',
|
458
|
+
'strict-transport-security' => options && get_value(options, :strictTransportSecurity) || 'max-age=1555200 includeSubDomains',
|
459
|
+
'x-content-type-options' => options && get_value(options, :xContentTypeOptions) || 'nosniff',
|
460
|
+
'x-dns-prefetch-control' => options && get_value(options, :xDnsPrefetchOptions) || 'off',
|
461
|
+
'x-download-options' => options && get_value(options, :xDownloadOptions) || 'noopen',
|
462
|
+
'x-frame-options' => options && get_value(options, :xFrameOptions) || 'SAMEORIGIN',
|
463
|
+
'x-permitted-cross-domain-policies' => options && get_value(options, :xPermittedCrossDomainPolicies) || 'none',
|
464
|
+
'x-xss-protection' => options && get_value(options, :xXssProtection) || '0'
|
465
|
+
}
|
466
|
+
|
467
|
+
def parse_http_request(request_line)
|
468
|
+
method, path, _ = request_line.split(' ')
|
469
|
+
{ method: method, path: path }
|
126
470
|
end
|
127
471
|
|
128
|
-
def
|
129
|
-
|
472
|
+
def parse_http_headers(client)
|
473
|
+
headers = {}
|
474
|
+
|
475
|
+
while (line = client.gets.chomp)
|
476
|
+
break if line.empty?
|
477
|
+
|
478
|
+
key, value = line.split(':', 2)
|
479
|
+
headers[key] = value.strip
|
480
|
+
end
|
481
|
+
|
482
|
+
headers
|
130
483
|
end
|
131
484
|
|
132
|
-
def
|
133
|
-
|
485
|
+
def parse_post_body(client, content_length)
|
486
|
+
body = ''
|
487
|
+
|
488
|
+
while content_length > 0
|
489
|
+
chunk = client.readpartial([content_length, 4096].min)
|
490
|
+
body += chunk
|
491
|
+
content_length -= chunk.length
|
492
|
+
end
|
493
|
+
|
494
|
+
body
|
134
495
|
end
|
135
496
|
|
136
|
-
def
|
137
|
-
|
497
|
+
def build_http_response(status, headers, body)
|
498
|
+
response = "HTTP/1.1 #{status}\r\n"
|
499
|
+
headers.each { |key, value| response += "#{key}: #{value}\r\n" }
|
500
|
+
response += "\r\n"
|
501
|
+
response += body
|
502
|
+
response
|
138
503
|
end
|
139
504
|
|
140
|
-
def
|
141
|
-
|
142
|
-
|
505
|
+
def handle_http_request(client, request_handler, http_headers)
|
506
|
+
request_line = client.gets
|
507
|
+
return unless request_line
|
508
|
+
|
509
|
+
request = parse_http_request(request_line)
|
510
|
+
|
511
|
+
headers = parse_http_headers(client)
|
512
|
+
|
513
|
+
if request[:path] != '/'
|
514
|
+
response = build_http_response('404 Not Found', http_headers, '')
|
515
|
+
client.print(response)
|
516
|
+
elsif request[:method] == 'OPTIONS'
|
517
|
+
response = build_http_response('204 No Content', http_headers, '')
|
518
|
+
client.print(response)
|
519
|
+
elsif request[:method] == 'POST'
|
520
|
+
|
521
|
+
content_length = headers['Content-Length'].to_i
|
522
|
+
body = parse_post_body(client, content_length)
|
523
|
+
|
524
|
+
begin
|
525
|
+
json_data = JSON.parse(body)
|
526
|
+
context = {
|
527
|
+
'headers' => headers
|
528
|
+
}
|
143
529
|
|
144
|
-
|
145
|
-
|
146
|
-
|
530
|
+
response_headers = http_headers.merge({
|
531
|
+
'Content-Type' => 'application/json'
|
532
|
+
})
|
147
533
|
|
148
|
-
|
149
|
-
|
534
|
+
result, error = request_handler.(json_data, context)
|
535
|
+
|
536
|
+
if error
|
537
|
+
response_json = error.to_json
|
538
|
+
response = build_http_response('500 Internal Server Error', response_headers, response_json)
|
539
|
+
client.print response
|
540
|
+
elsif result
|
541
|
+
response_json = result.to_json
|
542
|
+
response = build_http_response('200 OK', response_headers, response_json)
|
543
|
+
client.print response
|
150
544
|
else
|
151
|
-
|
545
|
+
response = build_http_response('500 Internal Server Error', response_headers, { 'message' => 'Request handler failed to return a result' }.to_json)
|
546
|
+
client.print response
|
152
547
|
end
|
548
|
+
|
549
|
+
rescue JSON::ParserError
|
550
|
+
response = build_http_response('400 Bad Request', http_headers, '')
|
153
551
|
end
|
552
|
+
|
154
553
|
else
|
155
|
-
|
554
|
+
response = build_http_response('405 Method Not Allowed', http_headers, '')
|
555
|
+
client.print(response)
|
156
556
|
end
|
557
|
+
client.close()
|
558
|
+
end
|
157
559
|
|
158
|
-
|
560
|
+
run = ->() do
|
159
561
|
|
160
|
-
|
161
|
-
|
162
|
-
rescue StandardError => error
|
163
|
-
return [request[:id], request[:route], nil, { message: error.message }]
|
164
|
-
end
|
562
|
+
server = TCPServer.new('localhost', port)
|
563
|
+
puts "Server listening on port #{port}"
|
165
564
|
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
if key.is_a?(String)
|
171
|
-
if obj.key?(key.to_sym)
|
172
|
-
filtered_obj[key.to_sym] = obj[key.to_sym]
|
173
|
-
end
|
174
|
-
elsif key.is_a?(Array)
|
175
|
-
nested_obj = obj[key[0].to_sym]
|
176
|
-
nested_arr = key[1]
|
177
|
-
if nested_obj.is_a?(Array)
|
178
|
-
filtered_arr = []
|
179
|
-
nested_obj.each do |nested_item|
|
180
|
-
filtered_nested_obj = filter_object(nested_item, nested_arr)
|
181
|
-
if filtered_nested_obj.keys.length > 0
|
182
|
-
filtered_arr << filtered_nested_obj
|
183
|
-
end
|
184
|
-
end
|
185
|
-
if filtered_arr.length > 0
|
186
|
-
filtered_obj[key[0].to_sym] = filtered_arr
|
187
|
-
end
|
188
|
-
elsif nested_obj.is_a?(Hash)
|
189
|
-
filtered_nested_obj = filter_object(nested_obj, nested_arr)
|
190
|
-
if filtered_nested_obj.keys.length > 0
|
191
|
-
filtered_obj[key[0].to_sym] = filtered_nested_obj
|
192
|
-
end
|
193
|
-
end
|
194
|
-
end
|
565
|
+
begin
|
566
|
+
loop do
|
567
|
+
client = server.accept
|
568
|
+
Thread.new { handle_http_request(client, request_handler, http_headers) }
|
195
569
|
end
|
196
|
-
|
570
|
+
rescue Interrupt
|
571
|
+
exit 1
|
197
572
|
end
|
198
|
-
|
573
|
+
|
199
574
|
end
|
200
575
|
|
201
|
-
|
576
|
+
return run
|
577
|
+
end
|
202
578
|
|
203
|
-
handler = ->(requests, context = {}) do
|
204
|
-
if !requests || !requests.is_a?(Array)
|
205
|
-
return handle_error(400, 'Request body should be a JSON array')
|
206
|
-
end
|
207
579
|
|
208
|
-
unique_ids = []
|
209
|
-
promises = []
|
210
580
|
|
211
|
-
|
212
|
-
|
213
|
-
|
581
|
+
def validate_server_options(options)
|
582
|
+
if options.nil? || options.empty?
|
583
|
+
return nil
|
584
|
+
elsif !options.is_a?(Hash)
|
585
|
+
return 'Options should be a hash'
|
586
|
+
else
|
587
|
+
if options.key?(:url)
|
588
|
+
if !options[:url].is_a?(String)
|
589
|
+
return '"url" option should be a string'
|
590
|
+
elsif !options[:url].start_with?('/')
|
591
|
+
return '"url" option should begin with a forward slash'
|
214
592
|
end
|
593
|
+
end
|
594
|
+
|
595
|
+
if options.key?(:cors)
|
596
|
+
if !options[:cors].is_a?(String) && !options[:cors].is_a?(TrueClass) && !options[:cors].is_a?(FalseClass)
|
597
|
+
return '"cors" option should be a string or boolean'
|
598
|
+
end
|
599
|
+
end
|
600
|
+
end
|
601
|
+
|
602
|
+
return nil
|
603
|
+
end
|
604
|
+
|
215
605
|
|
216
|
-
id = request[0]
|
217
|
-
route = request[1]
|
218
|
-
parameters = request[2] || nil
|
219
|
-
selector = request[3] || nil
|
220
606
|
|
221
|
-
|
222
|
-
|
607
|
+
def create_request_handler(routes)
|
608
|
+
raise ArgumentError, 'A routes object is required' unless routes.is_a?(Hash)
|
609
|
+
|
610
|
+
my_routes = {}
|
611
|
+
|
612
|
+
routes.each do |key, route|
|
613
|
+
route_error = validate_route(key)
|
614
|
+
raise ArgumentError, "#{route_error}: #{key}" if route_error
|
615
|
+
|
616
|
+
if route.is_a?(Array)
|
617
|
+
raise ArgumentError, "Route has no handlers: #{key}" if route.empty?
|
618
|
+
|
619
|
+
route.each do |handler|
|
620
|
+
raise ArgumentError, "All route handlers must be functions: #{key}" unless handler.is_a?(Proc)
|
223
621
|
end
|
224
622
|
|
225
|
-
|
226
|
-
|
623
|
+
my_routes[key] = { handler: route }
|
624
|
+
elsif route.is_a?(Hash)
|
625
|
+
unless route.key?(:handler)
|
626
|
+
raise ArgumentError, "Route has no handlers: #{key}"
|
227
627
|
end
|
228
628
|
|
229
|
-
if
|
230
|
-
|
231
|
-
|
232
|
-
return handle_error(400, 'Request item route should be at least two characters long')
|
233
|
-
elsif route[-1] == '/'
|
234
|
-
return handle_error(400, 'Request item route should not end in a forward slash')
|
235
|
-
elsif !/[a-zA-Z]/.match?(route[0])
|
236
|
-
return handle_error(400, 'Request item route should start with a letter')
|
237
|
-
else
|
238
|
-
return handle_error(
|
239
|
-
400,
|
240
|
-
'Request item route should contain only letters, numbers, dashes, underscores, and forward slashes'
|
241
|
-
)
|
629
|
+
if route[:handler].is_a?(Array)
|
630
|
+
route[:handler].each do |handler|
|
631
|
+
raise ArgumentError, "All route handlers must be functions: #{key}" unless handler.is_a?(Proc)
|
242
632
|
end
|
243
|
-
end
|
244
633
|
|
245
|
-
|
246
|
-
|
634
|
+
my_routes[key] = route
|
635
|
+
elsif route[:handler].is_a?(Proc)
|
636
|
+
my_routes[key] = { **route, handler: [route[:handler]] }
|
637
|
+
else
|
638
|
+
raise ArgumentError, "Route handler is not valid: #{key}"
|
247
639
|
end
|
640
|
+
elsif route.is_a?(Proc)
|
641
|
+
my_routes[key] = { handler: [route] }
|
642
|
+
else
|
643
|
+
raise ArgumentError, "Route is missing handler: #{key}"
|
644
|
+
end
|
645
|
+
end
|
646
|
+
|
647
|
+
handler = lambda do |requests, context = {}|
|
648
|
+
handle_request(my_routes, requests, context)
|
649
|
+
end
|
650
|
+
|
651
|
+
return handler
|
652
|
+
end
|
653
|
+
|
654
|
+
|
655
|
+
|
656
|
+
def validate_route(route)
|
657
|
+
route_regex = /^[a-zA-Z][a-zA-Z0-9_\-\/]*[a-zA-Z0-9]$/
|
658
|
+
if route.nil? || route.empty?
|
659
|
+
return 'Route is required'
|
660
|
+
elsif !(route =~ route_regex)
|
661
|
+
route_length = route.length
|
662
|
+
if route_length < 2
|
663
|
+
return 'Route should be at least two characters long'
|
664
|
+
elsif route[-1] == '/'
|
665
|
+
return 'Route should not end in a forward slash'
|
666
|
+
elsif !(route[0] =~ /^[a-zA-Z]/)
|
667
|
+
return 'Route should start with a letter'
|
668
|
+
elsif !(route[-1] =~ /^[a-zA-Z0-9]/)
|
669
|
+
return 'Route should end with a letter or a number'
|
670
|
+
else
|
671
|
+
return 'Route should contain only letters, numbers, dashes, underscores, and forward slashes'
|
672
|
+
end
|
673
|
+
elsif route =~ /\/[^a-zA-Z]/
|
674
|
+
return 'Sub-routes should start with a letter'
|
675
|
+
elsif route =~ /[^a-zA-Z0-9]\//
|
676
|
+
return 'Sub-routes should end with a letter or a number'
|
677
|
+
elsif route =~ /\/[a-zA-Z0-9_\-]{0,1}\//
|
678
|
+
return 'Sub-routes should be at least two characters long'
|
679
|
+
elsif route =~ /\/[a-zA-Z0-9_\-]$/
|
680
|
+
return 'Sub-routes should be at least two characters long'
|
681
|
+
elsif route =~ /^[a-zA-Z0-9_\-]\//
|
682
|
+
return 'Sub-routes should be at least two characters long'
|
683
|
+
end
|
684
|
+
|
685
|
+
return nil
|
686
|
+
end
|
687
|
+
|
688
|
+
|
689
|
+
|
690
|
+
def handle_request(routes, requests, context = {})
|
691
|
+
if requests.nil? || !requests.is_a?(Array)
|
692
|
+
return handle_error(400, 'Request should be an array')
|
693
|
+
end
|
248
694
|
|
249
|
-
|
250
|
-
|
695
|
+
unique_ids = []
|
696
|
+
promises = []
|
697
|
+
|
698
|
+
requests.each do |request|
|
699
|
+
request_length = request.length
|
700
|
+
if !request.is_a?(Array)
|
701
|
+
return handle_error(400, 'Request item should be an array')
|
702
|
+
end
|
703
|
+
|
704
|
+
id = request[0] || nil
|
705
|
+
route = request[1] || nil
|
706
|
+
parameters = request[2] || nil
|
707
|
+
selector = request[3] || nil
|
708
|
+
|
709
|
+
if id.nil? || !id.is_a?(String)
|
710
|
+
return handle_error(400, 'Request item should have an ID')
|
711
|
+
end
|
712
|
+
|
713
|
+
if route.nil? || !route.is_a?(String)
|
714
|
+
return handle_error(400, 'Request items should have a route')
|
715
|
+
end
|
716
|
+
|
717
|
+
if parameters && !parameters.is_a?(Hash)
|
718
|
+
return handle_error(400, 'Request item parameters should be an object')
|
719
|
+
end
|
720
|
+
|
721
|
+
if selector && !selector.is_a?(Array)
|
722
|
+
return handle_error(400, 'Request item selector should be an array')
|
723
|
+
end
|
724
|
+
|
725
|
+
if unique_ids.include?(id)
|
726
|
+
return handle_error(400, 'Request items should have unique IDs')
|
727
|
+
end
|
728
|
+
|
729
|
+
unique_ids << id
|
730
|
+
this_route = routes[route]
|
731
|
+
route_handler = nil
|
732
|
+
timeout = nil
|
733
|
+
|
734
|
+
if this_route.is_a?(Hash)
|
735
|
+
route_handler = this_route[:handler] || this_route['handler'] || method(:route_not_found)
|
736
|
+
timeout = this_route[:timeout] || this_route['timeout'] || nil
|
737
|
+
else
|
738
|
+
route_handler = this_route || method(:route_not_found)
|
739
|
+
end
|
740
|
+
|
741
|
+
request_object = {
|
742
|
+
id: id,
|
743
|
+
route: route,
|
744
|
+
parameters: parameters || {},
|
745
|
+
selector: selector
|
746
|
+
}
|
747
|
+
|
748
|
+
my_context = {
|
749
|
+
'requestId' => id,
|
750
|
+
'routeName' => route,
|
751
|
+
'selector' => selector,
|
752
|
+
'requestTime' => DateTime.now.to_time.to_i
|
753
|
+
}
|
754
|
+
if context.is_a?(Hash)
|
755
|
+
my_context = my_context.merge(context)
|
756
|
+
end
|
757
|
+
|
758
|
+
promises << Thread.new { route_reducer(route_handler, request_object, my_context, timeout) }
|
759
|
+
end
|
760
|
+
|
761
|
+
results = promises.map(&:value)
|
762
|
+
return handle_result(results)
|
763
|
+
end
|
764
|
+
|
765
|
+
|
766
|
+
|
767
|
+
def handle_result(result)
|
768
|
+
return result, nil
|
769
|
+
end
|
770
|
+
|
771
|
+
|
772
|
+
|
773
|
+
def handle_error(status, message)
|
774
|
+
return nil, {
|
775
|
+
'status' => status,
|
776
|
+
'message' => message
|
777
|
+
}
|
778
|
+
end
|
779
|
+
|
780
|
+
|
781
|
+
|
782
|
+
class BlestError < StandardError
|
783
|
+
attr_accessor :status
|
784
|
+
attr_accessor :code
|
785
|
+
attr_accessor :data
|
786
|
+
attr_accessor :stack
|
787
|
+
|
788
|
+
def initialize(message = nil)
|
789
|
+
@status = 500
|
790
|
+
@code = nil
|
791
|
+
@data = nil
|
792
|
+
@stack = nil
|
793
|
+
super(message)
|
794
|
+
end
|
795
|
+
end
|
796
|
+
|
797
|
+
|
798
|
+
|
799
|
+
def route_not_found(_, _)
|
800
|
+
error = BlestError.new
|
801
|
+
error.status = 404
|
802
|
+
error.stack = nil
|
803
|
+
raise error, 'Not Found'
|
804
|
+
end
|
805
|
+
|
806
|
+
|
807
|
+
|
808
|
+
def route_reducer(handler, request, context, timeout = nil)
|
809
|
+
safe_context = Marshal.load(Marshal.dump(context))
|
810
|
+
route = request[:route]
|
811
|
+
result = nil
|
812
|
+
error = nil
|
813
|
+
|
814
|
+
target = -> do
|
815
|
+
my_result = nil
|
816
|
+
if handler.is_a?(Array)
|
817
|
+
handler.each do |h|
|
818
|
+
break if error
|
819
|
+
temp_result = nil
|
820
|
+
if h.respond_to?(:call)
|
821
|
+
temp_result = Concurrent::Promises.future do
|
822
|
+
begin
|
823
|
+
h.call(request[:parameters], safe_context)
|
824
|
+
rescue => e
|
825
|
+
error = e
|
826
|
+
end
|
827
|
+
end.value
|
828
|
+
else
|
829
|
+
puts "Tried to resolve route '#{route}' with handler of type '#{h.class}'"
|
830
|
+
raise StandardError
|
831
|
+
end
|
832
|
+
|
833
|
+
if temp_result && temp_result != nil
|
834
|
+
if my_result && my_result != nil
|
835
|
+
puts result
|
836
|
+
puts temp_result
|
837
|
+
puts "Multiple handlers on the route '#{route}' returned results"
|
838
|
+
raise StandardError
|
839
|
+
else
|
840
|
+
my_result = temp_result
|
841
|
+
end
|
842
|
+
end
|
843
|
+
end
|
844
|
+
else
|
845
|
+
if handler.respond_to?(:call)
|
846
|
+
my_result = Concurrent::Promises.future do
|
847
|
+
begin
|
848
|
+
handler.call(request[:parameters], safe_context)
|
849
|
+
rescue => e
|
850
|
+
error = e
|
851
|
+
end
|
852
|
+
end.value
|
853
|
+
else
|
854
|
+
puts "Tried to resolve route '#{route}' with handler of type '#{handler.class}'"
|
855
|
+
raise StandardError
|
251
856
|
end
|
857
|
+
end
|
858
|
+
|
859
|
+
if error
|
860
|
+
raise error
|
861
|
+
end
|
862
|
+
|
863
|
+
my_result
|
864
|
+
end
|
252
865
|
|
253
|
-
|
254
|
-
|
866
|
+
begin
|
867
|
+
if timeout && timeout > 0
|
868
|
+
begin
|
869
|
+
result = Timeout.timeout(timeout / 1000.0) { target.call }
|
870
|
+
rescue Timeout::Error
|
871
|
+
puts "The route '#{route}' timed out after #{timeout} milliseconds"
|
872
|
+
return [request[:id], request[:route], nil, { 'message' => 'Internal Server Error', 'status' => 500 }]
|
255
873
|
end
|
874
|
+
else
|
875
|
+
result = target.call
|
876
|
+
end
|
256
877
|
|
257
|
-
|
878
|
+
if result.nil? || !result.is_a?(Hash)
|
879
|
+
puts "The route '#{route}' did not return a result object"
|
880
|
+
return [request[:id], request[:route], nil, { 'message' => 'Internal Server Error', 'status' => 500 }]
|
881
|
+
end
|
258
882
|
|
259
|
-
|
883
|
+
if request[:selector]
|
884
|
+
result = filter_object(result, request[:selector])
|
885
|
+
end
|
260
886
|
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
887
|
+
[request[:id], request[:route], result, nil]
|
888
|
+
rescue => error
|
889
|
+
puts error.backtrace
|
890
|
+
response_error = {
|
891
|
+
'message' => error.message || 'Internal Server Error',
|
892
|
+
'status' => error.respond_to?(:status) ? error.status : 500
|
893
|
+
}
|
267
894
|
|
268
|
-
|
895
|
+
if error.respond_to?(:code) && error.code.is_a?(String)
|
896
|
+
response_error['code'] = error.code
|
269
897
|
end
|
270
898
|
|
271
|
-
|
899
|
+
if error.respond_to?(:data) && error.data.is_a?(Hash)
|
900
|
+
response_error['data'] = error.data
|
901
|
+
end
|
272
902
|
|
273
|
-
|
274
|
-
|
903
|
+
if ENV['ENVIRONMENT'] != 'production' && ENV['APP_ENV'] != 'production' && ENV['RACK_ENV'] != 'production' && ENV['RAILS_ENV'] != 'production' && !error.respond_to?(:stack)
|
904
|
+
response_error['stack'] = error.backtrace
|
275
905
|
end
|
276
906
|
|
277
|
-
|
907
|
+
[request[:id], request[:route], nil, response_error]
|
278
908
|
end
|
909
|
+
end
|
279
910
|
|
280
|
-
|
911
|
+
|
912
|
+
|
913
|
+
def filter_object(obj, arr)
|
914
|
+
if arr.is_a?(Array)
|
915
|
+
filtered_obj = {}
|
916
|
+
arr.each do |key|
|
917
|
+
if key.is_a?(String)
|
918
|
+
if obj.key?(key.to_sym)
|
919
|
+
filtered_obj[key.to_sym] = obj[key.to_sym]
|
920
|
+
end
|
921
|
+
elsif key.is_a?(Array)
|
922
|
+
nested_obj = obj[key[0].to_sym]
|
923
|
+
nested_arr = key[1]
|
924
|
+
if nested_obj.is_a?(Array)
|
925
|
+
filtered_arr = []
|
926
|
+
nested_obj.each do |nested_item|
|
927
|
+
filtered_nested_obj = filter_object(nested_item, nested_arr)
|
928
|
+
if filtered_nested_obj.keys.length > 0
|
929
|
+
filtered_arr << filtered_nested_obj
|
930
|
+
end
|
931
|
+
end
|
932
|
+
if filtered_arr.length > 0
|
933
|
+
filtered_obj[key[0].to_sym] = filtered_arr
|
934
|
+
end
|
935
|
+
elsif nested_obj.is_a?(Hash)
|
936
|
+
filtered_nested_obj = filter_object(nested_obj, nested_arr)
|
937
|
+
if filtered_nested_obj.keys.length > 0
|
938
|
+
filtered_obj[key[0].to_sym] = filtered_nested_obj
|
939
|
+
end
|
940
|
+
end
|
941
|
+
end
|
942
|
+
end
|
943
|
+
return filtered_obj
|
944
|
+
end
|
945
|
+
return obj
|
281
946
|
end
|