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