tipi 0.39 → 0.43
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/.github/workflows/test.yml +4 -0
- data/.gitignore +5 -1
- data/CHANGELOG.md +30 -0
- data/Gemfile +5 -1
- data/Gemfile.lock +62 -25
- data/Rakefile +7 -3
- data/benchmarks/bm_http1_parser.rb +85 -0
- data/bin/benchmark +37 -0
- data/bin/h1pd +6 -0
- data/bin/tipi +3 -21
- data/df/server.rb +16 -87
- data/df/server_utils.rb +175 -0
- data/examples/full_service.rb +13 -0
- data/examples/http1_parser.rb +55 -0
- data/examples/http_server.rb +15 -3
- data/examples/http_server_forked.rb +3 -1
- data/examples/http_server_routes.rb +29 -0
- data/examples/http_server_static.rb +26 -0
- data/examples/https_server.rb +3 -0
- data/examples/servername_cb.rb +37 -0
- data/examples/websocket_demo.rb +2 -8
- data/examples/ws_page.html +2 -2
- data/lib/tipi.rb +89 -1
- data/lib/tipi/acme.rb +308 -0
- data/lib/tipi/cli.rb +30 -0
- data/lib/tipi/digital_fabric/agent.rb +7 -5
- data/lib/tipi/digital_fabric/agent_proxy.rb +16 -8
- data/lib/tipi/digital_fabric/executive.rb +6 -2
- data/lib/tipi/digital_fabric/protocol.rb +18 -3
- data/lib/tipi/digital_fabric/request_adapter.rb +0 -4
- data/lib/tipi/digital_fabric/service.rb +77 -49
- data/lib/tipi/http1_adapter.rb +91 -100
- data/lib/tipi/http2_adapter.rb +21 -6
- data/lib/tipi/http2_stream.rb +54 -44
- data/lib/tipi/rack_adapter.rb +2 -53
- data/lib/tipi/response_extensions.rb +17 -0
- data/lib/tipi/version.rb +1 -1
- data/test/helper.rb +60 -12
- data/test/test_http_server.rb +0 -27
- data/test/test_request.rb +2 -29
- data/tipi.gemspec +11 -7
- metadata +79 -26
- data/e +0 -0
data/lib/tipi/http2_adapter.rb
CHANGED
|
@@ -3,18 +3,30 @@
|
|
|
3
3
|
require 'http/2'
|
|
4
4
|
require_relative './http2_stream'
|
|
5
5
|
|
|
6
|
+
# patch to fix bug in HTTP2::Stream
|
|
7
|
+
class HTTP2::Stream
|
|
8
|
+
def end_stream?(frame)
|
|
9
|
+
case frame[:type]
|
|
10
|
+
when :data, :headers, :continuation
|
|
11
|
+
frame[:flags]&.include?(:end_stream)
|
|
12
|
+
else false
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
6
17
|
module Tipi
|
|
7
18
|
# HTTP2 server adapter
|
|
8
19
|
class HTTP2Adapter
|
|
9
|
-
def self.upgrade_each(socket, opts, headers, &block)
|
|
10
|
-
adapter = new(socket, opts, headers)
|
|
20
|
+
def self.upgrade_each(socket, opts, headers, body, &block)
|
|
21
|
+
adapter = new(socket, opts, headers, body)
|
|
11
22
|
adapter.each(&block)
|
|
12
23
|
end
|
|
13
24
|
|
|
14
|
-
def initialize(conn, opts, upgrade_headers = nil)
|
|
25
|
+
def initialize(conn, opts, upgrade_headers = nil, upgrade_body = nil)
|
|
15
26
|
@conn = conn
|
|
16
27
|
@opts = opts
|
|
17
28
|
@upgrade_headers = upgrade_headers
|
|
29
|
+
@upgrade_body = upgrade_body
|
|
18
30
|
@first = true
|
|
19
31
|
@rx = (upgrade_headers && upgrade_headers[':rx']) || 0
|
|
20
32
|
@tx = (upgrade_headers && upgrade_headers[':tx']) || 0
|
|
@@ -30,6 +42,8 @@ module Tipi
|
|
|
30
42
|
@transfer_count_request.tx_incr(data.bytesize)
|
|
31
43
|
end
|
|
32
44
|
@conn << data
|
|
45
|
+
rescue Polyphony::BaseException
|
|
46
|
+
raise
|
|
33
47
|
rescue Exception => e
|
|
34
48
|
@connection_fiber.transfer e
|
|
35
49
|
end
|
|
@@ -45,8 +59,7 @@ module Tipi
|
|
|
45
59
|
@conn << UPGRADE_MESSAGE
|
|
46
60
|
@tx += UPGRADE_MESSAGE.bytesize
|
|
47
61
|
settings = @upgrade_headers['http2-settings']
|
|
48
|
-
|
|
49
|
-
@interface.upgrade(settings, @upgrade_headers, '')
|
|
62
|
+
@interface.upgrade(settings, @upgrade_headers, @upgrade_body || '')
|
|
50
63
|
ensure
|
|
51
64
|
@upgrade_headers = nil
|
|
52
65
|
end
|
|
@@ -60,7 +73,7 @@ module Tipi
|
|
|
60
73
|
@rx += data.bytesize
|
|
61
74
|
@interface << data
|
|
62
75
|
end
|
|
63
|
-
rescue SystemCallError, IOError
|
|
76
|
+
rescue SystemCallError, IOError, HTTP2::Error::Error
|
|
64
77
|
# ignore
|
|
65
78
|
ensure
|
|
66
79
|
finalize_client_loop
|
|
@@ -87,10 +100,12 @@ module Tipi
|
|
|
87
100
|
def finalize_client_loop
|
|
88
101
|
@interface = nil
|
|
89
102
|
@streams.each_key(&:stop)
|
|
103
|
+
@conn.shutdown if @conn.respond_to?(:shutdown) rescue nil
|
|
90
104
|
@conn.close
|
|
91
105
|
end
|
|
92
106
|
|
|
93
107
|
def close
|
|
108
|
+
@conn.shutdown if @conn.respond_to?(:shutdown) rescue nil
|
|
94
109
|
@conn.close
|
|
95
110
|
end
|
|
96
111
|
|
data/lib/tipi/http2_stream.rb
CHANGED
|
@@ -15,8 +15,7 @@ module Tipi
|
|
|
15
15
|
@conn = conn
|
|
16
16
|
@first = first
|
|
17
17
|
@connection_fiber = Fiber.current
|
|
18
|
-
@stream_fiber = spin {
|
|
19
|
-
Thread.current.fiber_unschedule(@stream_fiber)
|
|
18
|
+
@stream_fiber = spin { run(&block) }
|
|
20
19
|
|
|
21
20
|
# Stream callbacks occur on the connection fiber (see HTTP2Adapter#each).
|
|
22
21
|
# The request handler is run on a separate fiber for each stream, allowing
|
|
@@ -34,16 +33,16 @@ module Tipi
|
|
|
34
33
|
stream.on(:half_close, &method(:on_half_close))
|
|
35
34
|
end
|
|
36
35
|
|
|
37
|
-
def
|
|
36
|
+
def run(&block)
|
|
37
|
+
request = receive
|
|
38
38
|
error = nil
|
|
39
39
|
block.(request)
|
|
40
40
|
@connection_fiber.schedule
|
|
41
|
-
rescue Polyphony::
|
|
42
|
-
|
|
41
|
+
rescue Polyphony::BaseException
|
|
42
|
+
raise
|
|
43
43
|
rescue Exception => e
|
|
44
44
|
error = e
|
|
45
45
|
ensure
|
|
46
|
-
@done = true
|
|
47
46
|
@connection_fiber.schedule error
|
|
48
47
|
end
|
|
49
48
|
|
|
@@ -55,29 +54,19 @@ module Tipi
|
|
|
55
54
|
@request.headers[':first'] = true
|
|
56
55
|
@first = false
|
|
57
56
|
end
|
|
58
|
-
@stream_fiber
|
|
57
|
+
@stream_fiber << @request
|
|
59
58
|
end
|
|
60
59
|
|
|
61
60
|
def on_data(data)
|
|
62
61
|
data = data.to_s # chunks might be wrapped in a HTTP2::Buffer
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
else
|
|
67
|
-
@request.buffer_body_chunk(data)
|
|
68
|
-
end
|
|
62
|
+
|
|
63
|
+
(@buffered_chunks ||= []) << data
|
|
64
|
+
@get_body_chunk_fiber&.schedule
|
|
69
65
|
end
|
|
70
66
|
|
|
71
67
|
def on_half_close
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
@stream_fiber.schedule
|
|
75
|
-
elsif @waiting_for_half_close
|
|
76
|
-
@waiting_for_half_close = nil
|
|
77
|
-
@stream_fiber.schedule
|
|
78
|
-
else
|
|
79
|
-
@request.complete!
|
|
80
|
-
end
|
|
68
|
+
@get_body_chunk_fiber&.schedule
|
|
69
|
+
@complete = true
|
|
81
70
|
end
|
|
82
71
|
|
|
83
72
|
def protocol
|
|
@@ -90,31 +79,38 @@ module Tipi
|
|
|
90
79
|
ensure
|
|
91
80
|
@adapter.unset_request_for_transfer_count(request)
|
|
92
81
|
end
|
|
93
|
-
|
|
94
|
-
def get_body_chunk(request)
|
|
95
|
-
|
|
96
|
-
return
|
|
82
|
+
|
|
83
|
+
def get_body_chunk(request, buffered_only = false)
|
|
84
|
+
@buffered_chunks ||= []
|
|
85
|
+
return @buffered_chunks.shift unless @buffered_chunks.empty?
|
|
86
|
+
return nil if @complete
|
|
97
87
|
|
|
98
|
-
|
|
99
|
-
@
|
|
100
|
-
# the chunk (or an exception) will be returned once the stream fiber is
|
|
101
|
-
# resumed
|
|
88
|
+
begin
|
|
89
|
+
@get_body_chunk_fiber = Fiber.current
|
|
102
90
|
suspend
|
|
91
|
+
ensure
|
|
92
|
+
@get_body_chunk_fiber = nil
|
|
103
93
|
end
|
|
104
|
-
|
|
105
|
-
@waiting_for_body_chunk = nil
|
|
94
|
+
@buffered_chunks.shift
|
|
106
95
|
end
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
return if @
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
96
|
+
|
|
97
|
+
def get_body(request)
|
|
98
|
+
@buffered_chunks ||= []
|
|
99
|
+
return @buffered_chunks.join if @complete
|
|
100
|
+
|
|
101
|
+
while !@complete
|
|
102
|
+
begin
|
|
103
|
+
@get_body_chunk_fiber = Fiber.current
|
|
104
|
+
suspend
|
|
105
|
+
ensure
|
|
106
|
+
@get_body_chunk_fiber = nil
|
|
107
|
+
end
|
|
115
108
|
end
|
|
116
|
-
|
|
117
|
-
|
|
109
|
+
@buffered_chunks.join
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def complete?(request)
|
|
113
|
+
@complete
|
|
118
114
|
end
|
|
119
115
|
|
|
120
116
|
# response API
|
|
@@ -123,9 +119,23 @@ module Tipi
|
|
|
123
119
|
headers[':status'] = headers[':status'].to_s
|
|
124
120
|
with_transfer_count(request) do
|
|
125
121
|
@stream.headers(transform_headers(headers))
|
|
122
|
+
@headers_sent = true
|
|
126
123
|
@stream.data(chunk || '')
|
|
127
124
|
end
|
|
128
|
-
|
|
125
|
+
rescue HTTP2::Error::StreamClosed
|
|
126
|
+
# ignore
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def respond_from_io(request, io, headers, chunk_size = 2**16)
|
|
130
|
+
headers[':status'] ||= Qeweney::Status::OK
|
|
131
|
+
headers[':status'] = headers[':status'].to_s
|
|
132
|
+
with_transfer_count(request) do
|
|
133
|
+
@stream.headers(transform_headers(headers))
|
|
134
|
+
@headers_sent = true
|
|
135
|
+
while (chunk = io.read(chunk_size))
|
|
136
|
+
@stream.data(chunk)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
129
139
|
rescue HTTP2::Error::StreamClosed
|
|
130
140
|
# ignore
|
|
131
141
|
end
|
|
@@ -180,7 +190,7 @@ module Tipi
|
|
|
180
190
|
end
|
|
181
191
|
|
|
182
192
|
def stop
|
|
183
|
-
return if @
|
|
193
|
+
return if @complete
|
|
184
194
|
|
|
185
195
|
@stream.close
|
|
186
196
|
@stream_fiber.schedule(Polyphony::MoveOn.new)
|
data/lib/tipi/rack_adapter.rb
CHANGED
|
@@ -3,25 +3,7 @@
|
|
|
3
3
|
require 'rack'
|
|
4
4
|
|
|
5
5
|
module Tipi
|
|
6
|
-
module RackAdapter
|
|
7
|
-
# Implements a rack input stream:
|
|
8
|
-
# https://www.rubydoc.info/github/rack/rack/master/file/SPEC#label-The+Input+Stream
|
|
9
|
-
class InputStream
|
|
10
|
-
def initialize(request)
|
|
11
|
-
@request = request
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def gets; end
|
|
15
|
-
|
|
16
|
-
def read(length = nil, outbuf = nil); end
|
|
17
|
-
|
|
18
|
-
def each(&block)
|
|
19
|
-
@request.each_chunk(&block)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def rewind; end
|
|
23
|
-
end
|
|
24
|
-
|
|
6
|
+
module RackAdapter
|
|
25
7
|
class << self
|
|
26
8
|
def run(app)
|
|
27
9
|
->(req) { respond(req, app.(env(req))) }
|
|
@@ -32,41 +14,8 @@ module Tipi
|
|
|
32
14
|
instance_eval(src, path, 1)
|
|
33
15
|
end
|
|
34
16
|
|
|
35
|
-
RACK_ENV = {
|
|
36
|
-
'SCRIPT_NAME' => '',
|
|
37
|
-
'rack.version' => Rack::VERSION,
|
|
38
|
-
'SERVER_PORT' => '80', # ?
|
|
39
|
-
'rack.url_scheme' => 'http', # ?
|
|
40
|
-
'rack.errors' => STDERR, # ?
|
|
41
|
-
'rack.multithread' => false,
|
|
42
|
-
'rack.run_once' => false,
|
|
43
|
-
'rack.hijack?' => false,
|
|
44
|
-
'rack.hijack' => nil,
|
|
45
|
-
'rack.hijack_io' => nil,
|
|
46
|
-
'rack.session' => nil,
|
|
47
|
-
'rack.logger' => nil,
|
|
48
|
-
'rack.multipart.buffer_size' => nil,
|
|
49
|
-
'rack.multipar.tempfile_factory' => nil
|
|
50
|
-
}
|
|
51
|
-
|
|
52
17
|
def env(request)
|
|
53
|
-
|
|
54
|
-
h[k] = env_value_from_request(request, k)
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
HTTP_HEADER_RE = /^HTTP_(.+)$/.freeze
|
|
59
|
-
|
|
60
|
-
def env_value_from_request(request, key)
|
|
61
|
-
case key
|
|
62
|
-
when 'REQUEST_METHOD' then request.method
|
|
63
|
-
when 'PATH_INFO' then request.path
|
|
64
|
-
when 'QUERY_STRING' then request.query_string || ''
|
|
65
|
-
when 'SERVER_NAME' then request.headers['host']
|
|
66
|
-
when 'rack.input' then InputStream.new(request)
|
|
67
|
-
when HTTP_HEADER_RE then request.headers[$1.downcase]
|
|
68
|
-
else RACK_ENV[key]
|
|
69
|
-
end
|
|
18
|
+
Qeweney.rack_env_from_request(request)
|
|
70
19
|
end
|
|
71
20
|
|
|
72
21
|
def respond(request, (status_code, headers, body))
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'qeweney/request'
|
|
4
|
+
|
|
5
|
+
module Tipi
|
|
6
|
+
module ResponseExtensions
|
|
7
|
+
SPLICE_CHUNKS_SIZE_THRESHOLD = 2**20
|
|
8
|
+
|
|
9
|
+
def serve_io(io, opts)
|
|
10
|
+
if !opts[:stat] || opts[:stat].size >= SPLICE_CHUNKS_SIZE_THRESHOLD
|
|
11
|
+
@adapter.respond_from_io(self, io, opts[:headers], opts[:chunk_size] || 2**14)
|
|
12
|
+
else
|
|
13
|
+
respond(io.read, opts[:headers] || {})
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
data/lib/tipi/version.rb
CHANGED
data/test/helper.rb
CHANGED
|
@@ -8,32 +8,29 @@ require_relative './eg'
|
|
|
8
8
|
require_relative './coverage' if ENV['COVERAGE']
|
|
9
9
|
|
|
10
10
|
require 'minitest/autorun'
|
|
11
|
-
require 'minitest/reporters'
|
|
12
11
|
|
|
13
12
|
require 'polyphony'
|
|
14
13
|
|
|
15
14
|
::Exception.__disable_sanitized_backtrace__ = true
|
|
16
15
|
|
|
17
|
-
Minitest::Reporters.use! [
|
|
18
|
-
Minitest::Reporters::SpecReporter.new
|
|
19
|
-
]
|
|
20
|
-
|
|
21
16
|
class MiniTest::Test
|
|
22
17
|
def setup
|
|
23
|
-
#
|
|
24
|
-
if Fiber.current.children.size > 0
|
|
25
|
-
puts "Children left: #{Fiber.current.children.inspect}"
|
|
26
|
-
exit!
|
|
27
|
-
end
|
|
18
|
+
# trace "* setup #{self.name}"
|
|
28
19
|
Fiber.current.setup_main_fiber
|
|
29
20
|
Fiber.current.instance_variable_set(:@auto_watcher, nil)
|
|
21
|
+
Thread.current.backend.finalize
|
|
30
22
|
Thread.current.backend = Polyphony::Backend.new
|
|
31
23
|
sleep 0
|
|
32
24
|
end
|
|
33
25
|
|
|
34
26
|
def teardown
|
|
35
|
-
#
|
|
27
|
+
# trace "* teardown #{self.name}"
|
|
36
28
|
Fiber.current.shutdown_all_children
|
|
29
|
+
if Fiber.current.children.size > 0
|
|
30
|
+
puts "Children left after #{self.name}: #{Fiber.current.children.inspect}"
|
|
31
|
+
exit!
|
|
32
|
+
end
|
|
33
|
+
Fiber.current.instance_variable_set(:@auto_watcher, nil)
|
|
37
34
|
rescue => e
|
|
38
35
|
puts e
|
|
39
36
|
puts e.backtrace.join("\n")
|
|
@@ -47,4 +44,55 @@ module Kernel
|
|
|
47
44
|
rescue Exception => e
|
|
48
45
|
e
|
|
49
46
|
end
|
|
50
|
-
|
|
47
|
+
|
|
48
|
+
def trace(*args)
|
|
49
|
+
STDOUT.orig_write(format_trace(args))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def format_trace(args)
|
|
53
|
+
if args.first.is_a?(String)
|
|
54
|
+
if args.size > 1
|
|
55
|
+
format("%s: %p\n", args.shift, args)
|
|
56
|
+
else
|
|
57
|
+
format("%s\n", args.first)
|
|
58
|
+
end
|
|
59
|
+
else
|
|
60
|
+
format("%p\n", args.size == 1 ? args.first : args)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
class IO
|
|
66
|
+
# Creates two mockup sockets for simulating server-client communication
|
|
67
|
+
def self.server_client_mockup
|
|
68
|
+
server_in, client_out = IO.pipe
|
|
69
|
+
client_in, server_out = IO.pipe
|
|
70
|
+
|
|
71
|
+
server_connection = mockup_connection(server_in, server_out, client_out)
|
|
72
|
+
client_connection = mockup_connection(client_in, client_out, server_out)
|
|
73
|
+
|
|
74
|
+
[server_connection, client_connection]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.mockup_connection(input, output, output2)
|
|
78
|
+
eg(
|
|
79
|
+
__parser_read_method__: ->() { :readpartial },
|
|
80
|
+
read: ->(*args) { input.read(*args) },
|
|
81
|
+
read_loop: ->(*args, &block) { input.read_loop(*args, &block) },
|
|
82
|
+
recv_loop: ->(*args, &block) { input.read_loop(*args, &block) },
|
|
83
|
+
readpartial: ->(*args) { input.readpartial(*args) },
|
|
84
|
+
recv: ->(*args) { input.readpartial(*args) },
|
|
85
|
+
'<<': ->(*args) { output.write(*args) },
|
|
86
|
+
write: ->(*args) { output.write(*args) },
|
|
87
|
+
close: -> { output.close },
|
|
88
|
+
eof?: -> { output2.closed? }
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
module Minitest::Assertions
|
|
94
|
+
def assert_in_range exp_range, act
|
|
95
|
+
msg = message(msg) { "Expected #{mu_pp(act)} to be in range #{mu_pp(exp_range)}" }
|
|
96
|
+
assert exp_range.include?(act), msg
|
|
97
|
+
end
|
|
98
|
+
end
|
data/test/test_http_server.rb
CHANGED
|
@@ -9,33 +9,6 @@ class String
|
|
|
9
9
|
end
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
class IO
|
|
13
|
-
# Creates two mockup sockets for simulating server-client communication
|
|
14
|
-
def self.server_client_mockup
|
|
15
|
-
server_in, client_out = IO.pipe
|
|
16
|
-
client_in, server_out = IO.pipe
|
|
17
|
-
|
|
18
|
-
server_connection = mockup_connection(server_in, server_out, client_out)
|
|
19
|
-
client_connection = mockup_connection(client_in, client_out, server_out)
|
|
20
|
-
|
|
21
|
-
[server_connection, client_connection]
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def self.mockup_connection(input, output, output2)
|
|
25
|
-
eg(
|
|
26
|
-
:read => ->(*args) { input.read(*args) },
|
|
27
|
-
:read_loop => ->(*args, &block) { input.read_loop(*args, &block) },
|
|
28
|
-
:recv_loop => ->(*args, &block) { input.read_loop(*args, &block) },
|
|
29
|
-
:readpartial => ->(*args) { input.readpartial(*args) },
|
|
30
|
-
:recv => ->(*args) { input.readpartial(*args) },
|
|
31
|
-
:<< => ->(*args) { output.write(*args) },
|
|
32
|
-
:write => ->(*args) { output.write(*args) },
|
|
33
|
-
:close => -> { output.close },
|
|
34
|
-
:eof? => -> { output2.closed? }
|
|
35
|
-
)
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
12
|
class HTTP1ServerTest < MiniTest::Test
|
|
40
13
|
def teardown
|
|
41
14
|
@server&.interrupt if @server&.alive?
|