tipi 0.41 → 0.46

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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/workflows/test.yml +3 -1
  4. data/.gitignore +3 -1
  5. data/CHANGELOG.md +34 -0
  6. data/Gemfile +7 -1
  7. data/Gemfile.lock +53 -33
  8. data/README.md +184 -8
  9. data/Rakefile +1 -7
  10. data/benchmarks/bm_http1_parser.rb +85 -0
  11. data/bin/benchmark +37 -0
  12. data/bin/h1pd +6 -0
  13. data/bin/tipi +3 -21
  14. data/bm.png +0 -0
  15. data/df/agent.rb +1 -1
  16. data/df/sample_agent.rb +2 -2
  17. data/df/server.rb +3 -1
  18. data/df/server_utils.rb +48 -46
  19. data/examples/full_service.rb +13 -0
  20. data/examples/hello.rb +5 -0
  21. data/examples/hello.ru +3 -3
  22. data/examples/http1_parser.rb +10 -8
  23. data/examples/http_server.js +1 -1
  24. data/examples/http_server.rb +4 -1
  25. data/examples/http_server_graceful.rb +1 -1
  26. data/examples/https_server.rb +41 -15
  27. data/examples/rack_server_forked.rb +26 -0
  28. data/examples/rack_server_https_forked.rb +1 -1
  29. data/examples/servername_cb.rb +37 -0
  30. data/examples/websocket_demo.rb +1 -1
  31. data/lib/tipi/acme.rb +320 -0
  32. data/lib/tipi/cli.rb +93 -0
  33. data/lib/tipi/config_dsl.rb +13 -13
  34. data/lib/tipi/configuration.rb +2 -2
  35. data/lib/tipi/controller/bare_polyphony.rb +0 -0
  36. data/lib/tipi/controller/bare_stock.rb +10 -0
  37. data/lib/tipi/controller/extensions.rb +37 -0
  38. data/lib/tipi/controller/stock_http1_adapter.rb +15 -0
  39. data/lib/tipi/controller/web_polyphony.rb +353 -0
  40. data/lib/tipi/controller/web_stock.rb +635 -0
  41. data/lib/tipi/controller.rb +12 -0
  42. data/lib/tipi/digital_fabric/agent.rb +5 -5
  43. data/lib/tipi/digital_fabric/agent_proxy.rb +15 -8
  44. data/lib/tipi/digital_fabric/executive.rb +7 -3
  45. data/lib/tipi/digital_fabric/protocol.rb +3 -3
  46. data/lib/tipi/digital_fabric/request_adapter.rb +0 -4
  47. data/lib/tipi/digital_fabric/service.rb +17 -18
  48. data/lib/tipi/handler.rb +2 -2
  49. data/lib/tipi/http1_adapter.rb +85 -124
  50. data/lib/tipi/http2_adapter.rb +29 -16
  51. data/lib/tipi/http2_stream.rb +52 -57
  52. data/lib/tipi/rack_adapter.rb +2 -2
  53. data/lib/tipi/response_extensions.rb +1 -1
  54. data/lib/tipi/supervisor.rb +75 -0
  55. data/lib/tipi/version.rb +1 -1
  56. data/lib/tipi/websocket.rb +3 -3
  57. data/lib/tipi.rb +9 -7
  58. data/test/coverage.rb +2 -2
  59. data/test/helper.rb +60 -12
  60. data/test/test_http_server.rb +14 -41
  61. data/test/test_request.rb +2 -29
  62. data/tipi.gemspec +10 -10
  63. metadata +80 -54
  64. data/examples/automatic_certificate.rb +0 -193
  65. data/ext/tipi/extconf.rb +0 -12
  66. data/ext/tipi/http1_parser.c +0 -534
  67. data/ext/tipi/http1_parser.h +0 -18
  68. data/ext/tipi/tipi_ext.c +0 -5
  69. data/lib/tipi/http1_adapter_new.rb +0 -293
@@ -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)
@@ -172,8 +172,9 @@ module DigitalFabric
172
172
  http_request(req)
173
173
  rescue IOError, Errno::ECONNREFUSED, Errno::EPIPE
174
174
  # ignore
175
- rescue Polyphony::Terminate
175
+ rescue Polyphony::Terminate => e
176
176
  req.respond(nil, { ':status' => Qeweney::Status::SERVICE_UNAVAILABLE }) if Fiber.current.graceful_shutdown?
177
+ raise e
177
178
  ensure
178
179
  @requests.delete(id)
179
180
  @long_running_requests.delete(id)
@@ -187,7 +188,6 @@ module DigitalFabric
187
188
  complete = msg[Protocol::Attribute::HttpRequest::COMPLETE]
188
189
  req = Qeweney::Request.new(headers, RequestAdapter.new(self, msg))
189
190
  req.buffer_body_chunk(body_chunk) if body_chunk
190
- req.complete! if complete
191
191
  req
192
192
  end
193
193