tipi 0.43 → 0.45

Sign up to get free protection for your applications and to get access to all the features.
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)