tipi 0.38 → 0.42

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +5 -1
  3. data/.gitignore +5 -0
  4. data/CHANGELOG.md +34 -0
  5. data/Gemfile +5 -1
  6. data/Gemfile.lock +58 -16
  7. data/Rakefile +7 -3
  8. data/TODO.md +77 -1
  9. data/benchmarks/bm_http1_parser.rb +61 -0
  10. data/bin/benchmark +37 -0
  11. data/bin/h1pd +6 -0
  12. data/bin/tipi +3 -21
  13. data/df/sample_agent.rb +1 -1
  14. data/df/server.rb +16 -47
  15. data/df/server_utils.rb +178 -0
  16. data/examples/full_service.rb +13 -0
  17. data/examples/http1_parser.rb +55 -0
  18. data/examples/http_server.rb +15 -3
  19. data/examples/http_server_forked.rb +5 -1
  20. data/examples/http_server_routes.rb +29 -0
  21. data/examples/http_server_static.rb +26 -0
  22. data/examples/http_server_throttled.rb +3 -2
  23. data/examples/https_server.rb +6 -4
  24. data/examples/https_wss_server.rb +2 -1
  25. data/examples/rack_server.rb +5 -0
  26. data/examples/rack_server_https.rb +1 -1
  27. data/examples/rack_server_https_forked.rb +4 -3
  28. data/examples/routing_server.rb +5 -4
  29. data/examples/servername_cb.rb +37 -0
  30. data/examples/websocket_demo.rb +2 -8
  31. data/examples/ws_page.html +2 -2
  32. data/ext/tipi/extconf.rb +13 -0
  33. data/ext/tipi/http1_parser.c +823 -0
  34. data/ext/tipi/http1_parser.h +18 -0
  35. data/ext/tipi/tipi_ext.c +5 -0
  36. data/lib/tipi.rb +89 -1
  37. data/lib/tipi/acme.rb +308 -0
  38. data/lib/tipi/cli.rb +30 -0
  39. data/lib/tipi/digital_fabric/agent.rb +22 -17
  40. data/lib/tipi/digital_fabric/agent_proxy.rb +95 -40
  41. data/lib/tipi/digital_fabric/executive.rb +6 -2
  42. data/lib/tipi/digital_fabric/protocol.rb +87 -15
  43. data/lib/tipi/digital_fabric/request_adapter.rb +6 -10
  44. data/lib/tipi/digital_fabric/service.rb +77 -51
  45. data/lib/tipi/http1_adapter.rb +116 -117
  46. data/lib/tipi/http2_adapter.rb +56 -10
  47. data/lib/tipi/http2_stream.rb +106 -53
  48. data/lib/tipi/rack_adapter.rb +2 -53
  49. data/lib/tipi/response_extensions.rb +17 -0
  50. data/lib/tipi/version.rb +1 -1
  51. data/security/http1.rb +12 -0
  52. data/test/helper.rb +60 -11
  53. data/test/test_http1_parser.rb +586 -0
  54. data/test/test_http_server.rb +0 -27
  55. data/test/test_request.rb +1 -28
  56. data/tipi.gemspec +11 -5
  57. metadata +96 -22
  58. data/e +0 -0
@@ -24,9 +24,11 @@ module DigitalFabric
24
24
  class GracefulShutdown < RuntimeError
25
25
  end
26
26
 
27
+ @@id = 0
28
+
27
29
  def run
28
30
  @fiber = Fiber.current
29
- @keep_alive_timer = spin_loop(interval: 5) { keep_alive }
31
+ @keep_alive_timer = spin_loop("#{@fiber.tag}-keep_alive", interval: 5) { keep_alive }
30
32
  while true
31
33
  connect_and_process_incoming_requests
32
34
  return if @shutdown
@@ -119,7 +121,7 @@ module DigitalFabric
119
121
 
120
122
  def recv_df_message(msg)
121
123
  @last_recv = Time.now
122
- case msg['kind']
124
+ case msg[Protocol::Attribute::KIND]
123
125
  when Protocol::SHUTDOWN
124
126
  recv_shutdown
125
127
  when Protocol::HTTP_REQUEST
@@ -130,7 +132,7 @@ module DigitalFabric
130
132
  recv_ws_request(msg)
131
133
  when Protocol::CONN_DATA, Protocol::CONN_CLOSE,
132
134
  Protocol::WS_DATA, Protocol::WS_CLOSE
133
- fiber = @requests[msg['id']]
135
+ fiber = @requests[msg[Protocol::Attribute::ID]]
134
136
  fiber << msg if fiber
135
137
  end
136
138
  end
@@ -140,7 +142,7 @@ module DigitalFabric
140
142
  # messages. This is so we can correctly stop long-running requests
141
143
  # upon graceful shutdown
142
144
  if is_long_running_request_response?(msg)
143
- id = msg[:id]
145
+ id = msg[Protocol::Attribute::ID]
144
146
  @long_running_requests[id] = @requests[id]
145
147
  end
146
148
  @last_send = Time.now
@@ -148,11 +150,11 @@ module DigitalFabric
148
150
  end
149
151
 
150
152
  def is_long_running_request_response?(msg)
151
- case msg[:kind]
153
+ case msg[Protocol::Attribute::KIND]
152
154
  when Protocol::HTTP_UPGRADE
153
155
  true
154
156
  when Protocol::HTTP_RESPONSE
155
- msg[:body] && !msg[:complete]
157
+ !msg[Protocol::Attribute::HttpResponse::COMPLETE]
156
158
  end
157
159
  end
158
160
 
@@ -165,13 +167,14 @@ module DigitalFabric
165
167
 
166
168
  def recv_http_request(msg)
167
169
  req = prepare_http_request(msg)
168
- id = msg['id']
169
- @requests[id] = spin do
170
+ id = msg[Protocol::Attribute::ID]
171
+ @requests[id] = spin("#{Fiber.current.tag}.#{id}") do
170
172
  http_request(req)
171
173
  rescue IOError, Errno::ECONNREFUSED, Errno::EPIPE
172
174
  # ignore
173
- rescue Polyphony::Terminate
175
+ rescue Polyphony::Terminate => e
174
176
  req.respond(nil, { ':status' => Qeweney::Status::SERVICE_UNAVAILABLE }) if Fiber.current.graceful_shutdown?
177
+ raise e
175
178
  ensure
176
179
  @requests.delete(id)
177
180
  @long_running_requests.delete(id)
@@ -180,17 +183,19 @@ module DigitalFabric
180
183
  end
181
184
 
182
185
  def prepare_http_request(msg)
183
- req = Qeweney::Request.new(msg['headers'], RequestAdapter.new(self, msg))
184
- req.buffer_body_chunk(msg['body']) if msg['body']
185
- req.complete! if msg['complete']
186
+ headers = msg[Protocol::Attribute::HttpRequest::HEADERS]
187
+ body_chunk = msg[Protocol::Attribute::HttpRequest::BODY_CHUNK]
188
+ complete = msg[Protocol::Attribute::HttpRequest::COMPLETE]
189
+ req = Qeweney::Request.new(headers, RequestAdapter.new(self, msg))
190
+ req.buffer_body_chunk(body_chunk) if body_chunk
186
191
  req
187
192
  end
188
193
 
189
194
  def recv_http_request_body(msg)
190
- fiber = @requests[msg['id']]
195
+ fiber = @requests[msg[Protocol::Attribute::ID]]
191
196
  return unless fiber
192
197
 
193
- fiber << msg['body']
198
+ fiber << msg[Protocol::Attribute::HttpRequestBody::BODY]
194
199
  end
195
200
 
196
201
  def get_http_request_body(id, limit)
@@ -199,9 +204,9 @@ module DigitalFabric
199
204
  end
200
205
 
201
206
  def recv_ws_request(msg)
202
- req = Qeweney::Request.new(msg['headers'], RequestAdapter.new(self, msg))
203
- id = msg['id']
204
- @requests[id] = @long_running_requests[id] = spin do
207
+ req = Qeweney::Request.new(msg[Protocol::Attribute::WS::HEADERS], RequestAdapter.new(self, msg))
208
+ id = msg[Protocol::Attribute::ID]
209
+ @requests[id] = @long_running_requests[id] = spin("#{Fiber.current.tag}.#{id}-ws") do
205
210
  ws_request(req)
206
211
  rescue IOError, Errno::ECONNREFUSED, Errno::EPIPE
207
212
  # ignore
@@ -31,14 +31,15 @@ module DigitalFabric
31
31
  def run
32
32
  @fiber = Fiber.current
33
33
  @service.mount(route, self)
34
- keep_alive_timer = spin_loop(interval: 5) { keep_alive }
34
+ @mounted = true
35
+ # keep_alive_timer = spin_loop("#{@fiber.tag}-keep_alive", interval: 5) { keep_alive }
35
36
  process_incoming_messages(false)
36
37
  rescue GracefulShutdown
37
38
  puts "Proxy got graceful shutdown, left: #{@requests.size} requests" if @requests.size > 0
38
- process_incoming_messages(true)
39
+ move_on_after(15) { process_incoming_messages(true) }
39
40
  ensure
40
- keep_alive_timer&.stop
41
- @service.unmount(self)
41
+ # keep_alive_timer&.stop
42
+ unmount
42
43
  end
43
44
 
44
45
  def process_incoming_messages(shutdown = false)
@@ -48,10 +49,18 @@ module DigitalFabric
48
49
  recv_df_message(msg)
49
50
  return if shutdown && @requests.empty?
50
51
  end
51
- rescue TimeoutError, IOError
52
+ rescue TimeoutError, IOError, SystemCallError
53
+ # ignore and just return in order to terminate the proxy
54
+ end
55
+
56
+ def unmount
57
+ return unless @mounted
58
+
59
+ @service.unmount(self)
60
+ @mounted = nil
52
61
  end
53
62
 
54
- def shutdown
63
+ def send_shutdown
55
64
  send_df_message(Protocol.shutdown)
56
65
  @fiber.raise GracefulShutdown.new
57
66
  end
@@ -82,9 +91,18 @@ module DigitalFabric
82
91
 
83
92
  def recv_df_message(message)
84
93
  @last_recv = Time.now
85
- return if message['kind'] == Protocol::PING
94
+ # puts "<<< #{message.inspect}"
95
+
96
+ case message[Protocol::Attribute::KIND]
97
+ when Protocol::PING
98
+ return
99
+ when Protocol::UNMOUNT
100
+ return unmount
101
+ when Protocol::STATS_REQUEST
102
+ return handle_stats_request(message[Protocol::Attribute::ID])
103
+ end
86
104
 
87
- handler = @requests[message['id']]
105
+ handler = @requests[message[Protocol::Attribute::ID]]
88
106
  if !handler
89
107
  # puts "Unknown request id in #{message}"
90
108
  return
@@ -94,6 +112,8 @@ module DigitalFabric
94
112
  end
95
113
 
96
114
  def send_df_message(message)
115
+ # puts ">>> #{message.inspect}" unless message[Protocol::Attribute::KIND] == Protocol::PING
116
+
97
117
  @last_send = Time.now
98
118
  @conn << message.to_msgpack
99
119
  end
@@ -124,57 +144,68 @@ module DigitalFabric
124
144
  t0 = Time.now
125
145
  t1 = nil
126
146
  with_request do |id|
127
- send_df_message(Protocol.http_request(id, req))
147
+ msg = Protocol.http_request(id, req.headers, req.next_chunk(true), req.complete?)
148
+ send_df_message(msg)
128
149
  while (message = receive)
129
150
  unless t1
130
151
  t1 = Time.now
131
- @service.record_latency_measurement(t1 - t0)
152
+ @service.record_latency_measurement(t1 - t0, req)
132
153
  end
133
- return if http_request_message(id, req, message)
154
+ kind = message[Protocol::Attribute::KIND]
155
+ attributes = message[Protocol::Attribute::HttpRequest::HEADERS..-1]
156
+ return if http_request_message(id, req, kind, attributes)
134
157
  end
135
158
  end
136
159
  rescue => e
137
- req.respond("Error: #{e.inspect}", ':status' => Qeweney::Status::INTERNAL_SERVER_ERROR)
160
+ p "Internal server error: #{e.inspect}"
161
+ puts e.backtrace.join("\n")
162
+ http_request_send_error_response(e)
163
+ end
164
+
165
+ def http_request_send_error_response(error)
166
+ response = format("Error: %s\n%s", error.inspect, error.backtrace.join("\n"))
167
+ req.respond(response, ':status' => Qeweney::Status::INTERNAL_SERVER_ERROR)
168
+ rescue IOError, SystemCallError
169
+ # ignore
138
170
  end
139
171
 
140
172
  # @return [Boolean] true if response is complete
141
- def http_request_message(id, req, message)
142
- case message['kind']
173
+ def http_request_message(id, req, kind, message)
174
+ case kind
143
175
  when Protocol::HTTP_UPGRADE
144
- http_custom_upgrade(id, req, message)
176
+ http_custom_upgrade(id, req, *message)
145
177
  true
146
178
  when Protocol::HTTP_GET_REQUEST_BODY
147
- http_get_request_body(id, req, message)
179
+ http_get_request_body(id, req, *message)
148
180
  false
149
181
  when Protocol::HTTP_RESPONSE
150
- headers = message['headers']
151
- body = message['body']
152
- done = message['complete']
153
- if !req.headers_sent? && done
154
- req.respond(body, headers|| {})
155
- true
156
- else
157
- req.send_headers(headers) if headers && !req.headers_sent?
158
- req.send_chunk(body, done: done) if body or done
159
- done
160
- end
182
+ http_response(id, req, *message)
161
183
  else
162
184
  # invalid message
163
185
  true
164
186
  end
165
187
  end
166
188
 
189
+ def send_transfer_count(key, rx, tx)
190
+ send_df_message(Protocol.transfer_count(key, rx, tx))
191
+ end
192
+
193
+ def handle_stats_request(id)
194
+ stats = @service.get_stats
195
+ send_df_message(Protocol.stats_response(id, stats))
196
+ end
197
+
167
198
  HTTP_RESPONSE_UPGRADE_HEADERS = { ':status' => Qeweney::Status::SWITCHING_PROTOCOLS }
168
199
 
169
- def http_custom_upgrade(id, req, message)
200
+ def http_custom_upgrade(id, req, headers)
170
201
  # send upgrade response
171
- upgrade_headers = message['headers'] ?
172
- message['headers'].merge(HTTP_RESPONSE_UPGRADE_HEADERS) :
202
+ upgrade_headers = headers ?
203
+ headers.merge(HTTP_RESPONSE_UPGRADE_HEADERS) :
173
204
  HTTP_RESPONSE_UPGRADE_HEADERS
174
205
  req.send_headers(upgrade_headers, true)
175
206
 
176
207
  conn = req.adapter.conn
177
- reader = spin do
208
+ reader = spin("#{Fiber.current.tag}.#{id}") do
178
209
  conn.recv_loop do |data|
179
210
  send_df_message(Protocol.conn_data(id, data))
180
211
  end
@@ -187,9 +218,9 @@ module DigitalFabric
187
218
  end
188
219
 
189
220
  def http_custom_upgrade_message(conn, message)
190
- case message['kind']
221
+ case message[Protocol::Attribute::KIND]
191
222
  when Protocol::CONN_DATA
192
- conn << message['data']
223
+ conn << message[:Protocol::Attribute::ConnData::DATA]
193
224
  false
194
225
  when Protocol::CONN_CLOSE
195
226
  true
@@ -199,8 +230,30 @@ module DigitalFabric
199
230
  end
200
231
  end
201
232
 
202
- def http_get_request_body(id, req, message)
203
- case (limit = message['limit'])
233
+ def http_response(id, req, body, headers, complete, transfer_count_key)
234
+ if !req.headers_sent? && complete
235
+ req.respond(body, headers|| {})
236
+ if transfer_count_key
237
+ rx, tx = req.transfer_counts
238
+ send_transfer_count(transfer_count_key, rx, tx)
239
+ end
240
+ true
241
+ else
242
+ req.send_headers(headers) if headers && !req.headers_sent?
243
+ req.send_chunk(body, done: complete) if body or complete
244
+
245
+ if complete && transfer_count_key
246
+ rx, tx = req.transfer_counts
247
+ send_transfer_count(transfer_count_key, rx, tx)
248
+ end
249
+ complete
250
+ end
251
+ rescue IOError, SystemCallError
252
+ # ignore error
253
+ end
254
+
255
+ def http_get_request_body(id, req, limit)
256
+ case limit
204
257
  when nil
205
258
  body = req.read
206
259
  else
@@ -230,9 +283,9 @@ module DigitalFabric
230
283
  with_request do |id|
231
284
  send_df_message(Protocol.ws_request(id, req.headers))
232
285
  response = receive
233
- case response['kind']
286
+ case response[0]
234
287
  when Protocol::WS_RESPONSE
235
- headers = response['headers'] || {}
288
+ headers = response[2] || {}
236
289
  status = headers[':status'] || Qeweney::Status::SWITCHING_PROTOCOLS
237
290
  if status != Qeweney::Status::SWITCHING_PROTOCOLS
238
291
  req.respond(nil, headers)
@@ -244,18 +297,20 @@ module DigitalFabric
244
297
  req.respond(nil, ':status' => Qeweney::Status::SERVICE_UNAVAILABLE)
245
298
  end
246
299
  end
300
+ rescue IOError, SystemCallError
301
+ # ignore
247
302
  end
248
303
 
249
304
  def run_websocket_connection(id, websocket)
250
- reader = spin do
305
+ reader = spin("#{Fiber.current}.#{id}-ws") do
251
306
  websocket.recv_loop do |data|
252
307
  send_df_message(Protocol.ws_data(id, data))
253
308
  end
254
309
  end
255
310
  while (message = receive)
256
- case message['kind']
311
+ case message[Protocol::Attribute::KIND]
257
312
  when Protocol::WS_DATA
258
- websocket << message['data']
313
+ websocket << message[Protocol::Attribute::WS::DATA]
259
314
  when Protocol::WS_CLOSE
260
315
  return
261
316
  else
@@ -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
- @service.timer.every(10) do
50
+ every(10) do
47
51
  message = last_service_stats
48
52
  req.send_chunk(format_sse_event(message.to_json))
49
53
  end
@@ -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,75 @@ module DigitalFabric
19
20
  WS_DATA = 'ws_data'
20
21
  WS_CLOSE = 'ws_close'
21
22
 
23
+ TRANSFER_COUNT = 'transfer_count'
24
+
25
+ STATS_REQUEST = 'stats_request'
26
+ STATS_RESPONSE = 'stats_response'
27
+
22
28
  SEND_TIMEOUT = 15
23
29
  RECV_TIMEOUT = SEND_TIMEOUT + 5
24
30
 
31
+ module Attribute
32
+ KIND = 0
33
+ ID = 1
34
+
35
+ module HttpRequest
36
+ HEADERS = 2
37
+ BODY_CHUNK = 3
38
+ COMPLETE = 4
39
+ end
40
+
41
+ module HttpResponse
42
+ BODY = 2
43
+ HEADERS = 3
44
+ COMPLETE = 4
45
+ TRANSFER_COUNT_KEY = 5
46
+ end
47
+
48
+ module HttpUpgrade
49
+ HEADERS = 2
50
+ end
51
+
52
+ module HttpGetRequestBody
53
+ LIMIT = 2
54
+ end
55
+
56
+ module HttpRequestBody
57
+ BODY = 2
58
+ COMPLETE = 3
59
+ end
60
+
61
+ module ConnectionData
62
+ DATA = 2
63
+ end
64
+
65
+ module WS
66
+ HEADERS = 2
67
+ DATA = 2
68
+ end
69
+
70
+ module TransferCount
71
+ KEY = 1
72
+ RX = 2
73
+ TX = 3
74
+ end
75
+
76
+ module Stats
77
+ STATS = 2
78
+ end
79
+ end
80
+
25
81
  class << self
26
82
  def ping
27
- { kind: PING }
83
+ [ PING ]
28
84
  end
29
85
 
30
86
  def shutdown
31
- { kind: SHUTDOWN }
87
+ [ SHUTDOWN ]
88
+ end
89
+
90
+ def unmount
91
+ [ UNMOUNT ]
32
92
  end
33
93
 
34
94
  DF_UPGRADE_RESPONSE = <<~HTTP.gsub("\n", "\r\n")
@@ -42,48 +102,60 @@ module DigitalFabric
42
102
  DF_UPGRADE_RESPONSE
43
103
  end
44
104
 
45
- def http_request(id, req)
46
- { kind: HTTP_REQUEST, id: id, headers: req.headers, body: req.next_chunk, complete: req.complete? }
105
+ def http_request(id, headers, buffered_chunk, complete)
106
+ [ HTTP_REQUEST, id, headers, buffered_chunk, complete ]
47
107
  end
48
108
 
49
- def http_response(id, body, headers, complete)
50
- { kind: HTTP_RESPONSE, id: id, body: body, headers: headers, complete: complete }
109
+ def http_response(id, body, headers, complete, transfer_count_key = nil)
110
+ [ HTTP_RESPONSE, id, body, headers, complete, transfer_count_key ]
51
111
  end
52
112
 
53
113
  def http_upgrade(id, headers)
54
- { kind: HTTP_UPGRADE, id: id }
114
+ [ HTTP_UPGRADE, id, headers ]
55
115
  end
56
116
 
57
117
  def http_get_request_body(id, limit = nil)
58
- { kind: HTTP_GET_REQUEST_BODY, id: id, limit: limit }
118
+ [ HTTP_GET_REQUEST_BODY, id, limit ]
59
119
  end
60
120
 
61
121
  def http_request_body(id, body, complete)
62
- { kind: HTTP_REQUEST_BODY, id: id, body: body, complete: complete }
122
+ [ HTTP_REQUEST_BODY, id, body, complete ]
63
123
  end
64
124
 
65
125
  def connection_data(id, data)
66
- { kind: CONN_DATA, id: id, data: data }
126
+ [ CONN_DATA, id, data ]
67
127
  end
68
128
 
69
129
  def connection_close(id)
70
- { kind: CONN_CLOSE, id: id }
130
+ [ CONN_CLOSE, id ]
71
131
  end
72
132
 
73
133
  def ws_request(id, headers)
74
- { kind: WS_REQUEST, id: id, headers: headers }
134
+ [ WS_REQUEST, id, headers ]
75
135
  end
76
136
 
77
137
  def ws_response(id, headers)
78
- { kind: WS_RESPONSE, id: id, headers: headers }
138
+ [ WS_RESPONSE, id, headers ]
79
139
  end
80
140
 
81
141
  def ws_data(id, data)
82
- { id: id, kind: WS_DATA, data: data }
142
+ [ WS_DATA, id, data ]
83
143
  end
84
144
 
85
145
  def ws_close(id)
86
- { id: id, kind: WS_CLOSE }
146
+ [ WS_CLOSE, id ]
147
+ end
148
+
149
+ def transfer_count(key, rx, tx)
150
+ [ TRANSFER_COUNT, key, rx, tx ]
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 ]
87
159
  end
88
160
  end
89
161
  end