tipi 0.37 → 0.40
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 +1 -1
- data/.gitignore +1 -0
- data/CHANGELOG.md +53 -29
- data/Gemfile.lock +23 -5
- data/TODO.md +79 -5
- data/df/sample_agent.rb +1 -1
- data/df/server.rb +63 -6
- data/examples/automatic_certificate.rb +193 -0
- data/examples/http_server.rb +11 -3
- data/examples/http_server_forked.rb +5 -1
- data/examples/http_server_routes.rb +29 -0
- data/examples/http_server_static.rb +38 -0
- data/examples/http_server_throttled.rb +3 -2
- data/examples/https_server.rb +10 -1
- 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/websocket_demo.rb +2 -8
- data/examples/ws_page.html +2 -2
- data/lib/tipi.rb +6 -0
- data/lib/tipi/digital_fabric/agent.rb +16 -13
- data/lib/tipi/digital_fabric/agent_proxy.rb +79 -27
- data/lib/tipi/digital_fabric/protocol.rb +71 -14
- data/lib/tipi/digital_fabric/request_adapter.rb +7 -7
- data/lib/tipi/digital_fabric/service.rb +10 -8
- data/lib/tipi/http1_adapter.rb +87 -36
- data/lib/tipi/http2_adapter.rb +37 -4
- data/lib/tipi/http2_stream.rb +79 -22
- data/lib/tipi/response_extensions.rb +17 -0
- data/lib/tipi/version.rb +1 -1
- data/test/test_http_server.rb +22 -37
- data/test/test_request.rb +4 -4
- data/tipi.gemspec +3 -2
- metadata +24 -6
|
@@ -4,6 +4,7 @@ module DigitalFabric
|
|
|
4
4
|
module Protocol
|
|
5
5
|
PING = 'ping'
|
|
6
6
|
SHUTDOWN = 'shutdown'
|
|
7
|
+
UNMOUNT = 'unmount'
|
|
7
8
|
|
|
8
9
|
HTTP_REQUEST = 'http_request'
|
|
9
10
|
HTTP_RESPONSE = 'http_response'
|
|
@@ -19,16 +20,68 @@ module DigitalFabric
|
|
|
19
20
|
WS_DATA = 'ws_data'
|
|
20
21
|
WS_CLOSE = 'ws_close'
|
|
21
22
|
|
|
23
|
+
TRANSFER_COUNT = 'transfer_count'
|
|
24
|
+
|
|
22
25
|
SEND_TIMEOUT = 15
|
|
23
26
|
RECV_TIMEOUT = SEND_TIMEOUT + 5
|
|
24
27
|
|
|
28
|
+
module Attribute
|
|
29
|
+
KIND = 0
|
|
30
|
+
ID = 1
|
|
31
|
+
|
|
32
|
+
module HttpRequest
|
|
33
|
+
HEADERS = 2
|
|
34
|
+
BODY_CHUNK = 3
|
|
35
|
+
COMPLETE = 4
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
module HttpResponse
|
|
39
|
+
BODY = 2
|
|
40
|
+
HEADERS = 3
|
|
41
|
+
COMPLETE = 4
|
|
42
|
+
TRANSFER_COUNT_KEY = 5
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
module HttpUpgrade
|
|
46
|
+
HEADERS = 2
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
module HttpGetRequestBody
|
|
50
|
+
LIMIT = 2
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
module HttpRequestBody
|
|
54
|
+
BODY = 2
|
|
55
|
+
COMPLETE = 3
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
module ConnectionData
|
|
59
|
+
DATA = 2
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
module WS
|
|
63
|
+
HEADERS = 2
|
|
64
|
+
DATA = 2
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
module TransferCount
|
|
68
|
+
KEY = 1
|
|
69
|
+
RX = 2
|
|
70
|
+
TX = 3
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
25
74
|
class << self
|
|
26
75
|
def ping
|
|
27
|
-
|
|
76
|
+
[ PING ]
|
|
28
77
|
end
|
|
29
78
|
|
|
30
79
|
def shutdown
|
|
31
|
-
|
|
80
|
+
[ SHUTDOWN ]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def unmount
|
|
84
|
+
[ UNMOUNT ]
|
|
32
85
|
end
|
|
33
86
|
|
|
34
87
|
DF_UPGRADE_RESPONSE = <<~HTTP.gsub("\n", "\r\n")
|
|
@@ -43,47 +96,51 @@ module DigitalFabric
|
|
|
43
96
|
end
|
|
44
97
|
|
|
45
98
|
def http_request(id, req)
|
|
46
|
-
|
|
99
|
+
[ HTTP_REQUEST, id, req.headers, req.next_chunk, req.complete? ]
|
|
47
100
|
end
|
|
48
101
|
|
|
49
|
-
def http_response(id, body, headers, complete)
|
|
50
|
-
|
|
102
|
+
def http_response(id, body, headers, complete, transfer_count_key = nil)
|
|
103
|
+
[ HTTP_RESPONSE, id, body, headers, complete, transfer_count_key ]
|
|
51
104
|
end
|
|
52
105
|
|
|
53
106
|
def http_upgrade(id, headers)
|
|
54
|
-
|
|
107
|
+
[ HTTP_UPGRADE, id, headers ]
|
|
55
108
|
end
|
|
56
109
|
|
|
57
110
|
def http_get_request_body(id, limit = nil)
|
|
58
|
-
|
|
111
|
+
[ HTTP_GET_REQUEST_BODY, id, limit ]
|
|
59
112
|
end
|
|
60
113
|
|
|
61
114
|
def http_request_body(id, body, complete)
|
|
62
|
-
|
|
115
|
+
[ HTTP_REQUEST_BODY, id, body, complete ]
|
|
63
116
|
end
|
|
64
117
|
|
|
65
118
|
def connection_data(id, data)
|
|
66
|
-
|
|
119
|
+
[ CONN_DATA, id, data ]
|
|
67
120
|
end
|
|
68
121
|
|
|
69
122
|
def connection_close(id)
|
|
70
|
-
|
|
123
|
+
[ CONN_CLOSE, id ]
|
|
71
124
|
end
|
|
72
125
|
|
|
73
126
|
def ws_request(id, headers)
|
|
74
|
-
|
|
127
|
+
[ WS_REQUEST, id, headers ]
|
|
75
128
|
end
|
|
76
129
|
|
|
77
130
|
def ws_response(id, headers)
|
|
78
|
-
|
|
131
|
+
[ WS_RESPONSE, id, headers ]
|
|
79
132
|
end
|
|
80
133
|
|
|
81
134
|
def ws_data(id, data)
|
|
82
|
-
|
|
135
|
+
[ WS_DATA, id, data ]
|
|
83
136
|
end
|
|
84
137
|
|
|
85
138
|
def ws_close(id)
|
|
86
|
-
|
|
139
|
+
[WS_CLOSE, id ]
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def transfer_count(key, rx, tx)
|
|
143
|
+
[ TRANSFER_COUNT, key, rx, tx ]
|
|
87
144
|
end
|
|
88
145
|
end
|
|
89
146
|
end
|
|
@@ -6,40 +6,40 @@ 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 consume_request
|
|
20
|
+
def consume_request(request)
|
|
21
21
|
@agent.get_http_request_body(@id, nil)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
def respond(body, headers)
|
|
24
|
+
def respond(request, body, headers)
|
|
25
25
|
@agent.send_df_message(
|
|
26
26
|
Protocol.http_response(@id, body, headers, true)
|
|
27
27
|
)
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
def send_headers(headers, opts = {})
|
|
30
|
+
def send_headers(request, headers, opts = {})
|
|
31
31
|
@agent.send_df_message(
|
|
32
32
|
Protocol.http_response(@id, nil, headers, false)
|
|
33
33
|
)
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
-
def send_chunk(body, done: )
|
|
36
|
+
def send_chunk(request, body, done: )
|
|
37
37
|
@agent.send_df_message(
|
|
38
38
|
Protocol.http_response(@id, body, nil, done)
|
|
39
39
|
)
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
-
def finish
|
|
42
|
+
def finish(request)
|
|
43
43
|
@agent.send_df_message(
|
|
44
44
|
Protocol.http_response(@id, nil, nil, true)
|
|
45
45
|
)
|
|
@@ -99,10 +99,12 @@ module DigitalFabric
|
|
|
99
99
|
end
|
|
100
100
|
|
|
101
101
|
agent.http_request(req)
|
|
102
|
-
rescue IOError, SystemCallError
|
|
102
|
+
rescue IOError, SystemCallError, HTTP2::Error::StreamClosed
|
|
103
103
|
@counters[:errors] += 1
|
|
104
104
|
rescue => e
|
|
105
105
|
@counters[:errors] += 1
|
|
106
|
+
puts '*' * 40
|
|
107
|
+
p req
|
|
106
108
|
p e
|
|
107
109
|
puts e.backtrace.join("\n")
|
|
108
110
|
req.respond(e.inspect, ':status' => Qeweney::Status::INTERNAL_SERVER_ERROR)
|
|
@@ -115,7 +117,7 @@ module DigitalFabric
|
|
|
115
117
|
req.headers['x-request-id'] = SecureRandom.uuid
|
|
116
118
|
conn = req.adapter.conn
|
|
117
119
|
req.headers['x-forwarded-for'] = conn.peeraddr(false)[2]
|
|
118
|
-
req.headers['x-forwarded-proto']
|
|
120
|
+
req.headers['x-forwarded-proto'] ||= conn.is_a?(OpenSSL::SSL::SSLSocket) ? 'https' : 'http'
|
|
119
121
|
end
|
|
120
122
|
|
|
121
123
|
def upgrade_request(req)
|
|
@@ -172,7 +174,7 @@ module DigitalFabric
|
|
|
172
174
|
def find_agent(req)
|
|
173
175
|
compile_agent_routes if @routing_changed
|
|
174
176
|
|
|
175
|
-
host = req.headers['host'] || INVALID_HOST
|
|
177
|
+
host = req.headers[':authority'] || req.headers['host'] || INVALID_HOST
|
|
176
178
|
path = req.headers[':path']
|
|
177
179
|
|
|
178
180
|
route = @route_keys.find do |route|
|
|
@@ -180,11 +182,11 @@ module DigitalFabric
|
|
|
180
182
|
end
|
|
181
183
|
return @routes[route] if route
|
|
182
184
|
|
|
183
|
-
# search for a known route for an agent that recently unmounted
|
|
184
|
-
route, wait_list = @waiting_lists.find do |route, _|
|
|
185
|
-
|
|
186
|
-
end
|
|
187
|
-
return wait_for_agent(wait_list) if route
|
|
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
|
|
188
190
|
|
|
189
191
|
nil
|
|
190
192
|
end
|
data/lib/tipi/http1_adapter.rb
CHANGED
|
@@ -29,8 +29,10 @@ module Tipi
|
|
|
29
29
|
|
|
30
30
|
# return [Boolean] true if client loop should stop
|
|
31
31
|
def handle_incoming_data(data, &block)
|
|
32
|
+
rx = data.bytesize
|
|
32
33
|
@parser << data
|
|
33
34
|
while (request = @requests_head)
|
|
35
|
+
request.headers[':rx'] = rx
|
|
34
36
|
if @first
|
|
35
37
|
request.headers[':first'] = true
|
|
36
38
|
@first = nil
|
|
@@ -48,15 +50,18 @@ module Tipi
|
|
|
48
50
|
# release references to various objects
|
|
49
51
|
@requests_head = @requests_tail = nil
|
|
50
52
|
@parser = nil
|
|
53
|
+
@splicing_pipe = nil
|
|
54
|
+
@conn.shutdown if @conn.respond_to?(:shutdown) rescue nil
|
|
51
55
|
@conn.close
|
|
52
56
|
end
|
|
53
57
|
|
|
54
58
|
# Reads a body chunk for the current request. Transfers control to the parse
|
|
55
59
|
# loop, and resumes once the parse_loop has fired the on_body callback
|
|
56
|
-
def get_body_chunk
|
|
60
|
+
def get_body_chunk(request)
|
|
57
61
|
@waiting_for_body_chunk = true
|
|
58
62
|
@next_chunk = nil
|
|
59
63
|
while !@requests_tail.complete? && (data = @conn.readpartial(8192))
|
|
64
|
+
request.rx_incr(data.bytesize)
|
|
60
65
|
@parser << data
|
|
61
66
|
return @next_chunk if @next_chunk
|
|
62
67
|
|
|
@@ -70,9 +75,10 @@ module Tipi
|
|
|
70
75
|
# Waits for the current request to complete. Transfers control to the parse
|
|
71
76
|
# loop, and resumes once the parse_loop has fired the on_message_complete
|
|
72
77
|
# callback
|
|
73
|
-
def consume_request
|
|
78
|
+
def consume_request(request)
|
|
74
79
|
request = @requests_head
|
|
75
80
|
@conn.recv_loop do |data|
|
|
81
|
+
request.rx_incr(data.bytesize)
|
|
76
82
|
@parser << data
|
|
77
83
|
return if request.complete?
|
|
78
84
|
end
|
|
@@ -87,6 +93,9 @@ module Tipi
|
|
|
87
93
|
headers = normalize_headers(headers)
|
|
88
94
|
headers[':path'] = @parser.request_url
|
|
89
95
|
headers[':method'] = @parser.http_method.downcase
|
|
96
|
+
scheme = (proto = headers['x-forwarded-proto']) ?
|
|
97
|
+
proto.downcase : scheme_from_connection
|
|
98
|
+
headers[':scheme'] = scheme
|
|
90
99
|
queue_request(Qeweney::Request.new(headers, self))
|
|
91
100
|
end
|
|
92
101
|
|
|
@@ -182,8 +191,12 @@ module Tipi
|
|
|
182
191
|
)
|
|
183
192
|
end
|
|
184
193
|
|
|
185
|
-
def websocket_connection(
|
|
186
|
-
Tipi::Websocket.new(@conn,
|
|
194
|
+
def websocket_connection(request)
|
|
195
|
+
Tipi::Websocket.new(@conn, request.headers)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def scheme_from_connection
|
|
199
|
+
@conn.is_a?(OpenSSL::SSL::SSLSocket) ? 'https' : 'http'
|
|
187
200
|
end
|
|
188
201
|
|
|
189
202
|
# response API
|
|
@@ -193,73 +206,111 @@ module Tipi
|
|
|
193
206
|
|
|
194
207
|
# Sends response including headers and body. Waits for the request to complete
|
|
195
208
|
# if not yet completed. The body is sent using chunked transfer encoding.
|
|
209
|
+
# @param request [Qeweney::Request] HTTP request
|
|
196
210
|
# @param body [String] response body
|
|
197
211
|
# @param headers
|
|
198
|
-
def respond(body, headers)
|
|
199
|
-
consume_request if @parsing
|
|
200
|
-
|
|
212
|
+
def respond(request, body, headers)
|
|
213
|
+
consume_request(request) if @parsing
|
|
214
|
+
formatted_headers = format_headers(headers, body, false)
|
|
215
|
+
request.tx_incr(formatted_headers.bytesize + (body ? body.bytesize : 0))
|
|
201
216
|
if body
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
data << body.bytesize.to_s(16) << CRLF << body << CRLF_ZERO_CRLF_CRLF
|
|
206
|
-
end
|
|
217
|
+
@conn.write(formatted_headers, body)
|
|
218
|
+
else
|
|
219
|
+
@conn.write(formatted_headers)
|
|
207
220
|
end
|
|
208
|
-
@conn.write(data.join)
|
|
209
221
|
end
|
|
222
|
+
|
|
223
|
+
def respond_from_io(request, io, headers, chunk_size = 2**14)
|
|
224
|
+
consume_request(request) if @parsing
|
|
225
|
+
|
|
226
|
+
formatted_headers = format_headers(headers, true, true)
|
|
227
|
+
request.tx_incr(formatted_headers.bytesize)
|
|
210
228
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
229
|
+
# assume chunked encoding
|
|
230
|
+
Thread.current.backend.splice_chunks(
|
|
231
|
+
io,
|
|
232
|
+
@conn,
|
|
233
|
+
formatted_headers,
|
|
234
|
+
"0\r\n\r\n",
|
|
235
|
+
->(len) { "#{len.to_s(16)}\r\n" },
|
|
236
|
+
"\r\n",
|
|
237
|
+
16384
|
|
238
|
+
)
|
|
239
|
+
end
|
|
240
|
+
|
|
216
241
|
# Sends response headers. If empty_response is truthy, the response status
|
|
217
242
|
# code will default to 204, otherwise to 200.
|
|
243
|
+
# @param request [Qeweney::Request] HTTP request
|
|
218
244
|
# @param headers [Hash] response headers
|
|
219
245
|
# @param empty_response [boolean] whether a response body will be sent
|
|
246
|
+
# @param chunked [boolean] whether to use chunked transfer encoding
|
|
220
247
|
# @return [void]
|
|
221
|
-
def send_headers(headers,
|
|
222
|
-
|
|
223
|
-
|
|
248
|
+
def send_headers(request, headers, empty_response: false, chunked: true)
|
|
249
|
+
formatted_headers = format_headers(headers, !empty_response, @parser.http_minor == 1 && chunked)
|
|
250
|
+
request.tx_incr(formatted_headers.bytesize)
|
|
251
|
+
@conn.write(formatted_headers)
|
|
224
252
|
end
|
|
225
253
|
|
|
226
254
|
# Sends a response body chunk. If no headers were sent, default headers are
|
|
227
255
|
# sent using #send_headers. if the done option is true(thy), an empty chunk
|
|
228
256
|
# will be sent to signal response completion to the client.
|
|
257
|
+
# @param request [Qeweney::Request] HTTP request
|
|
229
258
|
# @param chunk [String] response body chunk
|
|
230
259
|
# @param done [boolean] whether the response is completed
|
|
231
260
|
# @return [void]
|
|
232
|
-
def send_chunk(chunk, done: false)
|
|
233
|
-
data =
|
|
261
|
+
def send_chunk(request, chunk, done: false)
|
|
262
|
+
data = +''
|
|
234
263
|
data << "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n" if chunk
|
|
235
264
|
data << "0\r\n\r\n" if done
|
|
236
|
-
|
|
265
|
+
return if data.empty?
|
|
266
|
+
|
|
267
|
+
request.tx_incr(data.bytesize)
|
|
268
|
+
@conn.write(data)
|
|
237
269
|
end
|
|
238
270
|
|
|
271
|
+
def send_chunk_from_io(request, io, r, w, chunk_size)
|
|
272
|
+
len = w.splice(io, chunk_size)
|
|
273
|
+
if len > 0
|
|
274
|
+
Thread.current.backend.chain(
|
|
275
|
+
[:write, @conn, "#{len.to_s(16)}\r\n"],
|
|
276
|
+
[:splice, r, @conn, len],
|
|
277
|
+
[:write, @conn, "\r\n"]
|
|
278
|
+
)
|
|
279
|
+
else
|
|
280
|
+
@conn.write("0\r\n\r\n")
|
|
281
|
+
end
|
|
282
|
+
len
|
|
283
|
+
end
|
|
284
|
+
|
|
239
285
|
# Finishes the response to the current request. If no headers were sent,
|
|
240
286
|
# default headers are sent using #send_headers.
|
|
241
287
|
# @return [void]
|
|
242
|
-
def finish
|
|
288
|
+
def finish(request)
|
|
289
|
+
request.tx_incr(5)
|
|
243
290
|
@conn << "0\r\n\r\n"
|
|
244
291
|
end
|
|
245
292
|
|
|
246
293
|
def close
|
|
294
|
+
@conn.shutdown if @conn.respond_to?(:shutdown) rescue nil
|
|
247
295
|
@conn.close
|
|
248
296
|
end
|
|
249
297
|
|
|
250
298
|
private
|
|
251
299
|
|
|
300
|
+
INTERNAL_HEADER_REGEXP = /^:/.freeze
|
|
301
|
+
|
|
252
302
|
# Formats response headers into an array. If empty_response is true(thy),
|
|
253
303
|
# the response status code will default to 204, otherwise to 200.
|
|
254
304
|
# @param headers [Hash] response headers
|
|
255
|
-
# @param
|
|
305
|
+
# @param body [boolean] whether a response body will be sent
|
|
306
|
+
# @param chunked [boolean] whether to use chunked transfer encoding
|
|
256
307
|
# @return [String] formatted response headers
|
|
257
|
-
def format_headers(headers, body)
|
|
308
|
+
def format_headers(headers, body, chunked)
|
|
258
309
|
status = headers[':status']
|
|
259
310
|
status ||= (body ? Qeweney::Status::OK : Qeweney::Status::NO_CONTENT)
|
|
260
|
-
lines =
|
|
311
|
+
lines = format_status_line(body, status, chunked)
|
|
261
312
|
headers.each do |k, v|
|
|
262
|
-
next if k =~
|
|
313
|
+
next if k =~ INTERNAL_HEADER_REGEXP
|
|
263
314
|
|
|
264
315
|
collect_header_lines(lines, k, v)
|
|
265
316
|
end
|
|
@@ -267,11 +318,11 @@ module Tipi
|
|
|
267
318
|
lines
|
|
268
319
|
end
|
|
269
320
|
|
|
270
|
-
def format_status_line(body, status)
|
|
321
|
+
def format_status_line(body, status, chunked)
|
|
271
322
|
if !body
|
|
272
323
|
empty_status_line(status)
|
|
273
324
|
else
|
|
274
|
-
with_body_status_line(status, body)
|
|
325
|
+
with_body_status_line(status, body, chunked)
|
|
275
326
|
end
|
|
276
327
|
end
|
|
277
328
|
|
|
@@ -283,17 +334,17 @@ module Tipi
|
|
|
283
334
|
end
|
|
284
335
|
end
|
|
285
336
|
|
|
286
|
-
def with_body_status_line(status, body)
|
|
287
|
-
if
|
|
288
|
-
+"HTTP/1.0 #{status}\r\nContent-Length: #{body.bytesize}\r\n"
|
|
289
|
-
else
|
|
337
|
+
def with_body_status_line(status, body, chunked)
|
|
338
|
+
if chunked
|
|
290
339
|
+"HTTP/1.1 #{status}\r\nTransfer-Encoding: chunked\r\n"
|
|
340
|
+
else
|
|
341
|
+
+"HTTP/1.1 #{status}\r\nContent-Length: #{body.is_a?(String) ? body.bytesize : body.to_i}\r\n"
|
|
291
342
|
end
|
|
292
343
|
end
|
|
293
344
|
|
|
294
345
|
def collect_header_lines(lines, key, value)
|
|
295
346
|
if value.is_a?(Array)
|
|
296
|
-
value.inject(lines) { |
|
|
347
|
+
value.inject(lines) { |_, item| lines << "#{key}: #{item}\r\n" }
|
|
297
348
|
else
|
|
298
349
|
lines << "#{key}: #{value}\r\n"
|
|
299
350
|
end
|