blest 1.0.0 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|