tipi 0.38 → 0.42
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 +5 -1
- data/.gitignore +5 -0
- data/CHANGELOG.md +34 -0
- data/Gemfile +5 -1
- data/Gemfile.lock +58 -16
- data/Rakefile +7 -3
- data/TODO.md +77 -1
- data/benchmarks/bm_http1_parser.rb +61 -0
- data/bin/benchmark +37 -0
- data/bin/h1pd +6 -0
- data/bin/tipi +3 -21
- data/df/sample_agent.rb +1 -1
- data/df/server.rb +16 -47
- data/df/server_utils.rb +178 -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 +5 -1
- data/examples/http_server_routes.rb +29 -0
- data/examples/http_server_static.rb +26 -0
- data/examples/http_server_throttled.rb +3 -2
- data/examples/https_server.rb +6 -4
- data/examples/https_wss_server.rb +2 -1
- data/examples/rack_server.rb +5 -0
- data/examples/rack_server_https.rb +1 -1
- data/examples/rack_server_https_forked.rb +4 -3
- data/examples/routing_server.rb +5 -4
- data/examples/servername_cb.rb +37 -0
- data/examples/websocket_demo.rb +2 -8
- data/examples/ws_page.html +2 -2
- data/ext/tipi/extconf.rb +13 -0
- data/ext/tipi/http1_parser.c +823 -0
- data/ext/tipi/http1_parser.h +18 -0
- data/ext/tipi/tipi_ext.c +5 -0
- 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 +22 -17
- data/lib/tipi/digital_fabric/agent_proxy.rb +95 -40
- data/lib/tipi/digital_fabric/executive.rb +6 -2
- data/lib/tipi/digital_fabric/protocol.rb +87 -15
- data/lib/tipi/digital_fabric/request_adapter.rb +6 -10
- data/lib/tipi/digital_fabric/service.rb +77 -51
- data/lib/tipi/http1_adapter.rb +116 -117
- data/lib/tipi/http2_adapter.rb +56 -10
- data/lib/tipi/http2_stream.rb +106 -53
- data/lib/tipi/rack_adapter.rb +2 -53
- data/lib/tipi/response_extensions.rb +17 -0
- data/lib/tipi/version.rb +1 -1
- data/security/http1.rb +12 -0
- data/test/helper.rb +60 -11
- data/test/test_http1_parser.rb +586 -0
- data/test/test_http_server.rb +0 -27
- data/test/test_request.rb +1 -28
- data/tipi.gemspec +11 -5
- metadata +96 -22
- data/e +0 -0
data/lib/tipi/http2_adapter.rb
CHANGED
@@ -3,20 +3,34 @@
|
|
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
|
32
|
+
@tx = (upgrade_headers && upgrade_headers[':tx']) || 0
|
33
|
+
|
20
34
|
@interface = ::HTTP2::Server.new
|
21
35
|
@connection_fiber = Fiber.current
|
22
36
|
@interface.on(:frame, &method(:send_frame))
|
@@ -24,7 +38,12 @@ module Tipi
|
|
24
38
|
end
|
25
39
|
|
26
40
|
def send_frame(data)
|
41
|
+
if @transfer_count_request
|
42
|
+
@transfer_count_request.tx_incr(data.bytesize)
|
43
|
+
end
|
27
44
|
@conn << data
|
45
|
+
rescue Polyphony::BaseException
|
46
|
+
raise
|
28
47
|
rescue Exception => e
|
29
48
|
@connection_fiber.transfer e
|
30
49
|
end
|
@@ -38,9 +57,9 @@ module Tipi
|
|
38
57
|
|
39
58
|
def upgrade
|
40
59
|
@conn << UPGRADE_MESSAGE
|
60
|
+
@tx += UPGRADE_MESSAGE.bytesize
|
41
61
|
settings = @upgrade_headers['http2-settings']
|
42
|
-
|
43
|
-
@interface.upgrade(settings, @upgrade_headers, '')
|
62
|
+
@interface.upgrade(settings, @upgrade_headers, @upgrade_body || '')
|
44
63
|
ensure
|
45
64
|
@upgrade_headers = nil
|
46
65
|
end
|
@@ -49,16 +68,31 @@ module Tipi
|
|
49
68
|
def each(&block)
|
50
69
|
@interface.on(:stream) { |stream| start_stream(stream, &block) }
|
51
70
|
upgrade if @upgrade_headers
|
52
|
-
|
53
|
-
@conn.recv_loop
|
54
|
-
|
71
|
+
|
72
|
+
@conn.recv_loop do |data|
|
73
|
+
@rx += data.bytesize
|
74
|
+
@interface << data
|
75
|
+
end
|
76
|
+
rescue SystemCallError, IOError, HTTP2::Error::Error
|
55
77
|
# ignore
|
56
78
|
ensure
|
57
79
|
finalize_client_loop
|
58
80
|
end
|
81
|
+
|
82
|
+
def get_rx_count
|
83
|
+
count = @rx
|
84
|
+
@rx = 0
|
85
|
+
count
|
86
|
+
end
|
87
|
+
|
88
|
+
def get_tx_count
|
89
|
+
count = @tx
|
90
|
+
@tx = 0
|
91
|
+
count
|
92
|
+
end
|
59
93
|
|
60
94
|
def start_stream(stream, &block)
|
61
|
-
stream = HTTP2StreamHandler.new(stream, @conn, @first, &block)
|
95
|
+
stream = HTTP2StreamHandler.new(self, stream, @conn, @first, &block)
|
62
96
|
@first = nil if @first
|
63
97
|
@streams[stream] = true
|
64
98
|
end
|
@@ -66,11 +100,23 @@ module Tipi
|
|
66
100
|
def finalize_client_loop
|
67
101
|
@interface = nil
|
68
102
|
@streams.each_key(&:stop)
|
103
|
+
@conn.shutdown if @conn.respond_to?(:shutdown) rescue nil
|
69
104
|
@conn.close
|
70
105
|
end
|
71
106
|
|
72
107
|
def close
|
108
|
+
@conn.shutdown if @conn.respond_to?(:shutdown) rescue nil
|
73
109
|
@conn.close
|
74
110
|
end
|
111
|
+
|
112
|
+
def set_request_for_transfer_count(request)
|
113
|
+
@transfer_count_request = request
|
114
|
+
end
|
115
|
+
|
116
|
+
def unset_request_for_transfer_count(request)
|
117
|
+
return unless @transfer_count_request == request
|
118
|
+
|
119
|
+
@transfer_count_request = nil
|
120
|
+
end
|
75
121
|
end
|
76
122
|
end
|
data/lib/tipi/http2_stream.rb
CHANGED
@@ -9,13 +9,14 @@ module Tipi
|
|
9
9
|
attr_accessor :__next__
|
10
10
|
attr_reader :conn
|
11
11
|
|
12
|
-
def initialize(stream, conn, first, &block)
|
12
|
+
def initialize(adapter, stream, conn, first, &block)
|
13
|
+
@adapter = adapter
|
13
14
|
@stream = stream
|
14
15
|
@conn = conn
|
15
16
|
@first = first
|
16
17
|
@connection_fiber = Fiber.current
|
17
|
-
@stream_fiber = spin {
|
18
|
-
|
18
|
+
@stream_fiber = spin { run(&block) }
|
19
|
+
|
19
20
|
# Stream callbacks occur on the connection fiber (see HTTP2Adapter#each).
|
20
21
|
# The request handler is run on a separate fiber for each stream, allowing
|
21
22
|
# concurrent handling of incoming requests on the same HTTP/2 connection.
|
@@ -32,112 +33,164 @@ module Tipi
|
|
32
33
|
stream.on(:half_close, &method(:on_half_close))
|
33
34
|
end
|
34
35
|
|
35
|
-
def
|
36
|
+
def run(&block)
|
37
|
+
request = receive
|
36
38
|
error = nil
|
37
39
|
block.(request)
|
38
40
|
@connection_fiber.schedule
|
39
|
-
rescue Polyphony::
|
40
|
-
|
41
|
+
rescue Polyphony::BaseException
|
42
|
+
raise
|
41
43
|
rescue Exception => e
|
42
44
|
error = e
|
43
45
|
ensure
|
44
|
-
@done = true
|
45
46
|
@connection_fiber.schedule error
|
46
47
|
end
|
47
48
|
|
48
49
|
def on_headers(headers)
|
49
50
|
@request = Qeweney::Request.new(headers.to_h, self)
|
51
|
+
@request.rx_incr(@adapter.get_rx_count)
|
52
|
+
@request.tx_incr(@adapter.get_tx_count)
|
50
53
|
if @first
|
51
54
|
@request.headers[':first'] = true
|
52
55
|
@first = false
|
53
56
|
end
|
54
|
-
@stream_fiber
|
57
|
+
@stream_fiber << @request
|
55
58
|
end
|
56
|
-
|
59
|
+
|
57
60
|
def on_data(data)
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
@request.buffer_body_chunk(data)
|
63
|
-
end
|
61
|
+
data = data.to_s # chunks might be wrapped in a HTTP2::Buffer
|
62
|
+
|
63
|
+
(@buffered_chunks ||= []) << data
|
64
|
+
@get_body_chunk_fiber&.schedule
|
64
65
|
end
|
65
66
|
|
66
67
|
def on_half_close
|
67
|
-
|
68
|
-
|
69
|
-
@stream_fiber.schedule
|
70
|
-
elsif @waiting_for_half_close
|
71
|
-
@waiting_for_half_close = nil
|
72
|
-
@stream_fiber.schedule
|
73
|
-
else
|
74
|
-
@request.complete!
|
75
|
-
end
|
68
|
+
@get_body_chunk_fiber&.schedule
|
69
|
+
@complete = true
|
76
70
|
end
|
77
71
|
|
78
72
|
def protocol
|
79
73
|
'h2'
|
80
74
|
end
|
81
|
-
|
82
|
-
def
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
@waiting_for_body_chunk = true
|
87
|
-
# the chunk (or an exception) will be returned once the stream fiber is
|
88
|
-
# resumed
|
89
|
-
suspend
|
75
|
+
|
76
|
+
def with_transfer_count(request)
|
77
|
+
@adapter.set_request_for_transfer_count(request)
|
78
|
+
yield
|
90
79
|
ensure
|
91
|
-
@
|
80
|
+
@adapter.unset_request_for_transfer_count(request)
|
92
81
|
end
|
93
|
-
|
94
|
-
|
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
|
-
|
101
|
-
|
88
|
+
begin
|
89
|
+
@get_body_chunk_fiber = Fiber.current
|
90
|
+
suspend
|
91
|
+
ensure
|
92
|
+
@get_body_chunk_fiber = nil
|
93
|
+
end
|
94
|
+
@buffered_chunks.shift
|
95
|
+
end
|
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
|
108
|
+
end
|
109
|
+
@buffered_chunks.join
|
110
|
+
end
|
111
|
+
|
112
|
+
def complete?(request)
|
113
|
+
@complete
|
102
114
|
end
|
103
115
|
|
104
116
|
# response API
|
105
|
-
def respond(chunk, headers)
|
117
|
+
def respond(request, chunk, headers)
|
106
118
|
headers[':status'] ||= Qeweney::Status::OK
|
107
|
-
|
108
|
-
|
109
|
-
|
119
|
+
headers[':status'] = headers[':status'].to_s
|
120
|
+
with_transfer_count(request) do
|
121
|
+
@stream.headers(transform_headers(headers))
|
122
|
+
@headers_sent = true
|
123
|
+
@stream.data(chunk || '')
|
124
|
+
end
|
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
|
139
|
+
rescue HTTP2::Error::StreamClosed
|
140
|
+
# ignore
|
141
|
+
end
|
142
|
+
|
143
|
+
def transform_headers(headers)
|
144
|
+
headers.each_with_object([]) do |(k, v), a|
|
145
|
+
if v.is_a?(Array)
|
146
|
+
v.each { |vv| a << [k, vv.to_s] }
|
147
|
+
else
|
148
|
+
a << [k, v.to_s]
|
149
|
+
end
|
150
|
+
end
|
110
151
|
end
|
111
152
|
|
112
|
-
def send_headers(headers, empty_response
|
153
|
+
def send_headers(request, headers, empty_response: false)
|
113
154
|
return if @headers_sent
|
114
155
|
|
115
156
|
headers[':status'] ||= (empty_response ? Qeweney::Status::NO_CONTENT : Qeweney::Status::OK).to_s
|
116
|
-
|
157
|
+
with_transfer_count(request) do
|
158
|
+
@stream.headers(transform_headers(headers), end_stream: false)
|
159
|
+
end
|
117
160
|
@headers_sent = true
|
161
|
+
rescue HTTP2::Error::StreamClosed
|
162
|
+
# ignore
|
118
163
|
end
|
119
164
|
|
120
|
-
def send_chunk(chunk, done: false)
|
165
|
+
def send_chunk(request, chunk, done: false)
|
121
166
|
send_headers({}, false) unless @headers_sent
|
122
167
|
|
123
168
|
if chunk
|
124
|
-
|
169
|
+
with_transfer_count(request) do
|
170
|
+
@stream.data(chunk, end_stream: done)
|
171
|
+
end
|
125
172
|
elsif done
|
126
173
|
@stream.close
|
127
174
|
end
|
175
|
+
rescue HTTP2::Error::StreamClosed
|
176
|
+
# ignore
|
128
177
|
end
|
129
178
|
|
130
|
-
def finish
|
179
|
+
def finish(request)
|
131
180
|
if @headers_sent
|
132
181
|
@stream.close
|
133
182
|
else
|
134
183
|
headers[':status'] ||= Qeweney::Status::NO_CONTENT
|
135
|
-
|
184
|
+
with_transfer_count(request) do
|
185
|
+
@stream.headers(transform_headers(headers), end_stream: true)
|
186
|
+
end
|
136
187
|
end
|
188
|
+
rescue HTTP2::Error::StreamClosed
|
189
|
+
# ignore
|
137
190
|
end
|
138
191
|
|
139
192
|
def stop
|
140
|
-
return if @
|
193
|
+
return if @complete
|
141
194
|
|
142
195
|
@stream.close
|
143
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