blest 1.0.0 → 1.0.2
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/LICENSE +1 -1
- data/README.md +5 -40
- data/lib/blest.rb +3 -344
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: eb15eb80f38f62587b534d5ce5e5a81a0a8995050033745b2f100fede9557794
|
4
|
+
data.tar.gz: b7e758f3d3396af0f14952b3512c5e2550f7a35089add6aaf479994bb4ed778a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3c18640e749fb37f65d380ac18966002662b652081bafb908e9e7a1771dbae30348272888ae60bdf4f254fefeec6f4c2c822b2fc47341a4469312413c03f45d9
|
7
|
+
data.tar.gz: 731fedd0800a8e82a9ab3d7bd07e1bd6b29b88145ce8d43054f7d52b2519a6e46f8472273c8bfa85b5aaf7451d6821b19c698a3f97319b823b7c78947a47bbfd
|
data/LICENSE
CHANGED
data/README.md
CHANGED
@@ -24,39 +24,9 @@ gem install blest
|
|
24
24
|
|
25
25
|
## Usage
|
26
26
|
|
27
|
-
The `Blest` class of this library has an interface similar to Sinatra. It also provides a `Router` class with a `handle` method for use in an existing Ruby API and an `HttpClient` class with a `request` method for making BLEST HTTP requests.
|
28
|
-
|
29
|
-
```ruby
|
30
|
-
require 'blest'
|
31
|
-
|
32
|
-
app = Blest.new(timeout: 1000, port: 8080, host: 'localhost', cors: 'http://localhost:3000')
|
33
|
-
|
34
|
-
# Create some middleware (optional)
|
35
|
-
app.before do |body, context|
|
36
|
-
if context.dig('headers', 'auth') == 'myToken'?
|
37
|
-
context['user'] = {
|
38
|
-
# user info for example
|
39
|
-
}
|
40
|
-
nil
|
41
|
-
else
|
42
|
-
raise RuntimeError, "Unauthorized"
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
# Create a route controller
|
47
|
-
app.route('greet') do |body, context|
|
48
|
-
{
|
49
|
-
greeting: "Hi, #{body['name']}!"
|
50
|
-
}
|
51
|
-
end
|
52
|
-
|
53
|
-
# Start the server
|
54
|
-
app.listen
|
55
|
-
```
|
56
|
-
|
57
27
|
### Router
|
58
28
|
|
59
|
-
The following example uses Sinatra.
|
29
|
+
The following example uses Sinatra, but you can find examples with other frameworks [here](examples).
|
60
30
|
|
61
31
|
```ruby
|
62
32
|
require 'sinatra'
|
@@ -68,14 +38,9 @@ router = Router.new(timeout: 1000)
|
|
68
38
|
|
69
39
|
# Create some middleware (optional)
|
70
40
|
router.before do |body, context|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
}
|
75
|
-
nil
|
76
|
-
else
|
77
|
-
raise RuntimeError, "Unauthorized"
|
78
|
-
end
|
41
|
+
context['user'] = {
|
42
|
+
# user info for example
|
43
|
+
}
|
79
44
|
end
|
80
45
|
|
81
46
|
# Create a route controller
|
@@ -111,7 +76,7 @@ client = HttpClient.new('http://localhost:8080', max_batch_size = 25, buffer_del
|
|
111
76
|
|
112
77
|
# Send a request
|
113
78
|
begin
|
114
|
-
result = client.request('greet', { 'name': 'Steve' }
|
79
|
+
result = client.request('greet', { 'name': 'Steve' }).value
|
115
80
|
# Do something with the result
|
116
81
|
rescue => error
|
117
82
|
# Do something in case of error
|
data/lib/blest.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
require 'socket'
|
2
1
|
require 'json'
|
3
2
|
require 'concurrent'
|
4
3
|
require 'securerandom'
|
@@ -148,29 +147,6 @@ end
|
|
148
147
|
|
149
148
|
|
150
149
|
|
151
|
-
class Blest < Router
|
152
|
-
@options = nil
|
153
|
-
@errorhandler = nil
|
154
|
-
|
155
|
-
def initialize(options = nil)
|
156
|
-
@options = options
|
157
|
-
super(options)
|
158
|
-
end
|
159
|
-
|
160
|
-
def errorhandler(&errorhandler)
|
161
|
-
@errorhandler = errorhandler
|
162
|
-
puts 'The errorhandler method is not currently used'
|
163
|
-
end
|
164
|
-
|
165
|
-
def listen(*args)
|
166
|
-
request_handler = create_request_handler(@routes)
|
167
|
-
server = create_http_server(request_handler, @options)
|
168
|
-
server.call(*args)
|
169
|
-
end
|
170
|
-
end
|
171
|
-
|
172
|
-
|
173
|
-
|
174
150
|
class HttpClient
|
175
151
|
attr_reader :queue, :futures
|
176
152
|
attr_accessor :url, :max_batch_size, :buffer_delay, :headers
|
@@ -279,324 +255,6 @@ end
|
|
279
255
|
|
280
256
|
|
281
257
|
|
282
|
-
class HttpServer
|
283
|
-
attr_reader :url, :host, :port, :cors, :headers
|
284
|
-
|
285
|
-
def initialize(request_handler, options = {})
|
286
|
-
unless request_handler.is_a?(Router)
|
287
|
-
raise ArgumentError, "request_handler must be an instance of Router class"
|
288
|
-
end
|
289
|
-
@request_handler = request_handler
|
290
|
-
if options
|
291
|
-
options_error = validate_server_options(options)
|
292
|
-
raise ArgumentError, options_error if options_error
|
293
|
-
else
|
294
|
-
@options = {}
|
295
|
-
end
|
296
|
-
|
297
|
-
@url = options && get_value(options, :url) || '/'
|
298
|
-
@host = options && get_value(options, :host) || 'localhost'
|
299
|
-
@port = options && get_value(options, :port) || 8080
|
300
|
-
@cors = options && get_value(options, :cors) || false
|
301
|
-
cors_default = cors == true ? '*' : cors || ''
|
302
|
-
|
303
|
-
@headers = {
|
304
|
-
'access-control-allow-origin' => options && get_value(options, :accessControlAllowOrigin) || cors_default,
|
305
|
-
'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",
|
306
|
-
'cross-origin-opener-policy' => options && get_value(options, :crossOriginOpenerPolicy) || 'same-origin',
|
307
|
-
'cross-origin-resource-policy' => options && get_value(options, :crossOriginResourcePolicy) || 'same-origin',
|
308
|
-
'origin-agent-cluster' => options && get_value(options, :originAgentCluster) || '?1',
|
309
|
-
'referrer-policy' => options && get_value(options, :referrerPolicy) || 'no-referrer',
|
310
|
-
'strict-transport-security' => options && get_value(options, :strictTransportSecurity) || 'max-age=1555200 includeSubDomains',
|
311
|
-
'x-content-type-options' => options && get_value(options, :xContentTypeOptions) || 'nosniff',
|
312
|
-
'x-dns-prefetch-control' => options && get_value(options, :xDnsPrefetchOptions) || 'off',
|
313
|
-
'x-download-options' => options && get_value(options, :xDownloadOptions) || 'noopen',
|
314
|
-
'x-frame-options' => options && get_value(options, :xFrameOptions) || 'SAMEORIGIN',
|
315
|
-
'x-permitted-cross-domain-policies' => options && get_value(options, :xPermittedCrossDomainPolicies) || 'none',
|
316
|
-
'x-xss-protection' => options && get_value(options, :xXssProtection) || '0'
|
317
|
-
}
|
318
|
-
end
|
319
|
-
|
320
|
-
def listen()
|
321
|
-
server = TCPServer.new(@host, @port)
|
322
|
-
puts "Server listening on port #{@port}"
|
323
|
-
|
324
|
-
loop do
|
325
|
-
client = server.accept
|
326
|
-
Thread.new { handle_http_request(client, @request_handler, @http_headers) }
|
327
|
-
end
|
328
|
-
end
|
329
|
-
|
330
|
-
private
|
331
|
-
|
332
|
-
def parse_http_request(request_line)
|
333
|
-
method, path, _ = request_line.split(' ')
|
334
|
-
{ method: method, path: path }
|
335
|
-
end
|
336
|
-
|
337
|
-
def parse_http_headers(client)
|
338
|
-
headers = {}
|
339
|
-
|
340
|
-
while (line = client.gets.chomp)
|
341
|
-
break if line.empty?
|
342
|
-
|
343
|
-
key, value = line.split(':', 2)
|
344
|
-
headers[key] = value.strip
|
345
|
-
end
|
346
|
-
|
347
|
-
headers
|
348
|
-
end
|
349
|
-
|
350
|
-
def parse_post_body(client, content_length)
|
351
|
-
body = ''
|
352
|
-
|
353
|
-
while content_length > 0
|
354
|
-
chunk = client.readpartial([content_length, 4096].min)
|
355
|
-
body += chunk
|
356
|
-
content_length -= chunk.length
|
357
|
-
end
|
358
|
-
|
359
|
-
body
|
360
|
-
end
|
361
|
-
|
362
|
-
def build_http_response(status, headers, body)
|
363
|
-
response = "HTTP/1.1 #{status}\r\n"
|
364
|
-
headers.each { |key, value| response += "#{key}: #{value}\r\n" }
|
365
|
-
response += "\r\n"
|
366
|
-
response += body
|
367
|
-
response
|
368
|
-
end
|
369
|
-
|
370
|
-
def handle_http_request(client, request_handler, http_headers)
|
371
|
-
request_line = client.gets
|
372
|
-
return unless request_line
|
373
|
-
|
374
|
-
request = parse_http_request(request_line)
|
375
|
-
|
376
|
-
headers = parse_http_headers(client)
|
377
|
-
|
378
|
-
if request[:path] != '/'
|
379
|
-
response = build_http_response('404 Not Found', http_headers, '')
|
380
|
-
client.print(response)
|
381
|
-
elsif request[:method] == 'OPTIONS'
|
382
|
-
response = build_http_response('204 No Content', http_headers, '')
|
383
|
-
client.print(response)
|
384
|
-
elsif request[:method] == 'POST'
|
385
|
-
|
386
|
-
content_length = headers['Content-Length'].to_i
|
387
|
-
body = parse_post_body(client, content_length)
|
388
|
-
|
389
|
-
begin
|
390
|
-
json_data = JSON.parse(body)
|
391
|
-
context = {
|
392
|
-
'headers' => headers
|
393
|
-
}
|
394
|
-
|
395
|
-
response_headers = http_headers.merge({
|
396
|
-
'Content-Type' => 'application/json'
|
397
|
-
})
|
398
|
-
|
399
|
-
result, error = request_handler.(json_data, context)
|
400
|
-
|
401
|
-
if error
|
402
|
-
response_json = error.to_json
|
403
|
-
response = build_http_response('500 Internal Server Error', response_headers, response_json)
|
404
|
-
client.print response
|
405
|
-
elsif result
|
406
|
-
response_json = result.to_json
|
407
|
-
response = build_http_response('200 OK', response_headers, response_json)
|
408
|
-
client.print response
|
409
|
-
else
|
410
|
-
response = build_http_response('500 Internal Server Error', response_headers, { 'message' => 'Request handler failed to return a result' }.to_json)
|
411
|
-
client.print response
|
412
|
-
end
|
413
|
-
|
414
|
-
rescue JSON::ParserError
|
415
|
-
response = build_http_response('400 Bad Request', http_headers, '')
|
416
|
-
end
|
417
|
-
|
418
|
-
else
|
419
|
-
response = build_http_response('405 Method Not Allowed', http_headers, '')
|
420
|
-
client.print(response)
|
421
|
-
end
|
422
|
-
client.close()
|
423
|
-
end
|
424
|
-
|
425
|
-
end
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
def create_http_server(request_handler, options = nil)
|
434
|
-
if options
|
435
|
-
options_error = validate_server_options(options)
|
436
|
-
raise ArgumentError, options_error if options_error
|
437
|
-
end
|
438
|
-
|
439
|
-
url = options && get_value(options, :url) || '/'
|
440
|
-
port = options && get_value(options, :port) || 8080
|
441
|
-
cors = options && get_value(options, :cors) || false
|
442
|
-
cors_default = cors == true ? '*' : cors || ''
|
443
|
-
|
444
|
-
http_headers = {
|
445
|
-
'access-control-allow-origin' => options && get_value(options, :accessControlAllowOrigin) || cors_default,
|
446
|
-
'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",
|
447
|
-
'cross-origin-opener-policy' => options && get_value(options, :crossOriginOpenerPolicy) || 'same-origin',
|
448
|
-
'cross-origin-resource-policy' => options && get_value(options, :crossOriginResourcePolicy) || 'same-origin',
|
449
|
-
'origin-agent-cluster' => options && get_value(options, :originAgentCluster) || '?1',
|
450
|
-
'referrer-policy' => options && get_value(options, :referrerPolicy) || 'no-referrer',
|
451
|
-
'strict-transport-security' => options && get_value(options, :strictTransportSecurity) || 'max-age=1555200 includeSubDomains',
|
452
|
-
'x-content-type-options' => options && get_value(options, :xContentTypeOptions) || 'nosniff',
|
453
|
-
'x-dns-prefetch-control' => options && get_value(options, :xDnsPrefetchOptions) || 'off',
|
454
|
-
'x-download-options' => options && get_value(options, :xDownloadOptions) || 'noopen',
|
455
|
-
'x-frame-options' => options && get_value(options, :xFrameOptions) || 'SAMEORIGIN',
|
456
|
-
'x-permitted-cross-domain-policies' => options && get_value(options, :xPermittedCrossDomainPolicies) || 'none',
|
457
|
-
'x-xss-protection' => options && get_value(options, :xXssProtection) || '0'
|
458
|
-
}
|
459
|
-
|
460
|
-
def parse_http_request(request_line)
|
461
|
-
method, path, _ = request_line.split(' ')
|
462
|
-
{ method: method, path: path }
|
463
|
-
end
|
464
|
-
|
465
|
-
def parse_http_headers(client)
|
466
|
-
headers = {}
|
467
|
-
|
468
|
-
while (line = client.gets.chomp)
|
469
|
-
break if line.empty?
|
470
|
-
|
471
|
-
key, value = line.split(':', 2)
|
472
|
-
headers[key] = value.strip
|
473
|
-
end
|
474
|
-
|
475
|
-
headers
|
476
|
-
end
|
477
|
-
|
478
|
-
def parse_post_body(client, content_length)
|
479
|
-
body = ''
|
480
|
-
|
481
|
-
while content_length > 0
|
482
|
-
chunk = client.readpartial([content_length, 4096].min)
|
483
|
-
body += chunk
|
484
|
-
content_length -= chunk.length
|
485
|
-
end
|
486
|
-
|
487
|
-
body
|
488
|
-
end
|
489
|
-
|
490
|
-
def build_http_response(status, headers, body)
|
491
|
-
response = "HTTP/1.1 #{status}\r\n"
|
492
|
-
headers.each { |key, value| response += "#{key}: #{value}\r\n" }
|
493
|
-
response += "\r\n"
|
494
|
-
response += body
|
495
|
-
response
|
496
|
-
end
|
497
|
-
|
498
|
-
def handle_http_request(client, request_handler, http_headers)
|
499
|
-
request_line = client.gets
|
500
|
-
return unless request_line
|
501
|
-
|
502
|
-
request = parse_http_request(request_line)
|
503
|
-
|
504
|
-
headers = parse_http_headers(client)
|
505
|
-
|
506
|
-
if request[:path] != '/'
|
507
|
-
response = build_http_response('404 Not Found', http_headers, '')
|
508
|
-
client.print(response)
|
509
|
-
elsif request[:method] == 'OPTIONS'
|
510
|
-
response = build_http_response('204 No Content', http_headers, '')
|
511
|
-
client.print(response)
|
512
|
-
elsif request[:method] == 'POST'
|
513
|
-
|
514
|
-
content_length = headers['Content-Length'].to_i
|
515
|
-
body = parse_post_body(client, content_length)
|
516
|
-
|
517
|
-
begin
|
518
|
-
json_data = JSON.parse(body)
|
519
|
-
context = {
|
520
|
-
'headers' => headers
|
521
|
-
}
|
522
|
-
|
523
|
-
response_headers = http_headers.merge({
|
524
|
-
'Content-Type' => 'application/json'
|
525
|
-
})
|
526
|
-
|
527
|
-
result, error = request_handler.(json_data, context)
|
528
|
-
|
529
|
-
if error
|
530
|
-
response_json = error.to_json
|
531
|
-
response = build_http_response('500 Internal Server Error', response_headers, response_json)
|
532
|
-
client.print response
|
533
|
-
elsif result
|
534
|
-
response_json = result.to_json
|
535
|
-
response = build_http_response('200 OK', response_headers, response_json)
|
536
|
-
client.print response
|
537
|
-
else
|
538
|
-
response = build_http_response('500 Internal Server Error', response_headers, { 'message' => 'Request handler failed to return a result' }.to_json)
|
539
|
-
client.print response
|
540
|
-
end
|
541
|
-
|
542
|
-
rescue JSON::ParserError
|
543
|
-
response = build_http_response('400 Bad Request', http_headers, '')
|
544
|
-
end
|
545
|
-
|
546
|
-
else
|
547
|
-
response = build_http_response('405 Method Not Allowed', http_headers, '')
|
548
|
-
client.print(response)
|
549
|
-
end
|
550
|
-
client.close()
|
551
|
-
end
|
552
|
-
|
553
|
-
run = ->() do
|
554
|
-
|
555
|
-
server = TCPServer.new('localhost', port)
|
556
|
-
puts "Server listening on port #{port}"
|
557
|
-
|
558
|
-
begin
|
559
|
-
loop do
|
560
|
-
client = server.accept
|
561
|
-
Thread.new { handle_http_request(client, request_handler, http_headers) }
|
562
|
-
end
|
563
|
-
rescue Interrupt
|
564
|
-
exit 1
|
565
|
-
end
|
566
|
-
|
567
|
-
end
|
568
|
-
|
569
|
-
return run
|
570
|
-
end
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
def validate_server_options(options)
|
575
|
-
if options.nil? || options.empty?
|
576
|
-
return nil
|
577
|
-
elsif !options.is_a?(Hash)
|
578
|
-
return 'Options should be a hash'
|
579
|
-
else
|
580
|
-
if options.key?(:url)
|
581
|
-
if !options[:url].is_a?(String)
|
582
|
-
return '"url" option should be a string'
|
583
|
-
elsif !options[:url].start_with?('/')
|
584
|
-
return '"url" option should begin with a forward slash'
|
585
|
-
end
|
586
|
-
end
|
587
|
-
|
588
|
-
if options.key?(:cors)
|
589
|
-
if !options[:cors].is_a?(String) && !options[:cors].is_a?(TrueClass) && !options[:cors].is_a?(FalseClass)
|
590
|
-
return '"cors" option should be a string or boolean'
|
591
|
-
end
|
592
|
-
end
|
593
|
-
end
|
594
|
-
|
595
|
-
return nil
|
596
|
-
end
|
597
|
-
|
598
|
-
|
599
|
-
|
600
258
|
def create_request_handler(routes)
|
601
259
|
raise ArgumentError, 'A routes object is required' unless routes.is_a?(Hash)
|
602
260
|
|
@@ -884,8 +542,9 @@ def route_reducer(handler, request, context, timeout = nil)
|
|
884
542
|
end
|
885
543
|
|
886
544
|
# if request[:selector]
|
887
|
-
|
888
|
-
|
545
|
+
if request&.headers&._s
|
546
|
+
result = filter_object(result, request.headers._s)
|
547
|
+
end
|
889
548
|
|
890
549
|
[request[:id], request[:route], result, nil]
|
891
550
|
rescue => error
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: blest
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- JHunt
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-12-29 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: The Ruby reference implementation of BLEST (Batch-able, Lightweight,
|
14
14
|
Encrypted State Transfer), an improved communication protocol for web APIs which
|
@@ -36,7 +36,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
37
37
|
- - ">="
|
38
38
|
- !ruby/object:Gem::Version
|
39
|
-
version: '0'
|
39
|
+
version: '3.0'
|
40
40
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
41
41
|
requirements:
|
42
42
|
- - ">="
|