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
@@ -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
|
|