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.
@@ -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
- { kind: PING }
76
+ [ PING ]
28
77
  end
29
78
 
30
79
  def shutdown
31
- { kind: SHUTDOWN }
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
- { kind: HTTP_REQUEST, id: id, headers: req.headers, body: req.next_chunk, complete: req.complete? }
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
- { kind: HTTP_RESPONSE, id: id, body: body, headers: headers, complete: complete }
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
- { kind: HTTP_UPGRADE, id: id }
107
+ [ HTTP_UPGRADE, id, headers ]
55
108
  end
56
109
 
57
110
  def http_get_request_body(id, limit = nil)
58
- { kind: HTTP_GET_REQUEST_BODY, id: id, limit: limit }
111
+ [ HTTP_GET_REQUEST_BODY, id, limit ]
59
112
  end
60
113
 
61
114
  def http_request_body(id, body, complete)
62
- { kind: HTTP_REQUEST_BODY, id: id, body: body, complete: complete }
115
+ [ HTTP_REQUEST_BODY, id, body, complete ]
63
116
  end
64
117
 
65
118
  def connection_data(id, data)
66
- { kind: CONN_DATA, id: id, data: data }
119
+ [ CONN_DATA, id, data ]
67
120
  end
68
121
 
69
122
  def connection_close(id)
70
- { kind: CONN_CLOSE, id: id }
123
+ [ CONN_CLOSE, id ]
71
124
  end
72
125
 
73
126
  def ws_request(id, headers)
74
- { kind: WS_REQUEST, id: id, headers: headers }
127
+ [ WS_REQUEST, id, headers ]
75
128
  end
76
129
 
77
130
  def ws_response(id, headers)
78
- { kind: WS_RESPONSE, id: id, headers: headers }
131
+ [ WS_RESPONSE, id, headers ]
79
132
  end
80
133
 
81
134
  def ws_data(id, data)
82
- { id: id, kind: WS_DATA, data: data }
135
+ [ WS_DATA, id, data ]
83
136
  end
84
137
 
85
138
  def ws_close(id)
86
- { id: id, kind: WS_CLOSE }
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['id']
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'] = conn.is_a?(OpenSSL::SSL::SSLSocket) ? 'https' : 'http'
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
- (host == route[:host]) || (path =~ route[:path_regexp])
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
@@ -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(req)
186
- Tipi::Websocket.new(@conn, req.headers)
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
- data = format_headers(headers, body)
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
- if @parser.http_minor == 0
203
- data << body
204
- else
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
- DEFAULT_HEADERS_OPTS = {
212
- empty_response: false,
213
- consume_request: true
214
- }.freeze
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, opts = DEFAULT_HEADERS_OPTS)
222
- data = format_headers(headers, true)
223
- @conn.write(data.join)
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
- @conn.write(data.join)
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 empty_response [boolean] whether a response body will be sent
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 = [format_status_line(body, status)]
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 @parser.http_minor == 0
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) { |lines, item| data << "#{key}: #{item}\r\n" }
347
+ value.inject(lines) { |_, item| lines << "#{key}: #{item}\r\n" }
297
348
  else
298
349
  lines << "#{key}: #{value}\r\n"
299
350
  end