low_loop 0.4.0 → 0.5.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/lib/factories/response_factory.rb +2 -2
- data/lib/low_loop.rb +74 -11
- data/lib/requests/request_parser.rb +29 -18
- data/lib/responses/response_builder.rb +36 -9
- data/lib/support/low_frame.rb +9 -12
- data/lib/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: db3872b7a63b4b1fdb9036a98dce3f48ec8d85a0facd7fe42f8dd806a82cab17
|
|
4
|
+
data.tar.gz: 1dced9eafbbbc44921900cff33d734b8540ecd92a59a122f8654c7689f2d021a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d80a7cf83f40792f60f2652c13583b130e770ca31272f2ab3e5796b7ec232e581d1a48b41030247f18a6fdfa685457a6790b78b29211a167f7e5df14da2328fb
|
|
7
|
+
data.tar.gz: aab2f0b8a6a4450ddf9f5c2b1a9226abb0cc168aee50ed4c081e1ac2c4a54c83fcab57d80102c505b3dba8bcd35651a8544e72fa5eac9c80357d393b654cc3a4
|
|
@@ -11,7 +11,7 @@ module Low
|
|
|
11
11
|
headers = Protocol::HTTP::Headers.new(['content-type', 'text/html'])
|
|
12
12
|
body = Protocol::HTTP::Body::Buffered.wrap(body)
|
|
13
13
|
|
|
14
|
-
Protocol::HTTP::Response.new('
|
|
14
|
+
Protocol::HTTP::Response.new('HTTP/1.1', 200, headers, body)
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def file(path:, content_type:)
|
|
@@ -19,7 +19,7 @@ module Low
|
|
|
19
19
|
file = File.open(path, 'rb')
|
|
20
20
|
body = Protocol::HTTP::Body::File.new(file)
|
|
21
21
|
|
|
22
|
-
Protocol::HTTP::Response.new('
|
|
22
|
+
Protocol::HTTP::Response.new('HTTP/1.1', 200, headers, body)
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
25
|
end
|
data/lib/low_loop.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'async'
|
|
4
|
+
require 'io/wait'
|
|
4
5
|
require 'socket'
|
|
5
6
|
require 'low_type'
|
|
6
7
|
require 'low_event'
|
|
@@ -15,11 +16,14 @@ require_relative 'support/low_frame'
|
|
|
15
16
|
class LowLoop
|
|
16
17
|
include Observers
|
|
17
18
|
|
|
19
|
+
DEFAULT_KEEP_ALIVE_TIMEOUT = 30
|
|
20
|
+
DEFAULT_REQUEST_TIMEOUT = 10
|
|
21
|
+
|
|
18
22
|
attr_reader :config
|
|
19
23
|
|
|
20
24
|
def initialize(config:, router: nil, renderer: nil, show_output: true)
|
|
21
25
|
@config = config
|
|
22
|
-
@frame = LowFrame.new(renderer:, fps:
|
|
26
|
+
@frame = LowFrame.new(renderer:, fps: 10, show_output:)
|
|
23
27
|
|
|
24
28
|
observers(Low::Events::RequestEvent) << Low::FileServer.new(web_root: config.web_root, content_types: config.content_types)
|
|
25
29
|
observers(Low::Events::RequestEvent) << router if router
|
|
@@ -30,20 +34,21 @@ class LowLoop
|
|
|
30
34
|
def start
|
|
31
35
|
server = start_server
|
|
32
36
|
|
|
33
|
-
|
|
37
|
+
Async do |task|
|
|
38
|
+
# Background task.
|
|
39
|
+
task.async do
|
|
40
|
+
loop do
|
|
41
|
+
@frame.render if @frame.renderer
|
|
42
|
+
sleep 0.1 # 10fps
|
|
43
|
+
end
|
|
44
|
+
end
|
|
34
45
|
|
|
35
|
-
|
|
46
|
+
# Request handler.
|
|
36
47
|
loop do
|
|
37
48
|
socket = server.accept
|
|
38
49
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
Fiber.schedule do
|
|
42
|
-
request = Low::RequestParser.parse(socket:, host: config.host, port: config.port)
|
|
43
|
-
response_event = Low::Events::RequestEvent.take(request:)
|
|
44
|
-
response = response_event.response
|
|
45
|
-
|
|
46
|
-
Low::ResponseBuilder.respond(config:, socket:, response:)
|
|
50
|
+
task.async do
|
|
51
|
+
handle_connection(socket)
|
|
47
52
|
rescue StandardError => e
|
|
48
53
|
puts e.message
|
|
49
54
|
ensure
|
|
@@ -61,6 +66,14 @@ class LowLoop
|
|
|
61
66
|
server
|
|
62
67
|
end
|
|
63
68
|
|
|
69
|
+
def render
|
|
70
|
+
Async do
|
|
71
|
+
loop do
|
|
72
|
+
@frame.render
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
64
77
|
# Fallback mode for when there's no dependencies and you want to know that the server is still working.
|
|
65
78
|
def mirror(event:)
|
|
66
79
|
request = event.request
|
|
@@ -72,4 +85,54 @@ class LowLoop
|
|
|
72
85
|
def ==(other) = other.class == self.class
|
|
73
86
|
def eql?(other) = self == other
|
|
74
87
|
def hash = [self.class].hash
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def handle_connection(socket)
|
|
92
|
+
stream = Low::RequestParser.create_stream(socket:)
|
|
93
|
+
keep_alive = true
|
|
94
|
+
version = nil
|
|
95
|
+
|
|
96
|
+
while keep_alive
|
|
97
|
+
break unless socket.wait_readable(keep_alive_timeout)
|
|
98
|
+
|
|
99
|
+
socket.timeout = request_timeout
|
|
100
|
+
begin
|
|
101
|
+
request = Low::RequestParser.parse(stream:, host: config.host, port: config.port, version:)
|
|
102
|
+
rescue IO::TimeoutError
|
|
103
|
+
break
|
|
104
|
+
ensure
|
|
105
|
+
socket.timeout = nil
|
|
106
|
+
end
|
|
107
|
+
break if request.nil?
|
|
108
|
+
|
|
109
|
+
version ||= request.version
|
|
110
|
+
keep_alive = keep_alive?(request)
|
|
111
|
+
|
|
112
|
+
response_event = Low::Events::RequestEvent.take(request:)
|
|
113
|
+
response = response_event.response
|
|
114
|
+
|
|
115
|
+
Low::ResponseBuilder.respond(config:, socket:, response:, keep_alive:)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def keep_alive?(request)
|
|
120
|
+
tokens = (request.headers['connection'] || []).flat_map do |value|
|
|
121
|
+
value.split(',').map { |token| token.strip.downcase }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
if request.version.to_s.downcase.include?('1.0')
|
|
125
|
+
tokens.include?('keep-alive')
|
|
126
|
+
else
|
|
127
|
+
!tokens.include?('close')
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def keep_alive_timeout
|
|
132
|
+
config.keep_alive_timeout || DEFAULT_KEEP_ALIVE_TIMEOUT
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def request_timeout
|
|
136
|
+
config.request_timeout || DEFAULT_REQUEST_TIMEOUT
|
|
137
|
+
end
|
|
75
138
|
end
|
|
@@ -9,15 +9,21 @@ module Low
|
|
|
9
9
|
include LowType
|
|
10
10
|
|
|
11
11
|
class << self
|
|
12
|
-
def
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
def create_stream(socket: TCPSocket) -> { IO::Stream::Generic }
|
|
13
|
+
IO::Stream(socket)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def parse(stream: IO::Stream::Generic, host: String, port: Integer, version: nil) -> { ::Protocol::HTTP::Request | nil }
|
|
17
|
+
version ||= Async::HTTP::Protocol::HTTP.default.protocol_for(stream)::VERSION
|
|
18
|
+
|
|
19
|
+
result = parse_request(stream:)
|
|
20
|
+
return nil unless result
|
|
15
21
|
|
|
16
|
-
method,
|
|
22
|
+
method, full_path, = result
|
|
17
23
|
headers = parse_headers(stream:)
|
|
18
|
-
body = parse_body(stream:, method:)
|
|
24
|
+
body = parse_body(stream:, method:, headers:)
|
|
19
25
|
|
|
20
|
-
::Protocol::HTTP::Request.new('http', "#{host}:#{port}", method,
|
|
26
|
+
::Protocol::HTTP::Request.new('http', "#{host}:#{port}", method, full_path, version, headers, body)
|
|
21
27
|
end
|
|
22
28
|
|
|
23
29
|
private
|
|
@@ -30,23 +36,25 @@ module Low
|
|
|
30
36
|
# :header_3\r\n
|
|
31
37
|
# \r\n
|
|
32
38
|
# :body
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
request_line =
|
|
39
|
+
def parse_request(stream: IO::Stream::Generic)
|
|
40
|
+
request_line = stream.gets || return
|
|
41
|
+
|
|
42
|
+
request_line = request_line.strip
|
|
43
|
+
return nil if request_line.empty?
|
|
37
44
|
|
|
38
|
-
method, full_path, _http_version = request_line.
|
|
45
|
+
method, full_path, _http_version = request_line.split(' ', 3)
|
|
39
46
|
path, query = full_path.split('?', 2)
|
|
40
47
|
|
|
41
48
|
[method, full_path, path, query]
|
|
42
49
|
end
|
|
43
50
|
|
|
44
|
-
|
|
45
|
-
def parse_headers(stream:) -> { ::Protocol::HTTP::Headers }
|
|
51
|
+
def parse_headers(stream: IO::Stream::Generic) -> { ::Protocol::HTTP::Headers }
|
|
46
52
|
fields = []
|
|
47
53
|
|
|
48
|
-
|
|
49
|
-
|
|
54
|
+
# Sometimes returns nil which represents a client disconnect mid-request.
|
|
55
|
+
while (line = stream.gets)
|
|
56
|
+
line = line.strip
|
|
57
|
+
break if line.empty?
|
|
50
58
|
|
|
51
59
|
key, value = line.split(/:\s/, 2)
|
|
52
60
|
fields << [key, value]
|
|
@@ -55,10 +63,13 @@ module Low
|
|
|
55
63
|
::Protocol::HTTP::Headers.new(fields)
|
|
56
64
|
end
|
|
57
65
|
|
|
58
|
-
def parse_body(stream
|
|
59
|
-
return nil unless %w[POST PUT].include?(method)
|
|
66
|
+
def parse_body(stream: IO::Stream::Generic, method: String, headers: ::Protocol::HTTP::Headers)
|
|
67
|
+
return nil unless %w[POST PUT PATCH].include?(method)
|
|
68
|
+
|
|
69
|
+
content_length = headers['content-length']&.first&.to_i
|
|
70
|
+
return nil unless content_length&.positive?
|
|
60
71
|
|
|
61
|
-
stream.read
|
|
72
|
+
stream.read(content_length)
|
|
62
73
|
end
|
|
63
74
|
end
|
|
64
75
|
end
|
|
@@ -4,23 +4,50 @@ module Low
|
|
|
4
4
|
class ResponseBuilder
|
|
5
5
|
class << self
|
|
6
6
|
# TODO: Use Async wherever we can where it doesn't have "Task" requirement.
|
|
7
|
-
def respond(config:, socket:, response:)
|
|
8
|
-
|
|
9
|
-
socket.puts config.port.nil? ? "Host: #{config.host}\r\n" : "Host: #{config.host}:#{config.port}\r\n"
|
|
7
|
+
def respond(config:, socket:, response:, keep_alive: true)
|
|
8
|
+
file_body = response.body.respond_to?(:file)
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
if file_body
|
|
11
|
+
content_length = response.body.file.size
|
|
12
|
+
else
|
|
13
|
+
body_data = response.body.read || ''
|
|
14
|
+
content_length = body_data.bytesize
|
|
13
15
|
end
|
|
14
16
|
|
|
15
|
-
socket
|
|
17
|
+
write_status_line(socket, response)
|
|
18
|
+
write_host_header(socket, config)
|
|
19
|
+
write_response_headers(socket, response)
|
|
20
|
+
write_final_headers(socket, content_length, keep_alive)
|
|
16
21
|
|
|
17
|
-
if
|
|
22
|
+
if file_body
|
|
18
23
|
IO.copy_stream(response.body.file, socket)
|
|
19
24
|
else
|
|
20
|
-
socket.
|
|
25
|
+
socket.write(body_data)
|
|
21
26
|
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def write_status_line(socket, response)
|
|
32
|
+
socket.puts "#{response.version} #{response.status}\r\n"
|
|
33
|
+
end
|
|
22
34
|
|
|
23
|
-
|
|
35
|
+
def write_host_header(socket, config)
|
|
36
|
+
socket.puts config.port.nil? ? "Host: #{config.host}\r\n" : "Host: #{config.host}:#{config.port}\r\n"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def write_response_headers(socket, response)
|
|
40
|
+
response.headers.fields.each_slice(2) do |key, value|
|
|
41
|
+
next if %w[content-length connection].include?(key.to_s.downcase)
|
|
42
|
+
|
|
43
|
+
socket.puts "#{key}: #{value}\r\n"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def write_final_headers(socket, content_length, keep_alive)
|
|
48
|
+
socket.puts "Content-Length: #{content_length}\r\n"
|
|
49
|
+
socket.puts "Connection: #{keep_alive ? 'keep-alive' : 'close'}\r\n"
|
|
50
|
+
socket.puts "\r\n"
|
|
24
51
|
end
|
|
25
52
|
end
|
|
26
53
|
end
|
data/lib/support/low_frame.rb
CHANGED
|
@@ -5,28 +5,25 @@ require 'io/console'
|
|
|
5
5
|
class LowFrame
|
|
6
6
|
attr_reader :screen_size, :renderer
|
|
7
7
|
|
|
8
|
-
def initialize(renderer:, fps:
|
|
8
|
+
def initialize(renderer:, fps: 10, show_output: true)
|
|
9
9
|
@renderer = renderer
|
|
10
10
|
@show_output = show_output
|
|
11
11
|
|
|
12
12
|
# Millisecond duration of each frame. We lose a small amount of precision dropping the decimal.
|
|
13
13
|
@frame_time = ((1.0 / fps) * 1000).to_i
|
|
14
|
-
|
|
15
|
-
row_count, column_count = IO.console.winsize
|
|
16
|
-
@screen_size = { row_count:, column_count: }
|
|
17
|
-
|
|
18
14
|
@last_frame = nil
|
|
19
15
|
|
|
20
16
|
setup if renderer && show_output
|
|
21
17
|
end
|
|
22
18
|
|
|
23
19
|
def render
|
|
24
|
-
|
|
25
|
-
system 'clear' if @show_output
|
|
20
|
+
return unless @last_frame.nil? || (current_timestamp - @last_frame) >= @frame_time
|
|
26
21
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
row_count, column_count = IO.console.winsize
|
|
23
|
+
@screen_size = { row_count:, column_count: }
|
|
24
|
+
|
|
25
|
+
@last_frame = current_timestamp
|
|
26
|
+
@renderer.render(screen_size: @screen_size)
|
|
30
27
|
end
|
|
31
28
|
|
|
32
29
|
def setup
|
|
@@ -39,11 +36,11 @@ class LowFrame
|
|
|
39
36
|
end
|
|
40
37
|
|
|
41
38
|
def exit
|
|
42
|
-
trap('INT')
|
|
39
|
+
trap('INT') do
|
|
43
40
|
reset
|
|
44
41
|
system 'clear'
|
|
45
42
|
exit
|
|
46
|
-
|
|
43
|
+
end
|
|
47
44
|
end
|
|
48
45
|
|
|
49
46
|
private
|
data/lib/version.rb
CHANGED