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