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
|
@@ -15,7 +15,7 @@ module DigitalFabric
|
|
|
15
15
|
route[:executive] = true
|
|
16
16
|
@service.mount(route, self)
|
|
17
17
|
@current_request_count = 0
|
|
18
|
-
@updater = spin_loop(interval: 10) { update_service_stats }
|
|
18
|
+
# @updater = spin_loop(:executive_updater, interval: 10) { update_service_stats }
|
|
19
19
|
update_service_stats
|
|
20
20
|
end
|
|
21
21
|
|
|
@@ -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
|
|
@@ -22,6 +22,9 @@ module DigitalFabric
|
|
|
22
22
|
|
|
23
23
|
TRANSFER_COUNT = 'transfer_count'
|
|
24
24
|
|
|
25
|
+
STATS_REQUEST = 'stats_request'
|
|
26
|
+
STATS_RESPONSE = 'stats_response'
|
|
27
|
+
|
|
25
28
|
SEND_TIMEOUT = 15
|
|
26
29
|
RECV_TIMEOUT = SEND_TIMEOUT + 5
|
|
27
30
|
|
|
@@ -69,6 +72,10 @@ module DigitalFabric
|
|
|
69
72
|
RX = 2
|
|
70
73
|
TX = 3
|
|
71
74
|
end
|
|
75
|
+
|
|
76
|
+
module Stats
|
|
77
|
+
STATS = 2
|
|
78
|
+
end
|
|
72
79
|
end
|
|
73
80
|
|
|
74
81
|
class << self
|
|
@@ -95,8 +102,8 @@ module DigitalFabric
|
|
|
95
102
|
DF_UPGRADE_RESPONSE
|
|
96
103
|
end
|
|
97
104
|
|
|
98
|
-
def http_request(id,
|
|
99
|
-
[ HTTP_REQUEST, id,
|
|
105
|
+
def http_request(id, headers, buffered_chunk, complete)
|
|
106
|
+
[ HTTP_REQUEST, id, headers, buffered_chunk, complete ]
|
|
100
107
|
end
|
|
101
108
|
|
|
102
109
|
def http_response(id, body, headers, complete, transfer_count_key = nil)
|
|
@@ -136,12 +143,20 @@ module DigitalFabric
|
|
|
136
143
|
end
|
|
137
144
|
|
|
138
145
|
def ws_close(id)
|
|
139
|
-
[WS_CLOSE, id ]
|
|
146
|
+
[ WS_CLOSE, id ]
|
|
140
147
|
end
|
|
141
148
|
|
|
142
149
|
def transfer_count(key, rx, tx)
|
|
143
150
|
[ TRANSFER_COUNT, key, rx, tx ]
|
|
144
151
|
end
|
|
152
|
+
|
|
153
|
+
def stats_request(id)
|
|
154
|
+
[ STATS_REQUEST, id ]
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def stats_response(id, stats)
|
|
158
|
+
[ STATS_RESPONSE, id, stats ]
|
|
159
|
+
end
|
|
145
160
|
end
|
|
146
161
|
end
|
|
147
162
|
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)
|
|
@@ -13,26 +13,22 @@ module DigitalFabric
|
|
|
13
13
|
@token = token
|
|
14
14
|
@agents = {}
|
|
15
15
|
@routes = {}
|
|
16
|
-
@waiting_lists = {} # hash mapping routes to arrays of requests waiting for an agent to mount
|
|
17
16
|
@counters = {
|
|
18
17
|
connections: 0,
|
|
19
18
|
http_requests: 0,
|
|
20
19
|
errors: 0
|
|
21
20
|
}
|
|
22
21
|
@connection_count = 0
|
|
22
|
+
@current_request_count = 0
|
|
23
23
|
@http_latency_accumulator = 0
|
|
24
24
|
@http_latency_counter = 0
|
|
25
|
+
@http_latency_max = 0
|
|
25
26
|
@last_counters = @counters.merge(stamp: Time.now.to_f - 1)
|
|
26
27
|
@fiber = Fiber.current
|
|
27
|
-
@timer = Polyphony::Timer.new(resolution:
|
|
28
|
-
|
|
29
|
-
stats_updater = spin { @timer.every(10) { update_stats } }
|
|
30
|
-
@stats = {}
|
|
31
|
-
|
|
32
|
-
@current_request_count = 0
|
|
28
|
+
# @timer = Polyphony::Timer.new('service_timer', resolution: 5)
|
|
33
29
|
end
|
|
34
30
|
|
|
35
|
-
def
|
|
31
|
+
def calculate_stats
|
|
36
32
|
now = Time.now.to_f
|
|
37
33
|
elapsed = now - @last_counters[:stamp]
|
|
38
34
|
connections = @counters[:connections] - @last_counters[:connections]
|
|
@@ -40,23 +36,65 @@ module DigitalFabric
|
|
|
40
36
|
errors = @counters[:errors] - @last_counters[:errors]
|
|
41
37
|
@last_counters = @counters.merge(stamp: now)
|
|
42
38
|
|
|
43
|
-
average_latency = @http_latency_counter
|
|
44
|
-
@http_latency_accumulator / @http_latency_counter
|
|
45
|
-
0
|
|
39
|
+
average_latency = @http_latency_counter == 0 ? 0 :
|
|
40
|
+
@http_latency_accumulator / @http_latency_counter
|
|
46
41
|
@http_latency_accumulator = 0
|
|
47
42
|
@http_latency_counter = 0
|
|
48
|
-
|
|
49
|
-
@
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
43
|
+
max_latency = @http_latency_max
|
|
44
|
+
@http_latency_max = 0
|
|
45
|
+
|
|
46
|
+
cpu, rss = pid_cpu_and_rss(Process.pid)
|
|
47
|
+
|
|
48
|
+
backend_stats = Thread.backend.stats
|
|
49
|
+
op_rate = backend_stats[:op_count] / elapsed
|
|
50
|
+
switch_rate = backend_stats[:switch_count] / elapsed
|
|
51
|
+
poll_rate = backend_stats[:poll_count] / elapsed
|
|
52
|
+
|
|
53
|
+
object_space_stats = ObjectSpace.count_objects
|
|
54
|
+
|
|
55
|
+
{
|
|
56
|
+
service: {
|
|
57
|
+
agent_count: @agents.size,
|
|
58
|
+
connection_count: @connection_count,
|
|
59
|
+
connection_rate: connections / elapsed,
|
|
60
|
+
error_rate: errors / elapsed,
|
|
61
|
+
http_request_rate: http_requests / elapsed,
|
|
62
|
+
latency_avg: average_latency,
|
|
63
|
+
latency_max: max_latency,
|
|
64
|
+
pending_requests: @current_request_count,
|
|
65
|
+
},
|
|
66
|
+
backend: {
|
|
67
|
+
op_rate: op_rate,
|
|
68
|
+
pending_ops: backend_stats[:pending_ops],
|
|
69
|
+
poll_rate: poll_rate,
|
|
70
|
+
runqueue_size: backend_stats[:runqueue_size],
|
|
71
|
+
runqueue_high_watermark: backend_stats[:runqueue_max_length],
|
|
72
|
+
switch_rate: switch_rate,
|
|
73
|
+
|
|
74
|
+
},
|
|
75
|
+
process: {
|
|
76
|
+
cpu_usage: cpu,
|
|
77
|
+
rss: rss.to_f / 1024,
|
|
78
|
+
objects_total: object_space_stats[:TOTAL],
|
|
79
|
+
objects_free: object_space_stats[:FREE]
|
|
80
|
+
}
|
|
57
81
|
}
|
|
58
82
|
end
|
|
59
83
|
|
|
84
|
+
def pid_cpu_and_rss(pid)
|
|
85
|
+
s = `ps -p #{pid} -o %cpu,rss`
|
|
86
|
+
cpu, rss = s.lines[1].chomp.strip.split(' ')
|
|
87
|
+
[cpu.to_f, rss.to_i]
|
|
88
|
+
rescue Polyphony::BaseException
|
|
89
|
+
raise
|
|
90
|
+
rescue Exception
|
|
91
|
+
[nil, nil]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def get_stats
|
|
95
|
+
calculate_stats
|
|
96
|
+
end
|
|
97
|
+
|
|
60
98
|
def incr_connection_count
|
|
61
99
|
@connection_count += 1
|
|
62
100
|
end
|
|
@@ -77,23 +115,25 @@ module DigitalFabric
|
|
|
77
115
|
count
|
|
78
116
|
end
|
|
79
117
|
|
|
80
|
-
def record_latency_measurement(latency)
|
|
118
|
+
def record_latency_measurement(latency, req)
|
|
81
119
|
@http_latency_accumulator += latency
|
|
82
120
|
@http_latency_counter += 1
|
|
121
|
+
@http_latency_max = latency if latency > @http_latency_max
|
|
122
|
+
return if latency < 1.0
|
|
123
|
+
|
|
124
|
+
puts format('slow request (%.1f): %p', latency, req.headers)
|
|
83
125
|
end
|
|
84
126
|
|
|
85
|
-
def http_request(req)
|
|
127
|
+
def http_request(req, allow_df_upgrade = false)
|
|
86
128
|
@current_request_count += 1
|
|
87
129
|
@counters[:http_requests] += 1
|
|
88
130
|
@counters[:connections] += 1 if req.headers[':first']
|
|
89
131
|
|
|
90
|
-
return upgrade_request(req) if req.upgrade_protocol
|
|
132
|
+
return upgrade_request(req, allow_df_upgrade) if req.upgrade_protocol
|
|
91
133
|
|
|
92
134
|
inject_request_headers(req)
|
|
93
135
|
agent = find_agent(req)
|
|
94
136
|
unless agent
|
|
95
|
-
return req.respond('pong') if req.query[:q] == 'ping'
|
|
96
|
-
|
|
97
137
|
@counters[:errors] += 1
|
|
98
138
|
return req.respond(nil, ':status' => Qeweney::Status::SERVICE_UNAVAILABLE)
|
|
99
139
|
end
|
|
@@ -120,10 +160,14 @@ module DigitalFabric
|
|
|
120
160
|
req.headers['x-forwarded-proto'] ||= conn.is_a?(OpenSSL::SSL::SSLSocket) ? 'https' : 'http'
|
|
121
161
|
end
|
|
122
162
|
|
|
123
|
-
def upgrade_request(req)
|
|
163
|
+
def upgrade_request(req, allow_df_upgrade)
|
|
124
164
|
case (protocol = req.upgrade_protocol)
|
|
125
165
|
when 'df'
|
|
126
|
-
|
|
166
|
+
if allow_df_upgrade
|
|
167
|
+
df_upgrade(req)
|
|
168
|
+
else
|
|
169
|
+
req.respond(nil, ':status' => Qeweney::Status::SERVICE_UNAVAILABLE)
|
|
170
|
+
end
|
|
127
171
|
else
|
|
128
172
|
agent = find_agent(req)
|
|
129
173
|
unless agent
|
|
@@ -136,12 +180,16 @@ module DigitalFabric
|
|
|
136
180
|
end
|
|
137
181
|
|
|
138
182
|
def df_upgrade(req)
|
|
183
|
+
# we don't want to count connected agents
|
|
184
|
+
@current_request_count -= 1
|
|
139
185
|
if req.headers['df-token'] != @token
|
|
140
186
|
return req.respond(nil, ':status' => Qeweney::Status::FORBIDDEN)
|
|
141
187
|
end
|
|
142
188
|
|
|
143
189
|
req.adapter.conn << Protocol.df_upgrade_response
|
|
144
190
|
AgentProxy.new(self, req)
|
|
191
|
+
ensure
|
|
192
|
+
@current_request_count += 1
|
|
145
193
|
end
|
|
146
194
|
|
|
147
195
|
def mount(route, agent)
|
|
@@ -151,11 +199,6 @@ module DigitalFabric
|
|
|
151
199
|
@executive = agent if route[:executive]
|
|
152
200
|
@agents[agent] = route
|
|
153
201
|
@routing_changed = true
|
|
154
|
-
|
|
155
|
-
if (waiting = @waiting_lists[route])
|
|
156
|
-
waiting.each { |f| f.schedule(agent) }
|
|
157
|
-
@waiting_lists.delete(route)
|
|
158
|
-
end
|
|
159
202
|
end
|
|
160
203
|
|
|
161
204
|
def unmount(agent)
|
|
@@ -165,8 +208,6 @@ module DigitalFabric
|
|
|
165
208
|
@executive = nil if route[:executive]
|
|
166
209
|
@agents.delete(agent)
|
|
167
210
|
@routing_changed = true
|
|
168
|
-
|
|
169
|
-
@waiting_lists[route] ||= []
|
|
170
211
|
end
|
|
171
212
|
|
|
172
213
|
INVALID_HOST = 'INVALID_HOST'
|
|
@@ -182,12 +223,6 @@ module DigitalFabric
|
|
|
182
223
|
end
|
|
183
224
|
return @routes[route] if route
|
|
184
225
|
|
|
185
|
-
# # search for a known route for an agent that recently unmounted
|
|
186
|
-
# route, wait_list = @waiting_lists.find do |route, _|
|
|
187
|
-
# (host == route[:host]) || (path =~ route[:path_regexp])
|
|
188
|
-
# end
|
|
189
|
-
# return wait_for_agent(wait_list) if route
|
|
190
|
-
|
|
191
226
|
nil
|
|
192
227
|
end
|
|
193
228
|
|
|
@@ -202,13 +237,6 @@ module DigitalFabric
|
|
|
202
237
|
@route_keys = @routes.keys
|
|
203
238
|
end
|
|
204
239
|
|
|
205
|
-
def wait_for_agent(wait_list)
|
|
206
|
-
wait_list << Fiber.current
|
|
207
|
-
@timer.move_on_after(10) { suspend }
|
|
208
|
-
ensure
|
|
209
|
-
wait_list.delete(self)
|
|
210
|
-
end
|
|
211
|
-
|
|
212
240
|
def path_regexp(path)
|
|
213
241
|
/^#{path}/
|
|
214
242
|
end
|
|
@@ -216,8 +244,8 @@ module DigitalFabric
|
|
|
216
244
|
def graceful_shutdown
|
|
217
245
|
@shutdown = true
|
|
218
246
|
@agents.keys.each do |agent|
|
|
219
|
-
if agent.respond_to?(:
|
|
220
|
-
agent.
|
|
247
|
+
if agent.respond_to?(:send_shutdown)
|
|
248
|
+
agent.send_shutdown
|
|
221
249
|
else
|
|
222
250
|
@agents.delete(agent)
|
|
223
251
|
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,123 +15,78 @@ 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
|
|
31
|
+
# ignore
|
|
24
32
|
rescue SystemCallError, IOError
|
|
25
33
|
# ignore
|
|
26
34
|
ensure
|
|
27
35
|
finalize_client_loop
|
|
28
36
|
end
|
|
29
37
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
38
|
+
def handle_request(headers, &block)
|
|
39
|
+
scheme = (proto = headers['x-forwarded-proto']) ?
|
|
40
|
+
proto.downcase : scheme_from_connection
|
|
41
|
+
headers[':scheme'] = scheme
|
|
42
|
+
@protocol = headers[':protocol']
|
|
43
|
+
if @first
|
|
44
|
+
headers[':first'] = true
|
|
45
|
+
@first = nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
return true if upgrade_connection(headers, &block)
|
|
49
|
+
|
|
50
|
+
request = Qeweney::Request.new(headers, self)
|
|
51
|
+
if !@parser.complete?
|
|
52
|
+
request.buffer_body_chunk(@parser.read_body_chunk(true))
|
|
53
|
+
end
|
|
54
|
+
block.call(request)
|
|
55
|
+
return !persistent_connection?(headers)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def persistent_connection?(headers)
|
|
59
|
+
if headers[':protocol'] == 'http/1.1'
|
|
60
|
+
return headers['connection'] != 'close'
|
|
61
|
+
else
|
|
62
|
+
connection = headers['connection']
|
|
63
|
+
return connection && connection != 'close'
|
|
45
64
|
end
|
|
46
|
-
nil
|
|
47
65
|
end
|
|
48
66
|
|
|
49
67
|
def finalize_client_loop
|
|
50
|
-
# release references to various objects
|
|
51
|
-
@requests_head = @requests_tail = nil
|
|
52
68
|
@parser = nil
|
|
69
|
+
@splicing_pipe = nil
|
|
70
|
+
@conn.shutdown if @conn.respond_to?(:shutdown) rescue nil
|
|
53
71
|
@conn.close
|
|
54
72
|
end
|
|
55
73
|
|
|
56
74
|
# Reads a body chunk for the current request. Transfers control to the parse
|
|
57
75
|
# loop, and resumes once the parse_loop has fired the on_body callback
|
|
58
|
-
def get_body_chunk(request)
|
|
59
|
-
@
|
|
60
|
-
@next_chunk = nil
|
|
61
|
-
while !@requests_tail.complete? && (data = @conn.readpartial(8192))
|
|
62
|
-
request.rx_incr(data.bytesize)
|
|
63
|
-
@parser << data
|
|
64
|
-
return @next_chunk if @next_chunk
|
|
65
|
-
|
|
66
|
-
snooze
|
|
67
|
-
end
|
|
68
|
-
nil
|
|
69
|
-
ensure
|
|
70
|
-
@waiting_for_body_chunk = nil
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
# Waits for the current request to complete. Transfers control to the parse
|
|
74
|
-
# loop, and resumes once the parse_loop has fired the on_message_complete
|
|
75
|
-
# callback
|
|
76
|
-
def consume_request(request)
|
|
77
|
-
request = @requests_head
|
|
78
|
-
@conn.recv_loop do |data|
|
|
79
|
-
request.rx_incr(data.bytesize)
|
|
80
|
-
@parser << data
|
|
81
|
-
return if request.complete?
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def protocol
|
|
86
|
-
version = @parser.http_version
|
|
87
|
-
"HTTP #{version.join('.')}"
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def on_headers_complete(headers)
|
|
91
|
-
headers = normalize_headers(headers)
|
|
92
|
-
headers[':path'] = @parser.request_url
|
|
93
|
-
headers[':method'] = @parser.http_method.downcase
|
|
94
|
-
scheme = (proto = headers['x-forwarded-proto']) ?
|
|
95
|
-
proto.downcase : scheme_from_connection
|
|
96
|
-
headers[':scheme'] = scheme
|
|
97
|
-
queue_request(Qeweney::Request.new(headers, self))
|
|
76
|
+
def get_body_chunk(request, buffered_only = false)
|
|
77
|
+
@parser.read_body_chunk(buffered_only)
|
|
98
78
|
end
|
|
99
79
|
|
|
100
|
-
def
|
|
101
|
-
|
|
102
|
-
k = k.downcase
|
|
103
|
-
hk = h[k]
|
|
104
|
-
if hk
|
|
105
|
-
hk = h[k] = [hk] unless hk.is_a?(Array)
|
|
106
|
-
v.is_a?(Array) ? hk.concat(v) : hk << v
|
|
107
|
-
else
|
|
108
|
-
h[k] = v
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def queue_request(request)
|
|
114
|
-
if @requests_head
|
|
115
|
-
@requests_tail.__next__ = request
|
|
116
|
-
@requests_tail = request
|
|
117
|
-
else
|
|
118
|
-
@requests_head = @requests_tail = request
|
|
119
|
-
end
|
|
80
|
+
def get_body(request)
|
|
81
|
+
@parser.read_body
|
|
120
82
|
end
|
|
121
|
-
|
|
122
|
-
def
|
|
123
|
-
|
|
124
|
-
@next_chunk = chunk
|
|
125
|
-
@waiting_for_body_chunk = nil
|
|
126
|
-
else
|
|
127
|
-
@requests_tail.buffer_body_chunk(chunk)
|
|
128
|
-
end
|
|
83
|
+
|
|
84
|
+
def complete?(request)
|
|
85
|
+
@parser.complete?
|
|
129
86
|
end
|
|
130
87
|
|
|
131
|
-
def
|
|
132
|
-
@
|
|
133
|
-
@requests_tail.complete!(@parser.keep_alive?)
|
|
88
|
+
def protocol
|
|
89
|
+
@protocol
|
|
134
90
|
end
|
|
135
91
|
|
|
136
92
|
# Upgrades the connection to a different protocol, if the 'Upgrade' header is
|
|
@@ -168,14 +124,15 @@ module Tipi
|
|
|
168
124
|
end
|
|
169
125
|
|
|
170
126
|
def upgrade_with_handler(handler, headers)
|
|
171
|
-
@parser =
|
|
127
|
+
@parser = nil
|
|
172
128
|
handler.(self, headers)
|
|
173
129
|
true
|
|
174
130
|
end
|
|
175
131
|
|
|
176
132
|
def upgrade_to_http2(headers, &block)
|
|
177
|
-
|
|
178
|
-
|
|
133
|
+
headers = http2_upgraded_headers(headers)
|
|
134
|
+
body = @parser.read_body
|
|
135
|
+
HTTP2Adapter.upgrade_each(@conn, @opts, headers, body, &block)
|
|
179
136
|
true
|
|
180
137
|
end
|
|
181
138
|
|
|
@@ -208,7 +165,6 @@ module Tipi
|
|
|
208
165
|
# @param body [String] response body
|
|
209
166
|
# @param headers
|
|
210
167
|
def respond(request, body, headers)
|
|
211
|
-
consume_request(request) if @parsing
|
|
212
168
|
formatted_headers = format_headers(headers, body, false)
|
|
213
169
|
request.tx_incr(formatted_headers.bytesize + (body ? body.bytesize : 0))
|
|
214
170
|
if body
|
|
@@ -217,7 +173,23 @@ module Tipi
|
|
|
217
173
|
@conn.write(formatted_headers)
|
|
218
174
|
end
|
|
219
175
|
end
|
|
176
|
+
|
|
177
|
+
def respond_from_io(request, io, headers, chunk_size = 2**14)
|
|
178
|
+
formatted_headers = format_headers(headers, true, true)
|
|
179
|
+
request.tx_incr(formatted_headers.bytesize)
|
|
220
180
|
|
|
181
|
+
# assume chunked encoding
|
|
182
|
+
Thread.current.backend.splice_chunks(
|
|
183
|
+
io,
|
|
184
|
+
@conn,
|
|
185
|
+
formatted_headers,
|
|
186
|
+
"0\r\n\r\n",
|
|
187
|
+
->(len) { "#{len.to_s(16)}\r\n" },
|
|
188
|
+
"\r\n",
|
|
189
|
+
chunk_size
|
|
190
|
+
)
|
|
191
|
+
end
|
|
192
|
+
|
|
221
193
|
# Sends response headers. If empty_response is truthy, the response status
|
|
222
194
|
# code will default to 204, otherwise to 200.
|
|
223
195
|
# @param request [Qeweney::Request] HTTP request
|
|
@@ -226,10 +198,14 @@ module Tipi
|
|
|
226
198
|
# @param chunked [boolean] whether to use chunked transfer encoding
|
|
227
199
|
# @return [void]
|
|
228
200
|
def send_headers(request, headers, empty_response: false, chunked: true)
|
|
229
|
-
formatted_headers = format_headers(headers, !empty_response,
|
|
201
|
+
formatted_headers = format_headers(headers, !empty_response, http1_1?(request) && chunked)
|
|
230
202
|
request.tx_incr(formatted_headers.bytesize)
|
|
231
203
|
@conn.write(formatted_headers)
|
|
232
204
|
end
|
|
205
|
+
|
|
206
|
+
def http1_1?(request)
|
|
207
|
+
request.headers[':protocol'] == 'http/1.1'
|
|
208
|
+
end
|
|
233
209
|
|
|
234
210
|
# Sends a response body chunk. If no headers were sent, default headers are
|
|
235
211
|
# sent using #send_headers. if the done option is true(thy), an empty chunk
|
|
@@ -248,6 +224,20 @@ module Tipi
|
|
|
248
224
|
@conn.write(data)
|
|
249
225
|
end
|
|
250
226
|
|
|
227
|
+
def send_chunk_from_io(request, io, r, w, chunk_size)
|
|
228
|
+
len = w.splice(io, chunk_size)
|
|
229
|
+
if len > 0
|
|
230
|
+
Thread.current.backend.chain(
|
|
231
|
+
[:write, @conn, "#{len.to_s(16)}\r\n"],
|
|
232
|
+
[:splice, r, @conn, len],
|
|
233
|
+
[:write, @conn, "\r\n"]
|
|
234
|
+
)
|
|
235
|
+
else
|
|
236
|
+
@conn.write("0\r\n\r\n")
|
|
237
|
+
end
|
|
238
|
+
len
|
|
239
|
+
end
|
|
240
|
+
|
|
251
241
|
# Finishes the response to the current request. If no headers were sent,
|
|
252
242
|
# default headers are sent using #send_headers.
|
|
253
243
|
# @return [void]
|
|
@@ -257,6 +247,7 @@ module Tipi
|
|
|
257
247
|
end
|
|
258
248
|
|
|
259
249
|
def close
|
|
250
|
+
@conn.shutdown if @conn.respond_to?(:shutdown) rescue nil
|
|
260
251
|
@conn.close
|
|
261
252
|
end
|
|
262
253
|
|
|
@@ -303,7 +294,7 @@ module Tipi
|
|
|
303
294
|
if chunked
|
|
304
295
|
+"HTTP/1.1 #{status}\r\nTransfer-Encoding: chunked\r\n"
|
|
305
296
|
else
|
|
306
|
-
+"HTTP/1.1 #{status}\r\nContent-Length: #{body.bytesize}\r\n"
|
|
297
|
+
+"HTTP/1.1 #{status}\r\nContent-Length: #{body.is_a?(String) ? body.bytesize : body.to_i}\r\n"
|
|
307
298
|
end
|
|
308
299
|
end
|
|
309
300
|
|