tipi 0.42 → 0.47

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