tipi 0.43 → 0.45

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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/workflows/test.yml +1 -3
  4. data/CHANGELOG.md +12 -0
  5. data/Gemfile +3 -1
  6. data/Gemfile.lock +14 -7
  7. data/README.md +184 -8
  8. data/Rakefile +1 -7
  9. data/benchmarks/bm_http1_parser.rb +1 -1
  10. data/bin/benchmark +0 -0
  11. data/bin/h1pd +0 -0
  12. data/bm.png +0 -0
  13. data/df/agent.rb +1 -1
  14. data/df/sample_agent.rb +2 -2
  15. data/df/server_utils.rb +1 -1
  16. data/examples/hello.rb +5 -0
  17. data/examples/http_server.js +1 -1
  18. data/examples/http_server_graceful.rb +1 -1
  19. data/examples/https_server.rb +41 -18
  20. data/examples/rack_server_forked.rb +26 -0
  21. data/examples/rack_server_https_forked.rb +1 -1
  22. data/examples/websocket_demo.rb +1 -1
  23. data/lib/tipi/acme.rb +46 -39
  24. data/lib/tipi/cli.rb +79 -16
  25. data/lib/tipi/config_dsl.rb +13 -13
  26. data/lib/tipi/configuration.rb +2 -2
  27. data/lib/tipi/controller/bare_polyphony.rb +0 -0
  28. data/lib/tipi/controller/bare_stock.rb +10 -0
  29. data/lib/tipi/controller/stock_http1_adapter.rb +15 -0
  30. data/lib/tipi/controller/web_polyphony.rb +351 -0
  31. data/lib/tipi/controller/web_stock.rb +631 -0
  32. data/lib/tipi/controller.rb +12 -0
  33. data/lib/tipi/digital_fabric/agent.rb +3 -3
  34. data/lib/tipi/digital_fabric/agent_proxy.rb +11 -5
  35. data/lib/tipi/digital_fabric/executive.rb +1 -1
  36. data/lib/tipi/digital_fabric/protocol.rb +1 -1
  37. data/lib/tipi/digital_fabric/service.rb +8 -8
  38. data/lib/tipi/handler.rb +2 -2
  39. data/lib/tipi/http1_adapter.rb +32 -27
  40. data/lib/tipi/http2_adapter.rb +10 -10
  41. data/lib/tipi/http2_stream.rb +14 -14
  42. data/lib/tipi/rack_adapter.rb +2 -2
  43. data/lib/tipi/response_extensions.rb +1 -1
  44. data/lib/tipi/supervisor.rb +75 -0
  45. data/lib/tipi/version.rb +1 -1
  46. data/lib/tipi/websocket.rb +3 -3
  47. data/lib/tipi.rb +4 -83
  48. data/test/coverage.rb +2 -2
  49. data/test/test_http_server.rb +14 -14
  50. data/tipi.gemspec +3 -2
  51. metadata +30 -5
@@ -0,0 +1,631 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ever'
4
+ require 'localhost/authority'
5
+ require 'http/parser'
6
+ require 'qeweney'
7
+ require 'tipi/rack_adapter'
8
+
9
+ module Tipi
10
+ class Listener
11
+ def initialize(server, &handler)
12
+ @server = server
13
+ @handler = handler
14
+ end
15
+
16
+ def accept
17
+ socket, _addrinfo = @server.accept
18
+ @handler.call(socket)
19
+ end
20
+ end
21
+
22
+ class Connection
23
+ def io_ready
24
+ raise NotImplementedError
25
+ end
26
+ end
27
+
28
+ class HTTP1Connection < Connection
29
+ attr_reader :io
30
+
31
+ def initialize(io, evloop, &app)
32
+ @io = io
33
+ @evloop = evloop
34
+ @parser = Http::Parser.new(self)
35
+ @app = app
36
+ setup_read_request
37
+ end
38
+
39
+ def setup_read_request
40
+ @request_complete = nil
41
+ @request = nil
42
+ @response_buffer = nil
43
+ end
44
+
45
+ def on_headers_complete(headers)
46
+ headers = normalize_headers(headers)
47
+ headers[':path'] = @parser.request_url
48
+ headers[':method'] = @parser.http_method.downcase
49
+ scheme = (proto = headers['x-forwarded-proto']) ?
50
+ proto.downcase : scheme_from_connection
51
+ headers[':scheme'] = scheme
52
+ @request = Qeweney::Request.new(headers, self)
53
+ end
54
+
55
+ def normalize_headers(headers)
56
+ headers.each_with_object({}) do |(k, v), h|
57
+ k = k.downcase
58
+ hk = h[k]
59
+ if hk
60
+ hk = h[k] = [hk] unless hk.is_a?(Array)
61
+ v.is_a?(Array) ? hk.concat(v) : hk << v
62
+ else
63
+ h[k] = v
64
+ end
65
+ end
66
+ end
67
+
68
+ def scheme_from_connection
69
+ @io.is_a?(OpenSSL::SSL::SSLSocket) ? 'https' : 'http'
70
+ end
71
+
72
+ def on_body(chunk)
73
+ @request.buffer_body_chunk(chunk)
74
+ end
75
+
76
+ def on_message_complete
77
+ @request_complete = true
78
+ end
79
+
80
+ def io_ready
81
+ if !@request_complete
82
+ handle_read_request
83
+ else
84
+ handle_write_response
85
+ end
86
+ end
87
+
88
+ def handle_read_request
89
+ result = @io.read_nonblock(16384, exception: false)
90
+ case result
91
+ when :wait_readable
92
+ watch_io(false)
93
+ when :wait_writable
94
+ watch_io(true)
95
+ when nil
96
+ close_io
97
+ else
98
+ @parser << result
99
+ if @request_complete
100
+ handle_request
101
+ # @response = handle_request(@request_headers, @request_body)
102
+ # handle_write_response
103
+ else
104
+ watch_io(false)
105
+ end
106
+ end
107
+ rescue HTTP::Parser::Error, SystemCallError, IOError
108
+ close_io
109
+ end
110
+
111
+ def watch_io(rw)
112
+ @evloop.watch_io(self, @io, rw, true)
113
+ # @evloop.emit([:watch_io, self, @io, rw, true])
114
+ end
115
+
116
+ def close_io
117
+ @evloop.emit([:close_io, self, @io])
118
+ end
119
+
120
+ def handle_request
121
+ @app.call(@request)
122
+ # req = Qeweney::Request.new(headers, self)
123
+ # response_body = "Hello, world!"
124
+ # "HTTP/1.1 200 OK\nContent-Length: #{response_body.bytesize}\n\n#{response_body}"
125
+ end
126
+
127
+ # response API
128
+
129
+ CRLF = "\r\n"
130
+ CRLF_ZERO_CRLF_CRLF = "\r\n0\r\n\r\n"
131
+
132
+ # Sends response including headers and body. Waits for the request to complete
133
+ # if not yet completed. The body is sent using chunked transfer encoding.
134
+ # @param request [Qeweney::Request] HTTP request
135
+ # @param body [String] response body
136
+ # @param headers
137
+ def respond(request, body, headers)
138
+ formatted_headers = format_headers(headers, body, false)
139
+ request.tx_incr(formatted_headers.bytesize + (body ? body.bytesize : 0))
140
+ if body
141
+ handle_write(formatted_headers + body)
142
+ else
143
+ handle_write(formatted_headers)
144
+ end
145
+ end
146
+
147
+ # Sends response headers. If empty_response is truthy, the response status
148
+ # code will default to 204, otherwise to 200.
149
+ # @param request [Qeweney::Request] HTTP request
150
+ # @param headers [Hash] response headers
151
+ # @param empty_response [boolean] whether a response body will be sent
152
+ # @param chunked [boolean] whether to use chunked transfer encoding
153
+ # @return [void]
154
+ def send_headers(request, headers, empty_response: false, chunked: true)
155
+ formatted_headers = format_headers(headers, !empty_response, http1_1?(request) && chunked)
156
+ request.tx_incr(formatted_headers.bytesize)
157
+ handle_write(formatted_headers)
158
+ end
159
+
160
+ def http1_1?(request)
161
+ request.headers[':protocol'] == 'http/1.1'
162
+ end
163
+
164
+ # Sends a response body chunk. If no headers were sent, default headers are
165
+ # sent using #send_headers. if the done option is true(thy), an empty chunk
166
+ # will be sent to signal response completion to the client.
167
+ # @param request [Qeweney::Request] HTTP request
168
+ # @param chunk [String] response body chunk
169
+ # @param done [boolean] whether the response is completed
170
+ # @return [void]
171
+ def send_chunk(request, chunk, done: false)
172
+ data = +''
173
+ data << "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n" if chunk
174
+ data << "0\r\n\r\n" if done
175
+ return if data.empty?
176
+
177
+ request.tx_incr(data.bytesize)
178
+ handle_write(data)
179
+ end
180
+
181
+ # Finishes the response to the current request. If no headers were sent,
182
+ # default headers are sent using #send_headers.
183
+ # @return [void]
184
+ def finish(request)
185
+ request.tx_incr(5)
186
+ handle_write("0\r\n\r\n")
187
+ end
188
+
189
+ INTERNAL_HEADER_REGEXP = /^:/.freeze
190
+
191
+ # Formats response headers into an array. If empty_response is true(thy),
192
+ # the response status code will default to 204, otherwise to 200.
193
+ # @param headers [Hash] response headers
194
+ # @param body [boolean] whether a response body will be sent
195
+ # @param chunked [boolean] whether to use chunked transfer encoding
196
+ # @return [String] formatted response headers
197
+ def format_headers(headers, body, chunked)
198
+ status = headers[':status']
199
+ status ||= (body ? Qeweney::Status::OK : Qeweney::Status::NO_CONTENT)
200
+ lines = format_status_line(body, status, chunked)
201
+ headers.each do |k, v|
202
+ next if k =~ INTERNAL_HEADER_REGEXP
203
+
204
+ collect_header_lines(lines, k, v)
205
+ end
206
+ lines << CRLF
207
+ lines
208
+ end
209
+
210
+ def format_status_line(body, status, chunked)
211
+ if !body
212
+ empty_status_line(status)
213
+ else
214
+ with_body_status_line(status, body, chunked)
215
+ end
216
+ end
217
+
218
+ def empty_status_line(status)
219
+ if status == 204
220
+ +"HTTP/1.1 #{status}\r\n"
221
+ else
222
+ +"HTTP/1.1 #{status}\r\nContent-Length: 0\r\n"
223
+ end
224
+ end
225
+
226
+ def with_body_status_line(status, body, chunked)
227
+ if chunked
228
+ +"HTTP/1.1 #{status}\r\nTransfer-Encoding: chunked\r\n"
229
+ else
230
+ +"HTTP/1.1 #{status}\r\nContent-Length: #{body.is_a?(String) ? body.bytesize : body.to_i}\r\n"
231
+ end
232
+ end
233
+
234
+ def collect_header_lines(lines, key, value)
235
+ if value.is_a?(Array)
236
+ value.inject(lines) { |_, item| lines << "#{key}: #{item}\r\n" }
237
+ else
238
+ lines << "#{key}: #{value}\r\n"
239
+ end
240
+ end
241
+
242
+ def handle_write(data = nil)
243
+ if data
244
+ if @response_buffer
245
+ @response_buffer << data
246
+ else
247
+ @response_buffer = +data
248
+ end
249
+ end
250
+
251
+ result = @io.write_nonblock(@response_buffer, exception: false)
252
+ case result
253
+ when :wait_readable
254
+ watch_io(false)
255
+ when :wait_writable
256
+ watch_io(true)
257
+ when nil
258
+ close_io
259
+ else
260
+ setup_read_request
261
+ watch_io(false)
262
+ end
263
+ end
264
+ end
265
+
266
+ class Controller
267
+ def initialize(opts)
268
+ @opts = opts
269
+ @path = File.expand_path(@opts['path'])
270
+ @service = prepare_service
271
+ end
272
+
273
+ WORKER_COUNT_RANGE = (1..32).freeze
274
+
275
+ def run
276
+ worker_count = (@opts['workers'] || 1).to_i.clamp(WORKER_COUNT_RANGE)
277
+ return run_worker if worker_count == 1
278
+
279
+ supervise_workers(worker_count)
280
+ end
281
+
282
+ private
283
+
284
+ def supervise_workers(worker_count)
285
+ supervisor = spin do
286
+ worker_count.times do
287
+ pid = fork { run_worker }
288
+ puts "Forked worker pid: #{pid}"
289
+ Process.wait(pid)
290
+ puts "Done worker pid: #{pid}"
291
+ end
292
+ # supervise(restart: :always)
293
+ rescue Polyphony::Terminate
294
+ # TODO: find out how Terminate can leak like that (it's supposed to be
295
+ # caught in Fiber#run)
296
+ end
297
+ # trap('SIGTERM') { supervisor.terminate(true) }
298
+ # trap('SIGINT') do
299
+ # trap('SIGINT') { exit! }
300
+ # supervisor.terminate(true)
301
+ # end
302
+
303
+ # supervisor.await
304
+ end
305
+
306
+ def run_worker
307
+ @evloop = Ever::Loop.new
308
+ start_server(@service)
309
+ trap('SIGTERM') { @evloop.stop }
310
+ trap('SIGINT') do
311
+ trap('SIGINT') { exit! }
312
+ @evloop.stop
313
+ end
314
+ run_evloop
315
+ end
316
+
317
+ def run_evloop
318
+ @evloop.each do |event|
319
+ case event
320
+ when Listener
321
+ event.accept
322
+ when Connection
323
+ event.io_ready
324
+ when Array
325
+ cmd, key, io, rw, oneshot = event
326
+ case cmd
327
+ when :watch_io
328
+ @evloop.watch_io(key, io, rw, oneshot)
329
+ when :close_io
330
+ io.close
331
+ end
332
+ end
333
+ end
334
+ end
335
+
336
+ def prepare_service
337
+ if File.file?(@path)
338
+ File.extname(@path) == '.ru' ? rack_service : tipi_service
339
+ elsif File.directory?(@path)
340
+ static_service
341
+ else
342
+ raise "Invalid path specified #{@path}"
343
+ end
344
+ end
345
+
346
+ def start_app
347
+ if File.extname(@path) == '.ru'
348
+ start_rack_app
349
+ else
350
+ require(@path)
351
+ end
352
+ end
353
+
354
+ def rack_service
355
+ puts "Loading Rack app from #{@path}"
356
+ app = Tipi::RackAdapter.load(@path)
357
+ web_service(app)
358
+ end
359
+
360
+ def tipi_service
361
+ puts "Loading Tipi app from #{@path}"
362
+ require(@path)
363
+ app = Object.send(:app)
364
+ web_service(app)
365
+ end
366
+
367
+ def static_service
368
+ puts "Serving static files from #{@path}"
369
+ app = proc do |req|
370
+ p req: req
371
+ full_path = find_path(@path, req.path)
372
+ if full_path
373
+ req.serve_file(full_path)
374
+ else
375
+ req.respond(nil, ':status' => Qeweney::Status::NOT_FOUND)
376
+ end
377
+ end
378
+ web_service(app)
379
+ end
380
+
381
+ def web_service(app)
382
+ app = add_connection_headers(app)
383
+
384
+ prepare_listener(@opts['listen'], app)
385
+ end
386
+
387
+ def prepare_listener(spec, app)
388
+ case spec.shift
389
+ when 'http'
390
+ case spec.size
391
+ when 2
392
+ host, port = spec
393
+ port ||= 80
394
+ when 1
395
+ host = '0.0.0.0'
396
+ port = spec.first || 80
397
+ else
398
+ raise "Invalid listener spec"
399
+ end
400
+ prepare_http_listener(port, app)
401
+ when 'https'
402
+ case spec.size
403
+ when 2
404
+ host, port = spec
405
+ port ||= 80
406
+ when 1
407
+ host = 'localhost'
408
+ port = spec.first || 80
409
+ else
410
+ raise "Invalid listener spec"
411
+ end
412
+ port ||= 443
413
+ prepare_https_listener(host, port, app)
414
+ when 'full'
415
+ host, http_port, https_port = spec
416
+ http_port ||= 80
417
+ https_port ||= 443
418
+ prepare_full_service_listeners(host, http_port, https_port, app)
419
+ end
420
+ end
421
+
422
+ def prepare_http_listener(port, app)
423
+ puts "Listening for HTTP on localhost:#{port}"
424
+
425
+ proc do
426
+ start_listener('HTTP', port) do |socket|
427
+ start_client(socket, &app)
428
+ end
429
+ end
430
+ end
431
+
432
+ def start_client(socket, &app)
433
+ conn = HTTP1Connection.new(socket, @evloop, &app)
434
+ conn.watch_io(false)
435
+ end
436
+
437
+ LOCALHOST_REGEXP = /^(.+\.)?localhost$/.freeze
438
+
439
+ def prepare_https_listener(host, port, app)
440
+ localhost = host =~ LOCALHOST_REGEXP
441
+ return prepare_localhost_https_listener(port, app) if localhost
442
+
443
+ raise "No certificate found for #{host}"
444
+ # TODO: implement loading certificate
445
+ end
446
+
447
+ def prepare_localhost_https_listener(port, app)
448
+ puts "Listening for HTTPS on localhost:#{port}"
449
+
450
+ authority = Localhost::Authority.fetch
451
+ ctx = authority.server_context
452
+ ctx.ciphers = 'ECDH+aRSA'
453
+ Polyphony::Net.setup_alpn(ctx, Tipi::ALPN_PROTOCOLS)
454
+
455
+ proc do
456
+ https_listener = spin_accept_loop('HTTPS', port) do |socket|
457
+ start_https_connection_fiber(socket, ctx, nil, app)
458
+ rescue Exception => e
459
+ puts "Exception in https_listener block: #{e.inspect}\n#{e.backtrace.inspect}"
460
+ end
461
+ end
462
+ end
463
+
464
+ def prepare_full_service_listeners(host, http_port, https_port, app)
465
+ puts "Listening for HTTP on localhost:#{http_port}"
466
+ puts "Listening for HTTPS on localhost:#{https_port}"
467
+
468
+ redirect_host = (https_port == 443) ? host : "#{host}:#{https_port}"
469
+ redirect_app = ->(r) { r.redirect("https://#{redirect_host}#{r.path}") }
470
+ ctx = OpenSSL::SSL::SSLContext.new
471
+ ctx.ciphers = 'ECDH+aRSA'
472
+ Polyphony::Net.setup_alpn(ctx, Tipi::ALPN_PROTOCOLS)
473
+ certificate_store = create_certificate_store
474
+
475
+ proc do
476
+ challenge_handler = Tipi::ACME::HTTPChallengeHandler.new
477
+ certificate_manager = Tipi::ACME::CertificateManager.new(
478
+ master_ctx: ctx,
479
+ store: certificate_store,
480
+ challenge_handler: challenge_handler
481
+ )
482
+ http_app = certificate_manager.challenge_routing_app(redirect_app)
483
+
484
+ http_listener = spin_accept_loop('HTTP', http_port) do |socket|
485
+ Tipi.client_loop(socket, @opts, &http_app)
486
+ end
487
+
488
+ ssl_accept_thread_pool = Polyphony::ThreadPool.new(4)
489
+
490
+ https_listener = spin_accept_loop('HTTPS', https_port) do |socket|
491
+ start_https_connection_fiber(socket, ctx, ssl_accept_thread_pool, app)
492
+ rescue Exception => e
493
+ puts "Exception in https_listener block: #{e.inspect}\n#{e.backtrace.inspect}"
494
+ end
495
+ end
496
+
497
+ end
498
+
499
+ INVALID_PATH_REGEXP = /\/?(\.\.|\.)\//
500
+
501
+ def find_path(base, path)
502
+ return nil if path =~ INVALID_PATH_REGEXP
503
+
504
+ full_path = File.join(base, path)
505
+ return full_path if File.file?(full_path)
506
+ return find_path(full_path, 'index') if File.directory?(full_path)
507
+
508
+ qualified = "#{full_path}.html"
509
+ return qualified if File.file?(qualified)
510
+
511
+ nil
512
+ end
513
+
514
+ SOCKET_OPTS = {
515
+ reuse_addr: true,
516
+ reuse_port: true,
517
+ dont_linger: true,
518
+ }.freeze
519
+
520
+ def start_listener(name, port, &block)
521
+ host = '0.0.0.0'
522
+ socket = ::Socket.new(:INET, :STREAM).tap do |s|
523
+ s.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1)
524
+ s.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEPORT, 1)
525
+ s.setsockopt(Socket::SOL_SOCKET, Socket::SO_LINGER, [0, 0].pack('ii'))
526
+ addr = ::Socket.sockaddr_in(port, host)
527
+ s.bind(addr)
528
+ s.listen(Socket::SOMAXCONN)
529
+ end
530
+ listener = Listener.new(socket, &block)
531
+ @evloop.watch_io(listener, socket, false, false)
532
+ end
533
+
534
+ def spin_accept_loop(name, port, &block)
535
+ spin do
536
+ server = Polyphony::Net.tcp_listen('0.0.0.0', port, SOCKET_OPTS)
537
+ loop do
538
+ socket = server.accept
539
+ spin_connection_handler(name, socket, block)
540
+ rescue Polyphony::BaseException => e
541
+ raise
542
+ rescue Exception => e
543
+ puts "#{name} listener uncaught exception: #{e.inspect}"
544
+ end
545
+ ensure
546
+ finalize_listener(server) if server
547
+ end
548
+ end
549
+
550
+ def spin_connection_handler(name, socket, block)
551
+ spin do
552
+ block.(socket)
553
+ rescue Polyphony::BaseException
554
+ raise
555
+ rescue Exception => e
556
+ puts "Uncaught error in #{name} handler: #{e.inspect}"
557
+ p e.backtrace
558
+ end
559
+ end
560
+
561
+ def finalize_listener(server)
562
+ fiber = Fiber.current
563
+ gracefully_terminate_conections(fiber) if fiber.graceful_shutdown?
564
+ server.close
565
+ rescue Polyphony::BaseException
566
+ raise
567
+ rescue Exception => e
568
+ trace "Exception in finalize_listener: #{e.inspect}"
569
+ end
570
+
571
+ def gracefully_terminate_conections(fiber)
572
+ supervisor = spin { supervise }.detach
573
+ fiber.attach_all_children_to(supervisor)
574
+
575
+ # terminating the supervisor will
576
+ supervisor.terminate(true)
577
+ end
578
+
579
+ def add_connection_headers(app)
580
+ app
581
+ # proc do |req|
582
+ # conn = req.adapter.conn
583
+ # # req.headers[':peer'] = conn.peeraddr(false)[2]
584
+ # req.headers[':scheme'] ||= conn.is_a?(OpenSSL::SSL::SSLSocket) ? 'https' : 'http'
585
+ # app.(req)
586
+ # end
587
+ end
588
+
589
+ def ssl_accept(client)
590
+ client.accept
591
+ true
592
+ rescue Polyphony::BaseException
593
+ raise
594
+ rescue Exception => e
595
+ p e
596
+ e
597
+ end
598
+
599
+ def start_https_connection_fiber(socket, ctx, thread_pool, app)
600
+ client = OpenSSL::SSL::SSLSocket.new(socket, ctx)
601
+ client.sync_close = true
602
+
603
+ result = thread_pool ?
604
+ thread_pool.process { ssl_accept(client) } : ssl_accept(client)
605
+
606
+ if result.is_a?(Exception)
607
+ puts "Exception in SSL handshake: #{result.inspect}"
608
+ return
609
+ end
610
+
611
+ Tipi.client_loop(client, @opts, &app)
612
+ rescue => e
613
+ puts "Uncaught error in HTTPS connection fiber: #{e.inspect} bt: #{e.backtrace.inspect}"
614
+ ensure
615
+ (client ? client.close : socket.close) rescue nil
616
+ end
617
+
618
+ CERTIFICATE_STORE_DEFAULT_DIR = File.expand_path('~/.tipi').freeze
619
+ CERTIFICATE_STORE_DEFAULT_DB_PATH = File.join(
620
+ CERTIFICATE_STORE_DEFAULT_DIR, 'certificates.db').freeze
621
+
622
+ def create_certificate_store
623
+ FileUtils.mkdir(CERTIFICATE_STORE_DEFAULT_DIR) rescue nil
624
+ Tipi::ACME::SQLiteCertificateStore.new(CERTIFICATE_STORE_DEFAULT_DB_PATH)
625
+ end
626
+
627
+ def start_server(service)
628
+ service.call
629
+ end
630
+ end
631
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'json'
5
+
6
+ # get opts from STDIN
7
+ opts = JSON.parse(ARGV[0]) rescue nil
8
+ mod_path = "./controller/#{opts['app_type']}_#{opts['mode']}"
9
+ require_relative mod_path
10
+
11
+ controller = Tipi::Controller.new(opts)
12
+ controller.run
@@ -46,7 +46,7 @@ module DigitalFabric
46
46
  df_upgrade
47
47
  @connected = true
48
48
  @msgpack_reader = MessagePack::Unpacker.new
49
-
49
+
50
50
  process_incoming_requests
51
51
  rescue IOError, Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EPIPE, TimeoutError
52
52
  log 'Disconnected' if @connected
@@ -70,9 +70,9 @@ module DigitalFabric
70
70
  Upgrade: df
71
71
  DF-Token: %s
72
72
  DF-Mount: %s
73
-
73
+
74
74
  HTTP
75
-
75
+
76
76
  def df_upgrade
77
77
  @socket << format(UPGRADE_REQUEST, @token, mount_point)
78
78
  while (line = @socket.gets)
@@ -56,7 +56,7 @@ module DigitalFabric
56
56
  def unmount
57
57
  return unless @mounted
58
58
 
59
- @service.unmount(self)
59
+ @service.unmount(self)
60
60
  @mounted = nil
61
61
  end
62
62
 
@@ -147,11 +147,17 @@ module DigitalFabric
147
147
  msg = Protocol.http_request(id, req.headers, req.next_chunk(true), req.complete?)
148
148
  send_df_message(msg)
149
149
  while (message = receive)
150
+ kind = message[Protocol::Attribute::KIND]
150
151
  unless t1
151
152
  t1 = Time.now
152
- @service.record_latency_measurement(t1 - t0, req)
153
+ if kind == Protocol::HTTP_RESPONSE
154
+ headers = message[Protocol::Attribute::HttpResponse::HEADERS]
155
+ status = (headers && headers[':status']) || 200
156
+ if status < Qeweney::Status::BAD_REQUEST
157
+ @service.record_latency_measurement(t1 - t0, req)
158
+ end
159
+ end
153
160
  end
154
- kind = message[Protocol::Attribute::KIND]
155
161
  attributes = message[Protocol::Attribute::HttpRequest::HEADERS..-1]
156
162
  return if http_request_message(id, req, kind, attributes)
157
163
  end
@@ -241,7 +247,7 @@ module DigitalFabric
241
247
  else
242
248
  req.send_headers(headers) if headers && !req.headers_sent?
243
249
  req.send_chunk(body, done: complete) if body or complete
244
-
250
+
245
251
  if complete && transfer_count_key
246
252
  rx, tx = req.transfer_counts
247
253
  send_transfer_count(transfer_count_key, rx, tx)
@@ -285,7 +291,7 @@ module DigitalFabric
285
291
  response = receive
286
292
  case response[0]
287
293
  when Protocol::WS_RESPONSE
288
- headers = response[2] || {}
294
+ headers = response[2] || {}
289
295
  status = headers[':status'] || Qeweney::Status::SWITCHING_PROTOCOLS
290
296
  if status != Qeweney::Status::SWITCHING_PROTOCOLS
291
297
  req.respond(nil, headers)