tipi 0.41 → 0.46
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/FUNDING.yml +1 -0
- data/.github/workflows/test.yml +3 -1
- data/.gitignore +3 -1
- data/CHANGELOG.md +34 -0
- data/Gemfile +7 -1
- data/Gemfile.lock +53 -33
- data/README.md +184 -8
- data/Rakefile +1 -7
- data/benchmarks/bm_http1_parser.rb +85 -0
- data/bin/benchmark +37 -0
- data/bin/h1pd +6 -0
- data/bin/tipi +3 -21
- data/bm.png +0 -0
- data/df/agent.rb +1 -1
- data/df/sample_agent.rb +2 -2
- data/df/server.rb +3 -1
- data/df/server_utils.rb +48 -46
- data/examples/full_service.rb +13 -0
- data/examples/hello.rb +5 -0
- data/examples/hello.ru +3 -3
- data/examples/http1_parser.rb +10 -8
- data/examples/http_server.js +1 -1
- data/examples/http_server.rb +4 -1
- data/examples/http_server_graceful.rb +1 -1
- data/examples/https_server.rb +41 -15
- data/examples/rack_server_forked.rb +26 -0
- data/examples/rack_server_https_forked.rb +1 -1
- data/examples/servername_cb.rb +37 -0
- data/examples/websocket_demo.rb +1 -1
- data/lib/tipi/acme.rb +320 -0
- data/lib/tipi/cli.rb +93 -0
- data/lib/tipi/config_dsl.rb +13 -13
- data/lib/tipi/configuration.rb +2 -2
- data/lib/tipi/controller/bare_polyphony.rb +0 -0
- data/lib/tipi/controller/bare_stock.rb +10 -0
- data/lib/tipi/controller/extensions.rb +37 -0
- data/lib/tipi/controller/stock_http1_adapter.rb +15 -0
- data/lib/tipi/controller/web_polyphony.rb +353 -0
- data/lib/tipi/controller/web_stock.rb +635 -0
- data/lib/tipi/controller.rb +12 -0
- data/lib/tipi/digital_fabric/agent.rb +5 -5
- data/lib/tipi/digital_fabric/agent_proxy.rb +15 -8
- data/lib/tipi/digital_fabric/executive.rb +7 -3
- data/lib/tipi/digital_fabric/protocol.rb +3 -3
- data/lib/tipi/digital_fabric/request_adapter.rb +0 -4
- data/lib/tipi/digital_fabric/service.rb +17 -18
- data/lib/tipi/handler.rb +2 -2
- data/lib/tipi/http1_adapter.rb +85 -124
- data/lib/tipi/http2_adapter.rb +29 -16
- data/lib/tipi/http2_stream.rb +52 -57
- data/lib/tipi/rack_adapter.rb +2 -2
- data/lib/tipi/response_extensions.rb +1 -1
- data/lib/tipi/supervisor.rb +75 -0
- data/lib/tipi/version.rb +1 -1
- data/lib/tipi/websocket.rb +3 -3
- data/lib/tipi.rb +9 -7
- data/test/coverage.rb +2 -2
- data/test/helper.rb +60 -12
- data/test/test_http_server.rb +14 -41
- data/test/test_request.rb +2 -29
- data/tipi.gemspec +10 -10
- metadata +80 -54
- data/examples/automatic_certificate.rb +0 -193
- data/ext/tipi/extconf.rb +0 -12
- data/ext/tipi/http1_parser.c +0 -534
- data/ext/tipi/http1_parser.h +0 -18
- data/ext/tipi/tipi_ext.c +0 -5
- data/lib/tipi/http1_adapter_new.rb +0 -293
@@ -36,7 +36,7 @@ module DigitalFabric
|
|
36
36
|
process_incoming_messages(false)
|
37
37
|
rescue GracefulShutdown
|
38
38
|
puts "Proxy got graceful shutdown, left: #{@requests.size} requests" if @requests.size > 0
|
39
|
-
process_incoming_messages(true)
|
39
|
+
move_on_after(15) { process_incoming_messages(true) }
|
40
40
|
ensure
|
41
41
|
# keep_alive_timer&.stop
|
42
42
|
unmount
|
@@ -56,11 +56,11 @@ module DigitalFabric
|
|
56
56
|
def unmount
|
57
57
|
return unless @mounted
|
58
58
|
|
59
|
-
@service.unmount(self)
|
59
|
+
@service.unmount(self)
|
60
60
|
@mounted = nil
|
61
61
|
end
|
62
62
|
|
63
|
-
def
|
63
|
+
def send_shutdown
|
64
64
|
send_df_message(Protocol.shutdown)
|
65
65
|
@fiber.raise GracefulShutdown.new
|
66
66
|
end
|
@@ -144,13 +144,20 @@ module DigitalFabric
|
|
144
144
|
t0 = Time.now
|
145
145
|
t1 = nil
|
146
146
|
with_request do |id|
|
147
|
-
|
147
|
+
msg = Protocol.http_request(id, req.headers, req.next_chunk(true), req.complete?)
|
148
|
+
send_df_message(msg)
|
148
149
|
while (message = receive)
|
150
|
+
kind = message[Protocol::Attribute::KIND]
|
149
151
|
unless t1
|
150
152
|
t1 = Time.now
|
151
|
-
|
153
|
+
if kind == Protocol::HTTP_RESPONSE
|
154
|
+
headers = message[Protocol::Attribute::HttpResponse::HEADERS]
|
155
|
+
status = (headers && headers[':status']) || 200
|
156
|
+
if status < Qeweney::Status::BAD_REQUEST
|
157
|
+
@service.record_latency_measurement(t1 - t0, req)
|
158
|
+
end
|
159
|
+
end
|
152
160
|
end
|
153
|
-
kind = message[Protocol::Attribute::KIND]
|
154
161
|
attributes = message[Protocol::Attribute::HttpRequest::HEADERS..-1]
|
155
162
|
return if http_request_message(id, req, kind, attributes)
|
156
163
|
end
|
@@ -240,7 +247,7 @@ module DigitalFabric
|
|
240
247
|
else
|
241
248
|
req.send_headers(headers) if headers && !req.headers_sent?
|
242
249
|
req.send_chunk(body, done: complete) if body or complete
|
243
|
-
|
250
|
+
|
244
251
|
if complete && transfer_count_key
|
245
252
|
rx, tx = req.transfer_counts
|
246
253
|
send_transfer_count(transfer_count_key, rx, tx)
|
@@ -284,7 +291,7 @@ module DigitalFabric
|
|
284
291
|
response = receive
|
285
292
|
case response[0]
|
286
293
|
when Protocol::WS_RESPONSE
|
287
|
-
headers = response[2] || {}
|
294
|
+
headers = response[2] || {}
|
288
295
|
status = headers[':status'] || Qeweney::Status::SWITCHING_PROTOCOLS
|
289
296
|
if status != Qeweney::Status::SWITCHING_PROTOCOLS
|
290
297
|
req.respond(nil, headers)
|
@@ -16,7 +16,7 @@ module DigitalFabric
|
|
16
16
|
@service.mount(route, self)
|
17
17
|
@current_request_count = 0
|
18
18
|
# @updater = spin_loop(:executive_updater, interval: 10) { update_service_stats }
|
19
|
-
|
19
|
+
update_service_stats
|
20
20
|
end
|
21
21
|
|
22
22
|
def current_request_count
|
@@ -33,9 +33,13 @@ module DigitalFabric
|
|
33
33
|
req.respond(message.to_json, { 'Content-Type' => 'text.json' })
|
34
34
|
when '/stream/stats'
|
35
35
|
stream_stats(req)
|
36
|
+
when '/upload'
|
37
|
+
req.respond("body: #{req.read.inspect}")
|
36
38
|
else
|
37
39
|
req.respond('Invalid path', { ':status' => Qeweney::Status::NOT_FOUND })
|
38
40
|
end
|
41
|
+
rescue => e
|
42
|
+
puts "Error: #{e.inspect}"
|
39
43
|
ensure
|
40
44
|
@current_request_count -= 1
|
41
45
|
end
|
@@ -43,7 +47,7 @@ module DigitalFabric
|
|
43
47
|
def stream_stats(req)
|
44
48
|
req.send_headers({ 'Content-Type' => 'text/event-stream' })
|
45
49
|
|
46
|
-
|
50
|
+
every(10) do
|
47
51
|
message = last_service_stats
|
48
52
|
req.send_chunk(format_sse_event(message.to_json))
|
49
53
|
end
|
@@ -79,7 +83,7 @@ module DigitalFabric
|
|
79
83
|
raise 'Invalid output from top (cpu)'
|
80
84
|
end
|
81
85
|
cpu_utilization = 100 - Regexp.last_match(1).to_i
|
82
|
-
|
86
|
+
|
83
87
|
unless top =~ TOP_MEM_REGEXP && Regexp.last_match(1) =~ TOP_MEM_FREE_REGEXP
|
84
88
|
raise 'Invalid output from top (mem)'
|
85
89
|
end
|
@@ -102,8 +102,8 @@ module DigitalFabric
|
|
102
102
|
DF_UPGRADE_RESPONSE
|
103
103
|
end
|
104
104
|
|
105
|
-
def http_request(id,
|
106
|
-
[ HTTP_REQUEST, id,
|
105
|
+
def http_request(id, headers, buffered_chunk, complete)
|
106
|
+
[ HTTP_REQUEST, id, headers, buffered_chunk, complete ]
|
107
107
|
end
|
108
108
|
|
109
109
|
def http_response(id, body, headers, complete, transfer_count_key = nil)
|
@@ -141,7 +141,7 @@ module DigitalFabric
|
|
141
141
|
def ws_data(id, data)
|
142
142
|
[ WS_DATA, id, data ]
|
143
143
|
end
|
144
|
-
|
144
|
+
|
145
145
|
def ws_close(id)
|
146
146
|
[ WS_CLOSE, id ]
|
147
147
|
end
|
@@ -17,10 +17,6 @@ module DigitalFabric
|
|
17
17
|
@agent.get_http_request_body(@id, 1)
|
18
18
|
end
|
19
19
|
|
20
|
-
def consume_request(request)
|
21
|
-
@agent.get_http_request_body(@id, nil)
|
22
|
-
end
|
23
|
-
|
24
20
|
def respond(request, body, headers)
|
25
21
|
@agent.send_df_message(
|
26
22
|
Protocol.http_response(@id, body, headers, true)
|
@@ -25,7 +25,7 @@ module DigitalFabric
|
|
25
25
|
@http_latency_max = 0
|
26
26
|
@last_counters = @counters.merge(stamp: Time.now.to_f - 1)
|
27
27
|
@fiber = Fiber.current
|
28
|
-
@timer = Polyphony::Timer.new('service_timer', resolution: 5)
|
28
|
+
# @timer = Polyphony::Timer.new('service_timer', resolution: 5)
|
29
29
|
end
|
30
30
|
|
31
31
|
def calculate_stats
|
@@ -50,6 +50,8 @@ module DigitalFabric
|
|
50
50
|
switch_rate = backend_stats[:switch_count] / elapsed
|
51
51
|
poll_rate = backend_stats[:poll_count] / elapsed
|
52
52
|
|
53
|
+
object_space_stats = ObjectSpace.count_objects
|
54
|
+
|
53
55
|
{
|
54
56
|
service: {
|
55
57
|
agent_count: @agents.size,
|
@@ -73,6 +75,8 @@ module DigitalFabric
|
|
73
75
|
process: {
|
74
76
|
cpu_usage: cpu,
|
75
77
|
rss: rss.to_f / 1024,
|
78
|
+
objects_total: object_space_stats[:TOTAL],
|
79
|
+
objects_free: object_space_stats[:FREE]
|
76
80
|
}
|
77
81
|
}
|
78
82
|
end
|
@@ -81,10 +85,12 @@ module DigitalFabric
|
|
81
85
|
s = `ps -p #{pid} -o %cpu,rss`
|
82
86
|
cpu, rss = s.lines[1].chomp.strip.split(' ')
|
83
87
|
[cpu.to_f, rss.to_i]
|
88
|
+
rescue Polyphony::BaseException
|
89
|
+
raise
|
84
90
|
rescue Exception
|
85
91
|
[nil, nil]
|
86
92
|
end
|
87
|
-
|
93
|
+
|
88
94
|
def get_stats
|
89
95
|
calculate_stats
|
90
96
|
end
|
@@ -117,14 +123,14 @@ module DigitalFabric
|
|
117
123
|
|
118
124
|
puts format('slow request (%.1f): %p', latency, req.headers)
|
119
125
|
end
|
120
|
-
|
126
|
+
|
121
127
|
def http_request(req, allow_df_upgrade = false)
|
122
128
|
@current_request_count += 1
|
123
129
|
@counters[:http_requests] += 1
|
124
130
|
@counters[:connections] += 1 if req.headers[':first']
|
125
131
|
|
126
132
|
return upgrade_request(req, allow_df_upgrade) if req.upgrade_protocol
|
127
|
-
|
133
|
+
|
128
134
|
inject_request_headers(req)
|
129
135
|
agent = find_agent(req)
|
130
136
|
unless agent
|
@@ -153,7 +159,7 @@ module DigitalFabric
|
|
153
159
|
req.headers['x-forwarded-for'] = conn.peeraddr(false)[2]
|
154
160
|
req.headers['x-forwarded-proto'] ||= conn.is_a?(OpenSSL::SSL::SSLSocket) ? 'https' : 'http'
|
155
161
|
end
|
156
|
-
|
162
|
+
|
157
163
|
def upgrade_request(req, allow_df_upgrade)
|
158
164
|
case (protocol = req.upgrade_protocol)
|
159
165
|
when 'df'
|
@@ -172,7 +178,7 @@ module DigitalFabric
|
|
172
178
|
agent.http_upgrade(req, protocol)
|
173
179
|
end
|
174
180
|
end
|
175
|
-
|
181
|
+
|
176
182
|
def df_upgrade(req)
|
177
183
|
# we don't want to count connected agents
|
178
184
|
@current_request_count -= 1
|
@@ -185,7 +191,7 @@ module DigitalFabric
|
|
185
191
|
ensure
|
186
192
|
@current_request_count += 1
|
187
193
|
end
|
188
|
-
|
194
|
+
|
189
195
|
def mount(route, agent)
|
190
196
|
if route[:path]
|
191
197
|
route[:path_regexp] = path_regexp(route[:path])
|
@@ -194,7 +200,7 @@ module DigitalFabric
|
|
194
200
|
@agents[agent] = route
|
195
201
|
@routing_changed = true
|
196
202
|
end
|
197
|
-
|
203
|
+
|
198
204
|
def unmount(agent)
|
199
205
|
route = @agents[agent]
|
200
206
|
return unless route
|
@@ -205,7 +211,7 @@ module DigitalFabric
|
|
205
211
|
end
|
206
212
|
|
207
213
|
INVALID_HOST = 'INVALID_HOST'
|
208
|
-
|
214
|
+
|
209
215
|
def find_agent(req)
|
210
216
|
compile_agent_routes if @routing_changed
|
211
217
|
|
@@ -231,13 +237,6 @@ module DigitalFabric
|
|
231
237
|
@route_keys = @routes.keys
|
232
238
|
end
|
233
239
|
|
234
|
-
def wait_for_agent(wait_list)
|
235
|
-
wait_list << Fiber.current
|
236
|
-
@timer.move_on_after(10) { suspend }
|
237
|
-
ensure
|
238
|
-
wait_list.delete(self)
|
239
|
-
end
|
240
|
-
|
241
240
|
def path_regexp(path)
|
242
241
|
/^#{path}/
|
243
242
|
end
|
@@ -245,8 +244,8 @@ module DigitalFabric
|
|
245
244
|
def graceful_shutdown
|
246
245
|
@shutdown = true
|
247
246
|
@agents.keys.each do |agent|
|
248
|
-
if agent.respond_to?(:
|
249
|
-
agent.
|
247
|
+
if agent.respond_to?(:send_shutdown)
|
248
|
+
agent.send_shutdown
|
250
249
|
else
|
251
250
|
@agents.delete(agent)
|
252
251
|
end
|
data/lib/tipi/handler.rb
CHANGED
@@ -20,14 +20,14 @@ module Tipi
|
|
20
20
|
ensure
|
21
21
|
socket.close
|
22
22
|
end
|
23
|
-
|
23
|
+
|
24
24
|
ALPN_PROTOCOLS = %w[h2 http/1.1].freeze
|
25
25
|
H2_PROTOCOL = 'h2'
|
26
26
|
|
27
27
|
def protocol_adapter(socket, opts)
|
28
28
|
use_http2 = socket.respond_to?(:alpn_protocol) &&
|
29
29
|
socket.alpn_protocol == H2_PROTOCOL
|
30
|
-
|
30
|
+
|
31
31
|
klass = use_http2 ? HTTP2Adapter : HTTP1Adapter
|
32
32
|
klass.new(socket, opts)
|
33
33
|
end
|
data/lib/tipi/http1_adapter.rb
CHANGED
@@ -1,9 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require '
|
4
|
-
require_relative './http2_adapter'
|
3
|
+
require 'h1p'
|
5
4
|
require 'qeweney/request'
|
6
5
|
|
6
|
+
require_relative './http2_adapter'
|
7
|
+
|
7
8
|
module Tipi
|
8
9
|
# HTTP1 protocol implementation
|
9
10
|
class HTTP1Adapter
|
@@ -14,127 +15,83 @@ module Tipi
|
|
14
15
|
@conn = conn
|
15
16
|
@opts = opts
|
16
17
|
@first = true
|
17
|
-
@parser = ::
|
18
|
+
@parser = H1P::Parser.new(@conn)
|
18
19
|
end
|
19
|
-
|
20
|
+
|
20
21
|
def each(&block)
|
21
|
-
|
22
|
-
|
22
|
+
while true
|
23
|
+
headers = @parser.parse_headers
|
24
|
+
break unless headers
|
25
|
+
|
26
|
+
# handle_request returns true if connection is not persistent or was
|
27
|
+
# upgraded
|
28
|
+
break if handle_request(headers, &block)
|
23
29
|
end
|
30
|
+
rescue H1P::Error, ArgumentError
|
31
|
+
# an ArgumentError might be raised in the parser if an invalid input
|
32
|
+
# string is given as the HTTP method (String#upcase will raise on invalid HTTP string)
|
33
|
+
#
|
34
|
+
# ignore
|
24
35
|
rescue SystemCallError, IOError
|
25
36
|
# ignore
|
26
37
|
ensure
|
27
38
|
finalize_client_loop
|
28
39
|
end
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
@first = nil
|
39
|
-
end
|
40
|
-
return true if upgrade_connection(request.headers, &block)
|
41
|
-
|
42
|
-
@requests_head = request.__next__
|
43
|
-
block.call(request)
|
44
|
-
return true unless request.keep_alive?
|
40
|
+
|
41
|
+
def handle_request(headers, &block)
|
42
|
+
scheme = (proto = headers['x-forwarded-proto']) ?
|
43
|
+
proto.downcase : scheme_from_connection
|
44
|
+
headers[':scheme'] = scheme
|
45
|
+
@protocol = headers[':protocol']
|
46
|
+
if @first
|
47
|
+
headers[':first'] = true
|
48
|
+
@first = nil
|
45
49
|
end
|
46
|
-
|
50
|
+
|
51
|
+
return true if upgrade_connection(headers, &block)
|
52
|
+
|
53
|
+
request = Qeweney::Request.new(headers, self)
|
54
|
+
if !@parser.complete?
|
55
|
+
request.buffer_body_chunk(@parser.read_body_chunk(true))
|
56
|
+
end
|
57
|
+
block.call(request)
|
58
|
+
return !persistent_connection?(headers)
|
47
59
|
end
|
48
|
-
|
60
|
+
|
61
|
+
def persistent_connection?(headers)
|
62
|
+
if headers[':protocol'] == 'http/1.1'
|
63
|
+
return headers['connection'] != 'close'
|
64
|
+
else
|
65
|
+
connection = headers['connection']
|
66
|
+
return connection && connection != 'close'
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
49
70
|
def finalize_client_loop
|
50
|
-
# release references to various objects
|
51
|
-
@requests_head = @requests_tail = nil
|
52
71
|
@parser = nil
|
53
72
|
@splicing_pipe = nil
|
54
73
|
@conn.shutdown if @conn.respond_to?(:shutdown) rescue nil
|
55
74
|
@conn.close
|
56
75
|
end
|
57
|
-
|
76
|
+
|
58
77
|
# Reads a body chunk for the current request. Transfers control to the parse
|
59
78
|
# loop, and resumes once the parse_loop has fired the on_body callback
|
60
|
-
def get_body_chunk(request)
|
61
|
-
@
|
62
|
-
@next_chunk = nil
|
63
|
-
while !@requests_tail.complete? && (data = @conn.readpartial(8192))
|
64
|
-
request.rx_incr(data.bytesize)
|
65
|
-
@parser << data
|
66
|
-
return @next_chunk if @next_chunk
|
67
|
-
|
68
|
-
snooze
|
69
|
-
end
|
70
|
-
nil
|
71
|
-
ensure
|
72
|
-
@waiting_for_body_chunk = nil
|
73
|
-
end
|
74
|
-
|
75
|
-
# Waits for the current request to complete. Transfers control to the parse
|
76
|
-
# loop, and resumes once the parse_loop has fired the on_message_complete
|
77
|
-
# callback
|
78
|
-
def consume_request(request)
|
79
|
-
request = @requests_head
|
80
|
-
@conn.recv_loop do |data|
|
81
|
-
request.rx_incr(data.bytesize)
|
82
|
-
@parser << data
|
83
|
-
return if request.complete?
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
def protocol
|
88
|
-
version = @parser.http_version
|
89
|
-
"HTTP #{version.join('.')}"
|
90
|
-
end
|
91
|
-
|
92
|
-
def on_headers_complete(headers)
|
93
|
-
headers = normalize_headers(headers)
|
94
|
-
headers[':path'] = @parser.request_url
|
95
|
-
headers[':method'] = @parser.http_method.downcase
|
96
|
-
scheme = (proto = headers['x-forwarded-proto']) ?
|
97
|
-
proto.downcase : scheme_from_connection
|
98
|
-
headers[':scheme'] = scheme
|
99
|
-
queue_request(Qeweney::Request.new(headers, self))
|
79
|
+
def get_body_chunk(request, buffered_only = false)
|
80
|
+
@parser.read_body_chunk(buffered_only)
|
100
81
|
end
|
101
82
|
|
102
|
-
def
|
103
|
-
|
104
|
-
k = k.downcase
|
105
|
-
hk = h[k]
|
106
|
-
if hk
|
107
|
-
hk = h[k] = [hk] unless hk.is_a?(Array)
|
108
|
-
v.is_a?(Array) ? hk.concat(v) : hk << v
|
109
|
-
else
|
110
|
-
h[k] = v
|
111
|
-
end
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
|
-
def queue_request(request)
|
116
|
-
if @requests_head
|
117
|
-
@requests_tail.__next__ = request
|
118
|
-
@requests_tail = request
|
119
|
-
else
|
120
|
-
@requests_head = @requests_tail = request
|
121
|
-
end
|
83
|
+
def get_body(request)
|
84
|
+
@parser.read_body
|
122
85
|
end
|
123
|
-
|
124
|
-
def
|
125
|
-
|
126
|
-
@next_chunk = chunk
|
127
|
-
@waiting_for_body_chunk = nil
|
128
|
-
else
|
129
|
-
@requests_tail.buffer_body_chunk(chunk)
|
130
|
-
end
|
86
|
+
|
87
|
+
def complete?(request)
|
88
|
+
@parser.complete?
|
131
89
|
end
|
132
|
-
|
133
|
-
def
|
134
|
-
@
|
135
|
-
@requests_tail.complete!(@parser.keep_alive?)
|
90
|
+
|
91
|
+
def protocol
|
92
|
+
@protocol
|
136
93
|
end
|
137
|
-
|
94
|
+
|
138
95
|
# Upgrades the connection to a different protocol, if the 'Upgrade' header is
|
139
96
|
# given. By default the only supported upgrade protocol is HTTP2. Additional
|
140
97
|
# protocols, notably WebSocket, can be specified by passing a hash to the
|
@@ -160,27 +117,28 @@ module Tipi
|
|
160
117
|
def upgrade_connection(headers, &block)
|
161
118
|
upgrade_protocol = headers['upgrade']
|
162
119
|
return nil unless upgrade_protocol
|
163
|
-
|
120
|
+
|
164
121
|
upgrade_protocol = upgrade_protocol.downcase.to_sym
|
165
122
|
upgrade_handler = @opts[:upgrade] && @opts[:upgrade][upgrade_protocol]
|
166
123
|
return upgrade_with_handler(upgrade_handler, headers) if upgrade_handler
|
167
124
|
return upgrade_to_http2(headers, &block) if upgrade_protocol == :h2c
|
168
|
-
|
125
|
+
|
169
126
|
nil
|
170
127
|
end
|
171
|
-
|
128
|
+
|
172
129
|
def upgrade_with_handler(handler, headers)
|
173
|
-
@parser =
|
130
|
+
@parser = nil
|
174
131
|
handler.(self, headers)
|
175
132
|
true
|
176
133
|
end
|
177
|
-
|
134
|
+
|
178
135
|
def upgrade_to_http2(headers, &block)
|
179
|
-
|
180
|
-
|
136
|
+
headers = http2_upgraded_headers(headers)
|
137
|
+
body = @parser.read_body
|
138
|
+
HTTP2Adapter.upgrade_each(@conn, @opts, headers, body, &block)
|
181
139
|
true
|
182
140
|
end
|
183
|
-
|
141
|
+
|
184
142
|
# Returns headers for HTTP2 upgrade
|
185
143
|
# @param headers [Hash] request headers
|
186
144
|
# @return [Hash] headers for HTTP2 upgrade
|
@@ -198,10 +156,10 @@ module Tipi
|
|
198
156
|
def scheme_from_connection
|
199
157
|
@conn.is_a?(OpenSSL::SSL::SSLSocket) ? 'https' : 'http'
|
200
158
|
end
|
201
|
-
|
159
|
+
|
202
160
|
# response API
|
203
161
|
|
204
|
-
CRLF = "\r\n"
|
162
|
+
CRLF = "\r\n"
|
205
163
|
CRLF_ZERO_CRLF_CRLF = "\r\n0\r\n\r\n"
|
206
164
|
|
207
165
|
# Sends response including headers and body. Waits for the request to complete
|
@@ -210,7 +168,6 @@ module Tipi
|
|
210
168
|
# @param body [String] response body
|
211
169
|
# @param headers
|
212
170
|
def respond(request, body, headers)
|
213
|
-
consume_request(request) if @parsing
|
214
171
|
formatted_headers = format_headers(headers, body, false)
|
215
172
|
request.tx_incr(formatted_headers.bytesize + (body ? body.bytesize : 0))
|
216
173
|
if body
|
@@ -220,19 +177,19 @@ module Tipi
|
|
220
177
|
end
|
221
178
|
end
|
222
179
|
|
223
|
-
|
224
|
-
consume_request(request) if @parsing
|
180
|
+
CHUNK_LENGTH_PROC = ->(len) { "#{len.to_s(16)}\r\n" }
|
225
181
|
|
182
|
+
def respond_from_io(request, io, headers, chunk_size = 2**14)
|
226
183
|
formatted_headers = format_headers(headers, true, true)
|
227
184
|
request.tx_incr(formatted_headers.bytesize)
|
228
|
-
|
185
|
+
|
229
186
|
# assume chunked encoding
|
230
187
|
Thread.current.backend.splice_chunks(
|
231
188
|
io,
|
232
189
|
@conn,
|
233
190
|
formatted_headers,
|
234
191
|
"0\r\n\r\n",
|
235
|
-
|
192
|
+
CHUNK_LENGTH_PROC,
|
236
193
|
"\r\n",
|
237
194
|
chunk_size
|
238
195
|
)
|
@@ -246,11 +203,15 @@ module Tipi
|
|
246
203
|
# @param chunked [boolean] whether to use chunked transfer encoding
|
247
204
|
# @return [void]
|
248
205
|
def send_headers(request, headers, empty_response: false, chunked: true)
|
249
|
-
formatted_headers = format_headers(headers, !empty_response,
|
206
|
+
formatted_headers = format_headers(headers, !empty_response, http1_1?(request) && chunked)
|
250
207
|
request.tx_incr(formatted_headers.bytesize)
|
251
208
|
@conn.write(formatted_headers)
|
252
209
|
end
|
253
|
-
|
210
|
+
|
211
|
+
def http1_1?(request)
|
212
|
+
request.headers[':protocol'] == 'http/1.1'
|
213
|
+
end
|
214
|
+
|
254
215
|
# Sends a response body chunk. If no headers were sent, default headers are
|
255
216
|
# sent using #send_headers. if the done option is true(thy), an empty chunk
|
256
217
|
# will be sent to signal response completion to the client.
|
@@ -267,7 +228,7 @@ module Tipi
|
|
267
228
|
request.tx_incr(data.bytesize)
|
268
229
|
@conn.write(data)
|
269
230
|
end
|
270
|
-
|
231
|
+
|
271
232
|
def send_chunk_from_io(request, io, r, w, chunk_size)
|
272
233
|
len = w.splice(io, chunk_size)
|
273
234
|
if len > 0
|
@@ -289,12 +250,12 @@ module Tipi
|
|
289
250
|
request.tx_incr(5)
|
290
251
|
@conn << "0\r\n\r\n"
|
291
252
|
end
|
292
|
-
|
253
|
+
|
293
254
|
def close
|
294
255
|
@conn.shutdown if @conn.respond_to?(:shutdown) rescue nil
|
295
256
|
@conn.close
|
296
257
|
end
|
297
|
-
|
258
|
+
|
298
259
|
private
|
299
260
|
|
300
261
|
INTERNAL_HEADER_REGEXP = /^:/.freeze
|
@@ -311,13 +272,13 @@ module Tipi
|
|
311
272
|
lines = format_status_line(body, status, chunked)
|
312
273
|
headers.each do |k, v|
|
313
274
|
next if k =~ INTERNAL_HEADER_REGEXP
|
314
|
-
|
275
|
+
|
315
276
|
collect_header_lines(lines, k, v)
|
316
277
|
end
|
317
278
|
lines << CRLF
|
318
279
|
lines
|
319
280
|
end
|
320
|
-
|
281
|
+
|
321
282
|
def format_status_line(body, status, chunked)
|
322
283
|
if !body
|
323
284
|
empty_status_line(status)
|
@@ -325,7 +286,7 @@ module Tipi
|
|
325
286
|
with_body_status_line(status, body, chunked)
|
326
287
|
end
|
327
288
|
end
|
328
|
-
|
289
|
+
|
329
290
|
def empty_status_line(status)
|
330
291
|
if status == 204
|
331
292
|
+"HTTP/1.1 #{status}\r\n"
|
@@ -333,7 +294,7 @@ module Tipi
|
|
333
294
|
+"HTTP/1.1 #{status}\r\nContent-Length: 0\r\n"
|
334
295
|
end
|
335
296
|
end
|
336
|
-
|
297
|
+
|
337
298
|
def with_body_status_line(status, body, chunked)
|
338
299
|
if chunked
|
339
300
|
+"HTTP/1.1 #{status}\r\nTransfer-Encoding: chunked\r\n"
|