blest 1.0.0 → 1.0.1
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 +1 -31
- data/lib/blest.rb +0 -342
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dda8d494a672ecd29209b9971076decfaec476253157103dc75a6d74cfc2f327
|
4
|
+
data.tar.gz: dee130556eb32a00cdbb14ae3f5c160133241763061d6558592d1712963cbfc4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4407b468d713463014fa4449efb4e2be40ac59d93fbca2302d1ddc13aa036907b0ba6b4067cd92e1e90a64e37dcca3cd29b0aec605722fd7a5edf4458058245f
|
7
|
+
data.tar.gz: f1c050cdaa44297e90c968ba99be88f3fb440743a959e9ab32286d04ce42a4dc886059bb598c1c29ab0d38d41d7bccc384a86c9d114d5f6398f68da275afb8a3
|
data/LICENSE
CHANGED
data/README.md
CHANGED
@@ -24,36 +24,6 @@ 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
29
|
The following example uses Sinatra.
|
@@ -111,7 +81,7 @@ client = HttpClient.new('http://localhost:8080', max_batch_size = 25, buffer_del
|
|
111
81
|
|
112
82
|
# Send a request
|
113
83
|
begin
|
114
|
-
result = client.request('greet', { 'name': 'Steve' },
|
84
|
+
result = client.request('greet', { 'name': 'Steve' }, { 'auth': 'myToken' }).value
|
115
85
|
# Do something with the result
|
116
86
|
rescue => error
|
117
87
|
# 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
|
|
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.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- JHunt
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-10-
|
11
|
+
date: 2024-10-31 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
|