tipi 0.40 → 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 (63) 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 +5 -1
  5. data/CHANGELOG.md +35 -0
  6. data/Gemfile +7 -1
  7. data/Gemfile.lock +55 -29
  8. data/README.md +184 -8
  9. data/Rakefile +1 -3
  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 +16 -102
  18. data/df/server_utils.rb +175 -0
  19. data/examples/full_service.rb +13 -0
  20. data/examples/hello.rb +5 -0
  21. data/examples/http1_parser.rb +55 -0
  22. data/examples/http_server.js +1 -1
  23. data/examples/http_server.rb +15 -3
  24. data/examples/http_server_graceful.rb +1 -1
  25. data/examples/http_server_static.rb +6 -18
  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 +315 -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/{e → lib/tipi/controller/bare_polyphony.rb} +0 -0
  36. data/lib/tipi/controller/bare_stock.rb +10 -0
  37. data/lib/tipi/controller/stock_http1_adapter.rb +15 -0
  38. data/lib/tipi/controller/web_polyphony.rb +351 -0
  39. data/lib/tipi/controller/web_stock.rb +631 -0
  40. data/lib/tipi/controller.rb +12 -0
  41. data/lib/tipi/digital_fabric/agent.rb +10 -8
  42. data/lib/tipi/digital_fabric/agent_proxy.rb +26 -12
  43. data/lib/tipi/digital_fabric/executive.rb +7 -3
  44. data/lib/tipi/digital_fabric/protocol.rb +19 -4
  45. data/lib/tipi/digital_fabric/request_adapter.rb +0 -4
  46. data/lib/tipi/digital_fabric/service.rb +84 -56
  47. data/lib/tipi/handler.rb +2 -2
  48. data/lib/tipi/http1_adapter.rb +86 -125
  49. data/lib/tipi/http2_adapter.rb +29 -16
  50. data/lib/tipi/http2_stream.rb +52 -56
  51. data/lib/tipi/rack_adapter.rb +2 -53
  52. data/lib/tipi/response_extensions.rb +2 -2
  53. data/lib/tipi/supervisor.rb +75 -0
  54. data/lib/tipi/version.rb +1 -1
  55. data/lib/tipi/websocket.rb +3 -3
  56. data/lib/tipi.rb +8 -5
  57. data/test/coverage.rb +2 -2
  58. data/test/helper.rb +60 -12
  59. data/test/test_http_server.rb +14 -41
  60. data/test/test_request.rb +2 -29
  61. data/tipi.gemspec +12 -8
  62. metadata +88 -28
  63. data/examples/automatic_certificate.rb +0 -193
data/df/server.rb CHANGED
@@ -1,111 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'bundler/setup'
4
- require 'tipi'
5
- require 'tipi/digital_fabric'
6
- require 'tipi/digital_fabric/executive'
7
- require 'json'
8
- require 'fileutils'
9
- require 'localhost/authority'
3
+ require_relative 'server_utils'
10
4
 
11
- FileUtils.cd(__dir__)
5
+ listeners = [
6
+ listen_http,
7
+ listen_https,
8
+ listen_unix
9
+ ]
12
10
 
13
- service = DigitalFabric::Service.new(token: 'foobar')
14
- executive = DigitalFabric::Executive.new(service, { host: 'executive.realiteq.net' })
15
-
16
- GC.disable
17
- Thread.current.backend.idle_gc_period = 60
18
-
19
- # spin_loop(interval: 60) { GC.start }
20
-
21
- class Polyphony::BaseException
22
- attr_reader :caller_backtrace
23
- end
24
-
25
- puts "pid: #{Process.pid}"
26
-
27
- http_listener = spin do
28
- opts = {
29
- reuse_addr: true,
30
- dont_linger: true,
31
- }
32
- puts 'Listening for HTTP on localhost:10080'
33
- server = Polyphony::Net.tcp_listen('0.0.0.0', 10080, opts)
34
- server.accept_loop do |client|
35
- spin do
36
- service.incr_connection_count
37
- Tipi.client_loop(client, opts) { |req| service.http_request(req) }
38
- ensure
39
- service.decr_connection_count
40
- end
41
- rescue Exception => e
42
- puts "HTTP accept_loop error: #{e.inspect}"
43
- puts e.backtrace.join("\n")
44
- end
45
- end
46
-
47
- CERTIFICATE_REGEXP = /(-----BEGIN CERTIFICATE-----\n[^-]+-----END CERTIFICATE-----\n)/.freeze
48
-
49
- https_listener = spin do
50
- private_key = OpenSSL::PKey::RSA.new IO.read('../../reality/ssl/privkey.pem')
51
- c = IO.read('../../reality/ssl/cacert.pem')
52
- certificates = c.scan(CERTIFICATE_REGEXP).map { |p| OpenSSL::X509::Certificate.new(p.first) }
53
- ctx = OpenSSL::SSL::SSLContext.new
54
- cert = certificates.shift
55
- puts "Certificate expires: #{cert.not_after.inspect}"
56
- ctx.add_certificate(cert, private_key, certificates)
57
- ctx.ciphers = 'ECDH+aRSA'
58
-
59
- # TODO: further limit ciphers
60
- # ref: https://github.com/socketry/falcon/blob/3ec805b3ceda0a764a2c5eb68cde33897b6a35ff/lib/falcon/environments/tls.rb
61
- # ref: https://github.com/socketry/falcon/blob/3ec805b3ceda0a764a2c5eb68cde33897b6a35ff/lib/falcon/tls.rb
62
-
63
- opts = {
64
- reuse_addr: true,
65
- dont_linger: true,
66
- secure_context: ctx,
67
- alpn_protocols: Tipi::ALPN_PROTOCOLS
68
- }
69
-
70
- puts 'Listening for HTTPS on localhost:10443'
71
- server = Polyphony::Net.tcp_listen('0.0.0.0', 10443, opts)
72
- loop do
73
- client = server.accept
74
- spin do
75
- service.incr_connection_count
76
- Tipi.client_loop(client, opts) { |req| service.http_request(req) }
77
- rescue Exception => e
78
- puts "Exception: #{e.inspect}"
79
- puts e.backtrace.join("\n")
80
- ensure
81
- service.decr_connection_count
82
- end
83
- rescue Polyphony::BaseException
84
- raise
85
- rescue OpenSSL::SSL::SSLError, SystemCallError => e
86
- puts "HTTPS accept error: #{e.inspect}"
87
- rescue Exception => e
88
- puts "HTTPS accept error: #{e.inspect}"
89
- puts e.backtrace.join("\n")
90
- end
91
- end
92
-
93
- UNIX_SOCKET_PATH = '/tmp/df.sock'
94
- unix_listener = spin do
95
- puts "Listening on #{UNIX_SOCKET_PATH}"
96
- FileUtils.rm(UNIX_SOCKET_PATH) if File.exists?(UNIX_SOCKET_PATH)
97
- socket = UNIXServer.new(UNIX_SOCKET_PATH)
98
- Tipi.accept_loop(socket, {}) { |req| service.http_request(req) }
99
- end
11
+ spin_loop(interval: 60) { GC.compact } if GC.respond_to?(:compact)
100
12
 
101
13
  begin
102
- Fiber.await(http_listener, https_listener, unix_listener)
14
+ log('Starting DF server')
15
+ Fiber.await(*listeners)
103
16
  rescue Interrupt
104
- puts "Got SIGINT, shutting down gracefully"
105
- service.graceful_shutdown
106
- puts "post graceful shutdown"
17
+ log('Got SIGINT, shutting down gracefully')
18
+ @service.graceful_shutdown
19
+ rescue SystemExit
20
+ # ignore
107
21
  rescue Exception => e
108
- puts '*' * 40
109
- p e
110
- puts e.backtrace.join("\n")
22
+ log("Uncaught exception", error: e, source: e.source_fiber, raising: e.raising_fiber, backtrace: e.backtrace)
23
+ ensure
24
+ log('DF server stopped')
111
25
  end
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'tipi'
5
+ require 'tipi/digital_fabric'
6
+ require 'tipi/digital_fabric/executive'
7
+ require 'json'
8
+ require 'fileutils'
9
+ require 'time'
10
+ require 'polyphony/extensions/debug'
11
+
12
+ FileUtils.cd(__dir__)
13
+
14
+ @service = DigitalFabric::Service.new(token: 'foobar')
15
+ @executive = DigitalFabric::Executive.new(@service, { host: '@executive.realiteq.net' })
16
+
17
+ @pid = Process.pid
18
+
19
+ def log(msg, **ctx)
20
+ text = format(
21
+ "%s (%d) %s\n",
22
+ Time.now.strftime('%Y-%m-%d %H:%M:%S.%3N'),
23
+ @pid,
24
+ msg
25
+ )
26
+ STDOUT.orig_write text
27
+ return if ctx.empty?
28
+
29
+ ctx.each { |k, v| STDOUT.orig_write format(" %s: %s\n", k, v.inspect) }
30
+ end
31
+
32
+ def listen_http
33
+ spin(:http_listener) do
34
+ opts = {
35
+ reuse_addr: true,
36
+ dont_linger: true,
37
+ }
38
+ log('Listening for HTTP on localhost:10080')
39
+ server = Polyphony::Net.tcp_listen('0.0.0.0', 10080, opts)
40
+ id = 0
41
+ loop do
42
+ client = server.accept
43
+ # log("Accept HTTP connection", client: client)
44
+ spin("http#{id += 1}") do
45
+ @service.incr_connection_count
46
+ Tipi.client_loop(client, opts) { |req| @service.http_request(req) }
47
+ ensure
48
+ # log("Done with HTTP connection", client: client)
49
+ @service.decr_connection_count
50
+ end
51
+ rescue Polyphony::BaseException
52
+ raise
53
+ rescue Exception => e
54
+ log 'HTTP accept (unknown) error', error: e, backtrace: e.backtrace
55
+ end
56
+ end
57
+ end
58
+
59
+ CERTIFICATE_REGEXP = /(-----BEGIN CERTIFICATE-----\n[^-]+-----END CERTIFICATE-----\n)/.freeze
60
+
61
+ def listen_https
62
+ spin(:https_listener) do
63
+ private_key = OpenSSL::PKey::RSA.new IO.read('../../reality/ssl/privkey.pem')
64
+ c = IO.read('../../reality/ssl/cacert.pem')
65
+ certificates = c.scan(CERTIFICATE_REGEXP).map { |p| OpenSSL::X509::Certificate.new(p.first) }
66
+ ctx = OpenSSL::SSL::SSLContext.new
67
+ ctx.security_level = 0
68
+ cert = certificates.shift
69
+ log "SSL Certificate expires: #{cert.not_after.inspect}"
70
+ ctx.add_certificate(cert, private_key, certificates)
71
+ # ctx.ciphers = 'ECDH+aRSA'
72
+ ctx.max_version = OpenSSL::SSL::TLS1_3_VERSION
73
+ ctx.min_version = OpenSSL::SSL::SSL3_VERSION
74
+ ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
75
+
76
+ # TODO: further limit ciphers
77
+ # ref: https://github.com/socketry/falcon/blob/3ec805b3ceda0a764a2c5eb68cde33897b6a35ff/lib/falcon/environments/tls.rb
78
+ # ref: https://github.com/socketry/falcon/blob/3ec805b3ceda0a764a2c5eb68cde33897b6a35ff/lib/falcon/tls.rb
79
+
80
+ opts = {
81
+ reuse_addr: true,
82
+ dont_linger: true,
83
+ secure_context: ctx,
84
+ alpn_protocols: Tipi::ALPN_PROTOCOLS
85
+ }
86
+
87
+ log('Listening for HTTPS on localhost:10443')
88
+ server = Polyphony::Net.tcp_listen('0.0.0.0', 10443, opts)
89
+ id = 0
90
+ loop do
91
+ client = server.accept rescue nil
92
+ next unless client
93
+
94
+ # log('Accept HTTPS client connection', client: client)
95
+ spin("https#{id += 1}") do
96
+ @service.incr_connection_count
97
+ Tipi.client_loop(client, opts) { |req| @service.http_request(req) }
98
+ rescue => e
99
+ log('Error while handling HTTPS client', client: client, error: e, backtrace: e.backtrace)
100
+ ensure
101
+ # log("Done with HTTP connection", client: client)
102
+ @service.decr_connection_count
103
+ end
104
+ # rescue OpenSSL::SSL::SSLError, SystemCallError, TypeError => e
105
+ # log('HTTPS accept error', error: e)
106
+ rescue Polyphony::BaseException
107
+ raise
108
+ rescue Exception => e
109
+ log 'HTTPS listener error: ', error: e, backtrace: e.backtrace
110
+ end
111
+ end
112
+ end
113
+
114
+ UNIX_SOCKET_PATH = '/tmp/df.sock'
115
+ def listen_unix
116
+ spin(:unix_listener) do
117
+ log("Listening on #{UNIX_SOCKET_PATH}")
118
+ FileUtils.rm(UNIX_SOCKET_PATH) if File.exists?(UNIX_SOCKET_PATH)
119
+ socket = UNIXServer.new(UNIX_SOCKET_PATH)
120
+
121
+ id = 0
122
+ loop do
123
+ client = socket.accept
124
+ # log('Accept Unix connection', client: client)
125
+ spin("unix#{id += 1}") do
126
+ Tipi.client_loop(client, {}) { |req| @service.http_request(req, true) }
127
+ end
128
+ rescue Polyphony::BaseException
129
+ raise
130
+ rescue Exception => e
131
+ log 'Unix accept error', error: e, backtrace: e.backtrace
132
+ end
133
+ end
134
+ end
135
+
136
+ def listen_df
137
+ spin(:df_listener) do
138
+ opts = {
139
+ reuse_addr: true,
140
+ reuse_port: true,
141
+ dont_linger: true,
142
+ }
143
+ log('Listening for DF connections on localhost:4321')
144
+ server = Polyphony::Net.tcp_listen('0.0.0.0', 4321, opts)
145
+
146
+ id = 0
147
+ loop do
148
+ client = server.accept
149
+ # log('Accept DF connection', client: client)
150
+ spin("df#{id += 1}") do
151
+ Tipi.client_loop(client, {}) { |req| @service.http_request(req, true) }
152
+ end
153
+ rescue Polyphony::BaseException
154
+ raise
155
+ rescue Exception => e
156
+ log 'DF accept (unknown) error', error: e, backtrace: e.backtrace
157
+ end
158
+ end
159
+ end
160
+
161
+ if ENV['TRACE'] == '1'
162
+ Thread.backend.trace_proc = proc do |event, fiber, value, pri|
163
+ fiber_id = fiber.tag || fiber.inspect
164
+ case event
165
+ when :fiber_schedule
166
+ log format("=> %s %s %s %s", event, fiber_id, value.inspect, pri ? '(priority)' : '')
167
+ when :fiber_run
168
+ log format("=> %s %s %s", event, fiber_id, value.inspect)
169
+ when :fiber_create, :fiber_terminate
170
+ log format("=> %s %s", event, fiber_id)
171
+ else
172
+ log format("=> %s", event)
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'tipi'
5
+
6
+ ::Exception.__disable_sanitized_backtrace__ = true
7
+
8
+ certificate_db_path = File.expand_path('certificate_store.db', __dir__)
9
+ certificate_store = Tipi::ACME::SQLiteCertificateStore.new(certificate_db_path)
10
+
11
+ Tipi.full_service(
12
+ certificate_store: certificate_store
13
+ ) { |req| req.respond('Hello, world!') }
data/examples/hello.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ def app
4
+ ->(req) { req.respond('Hello, world!', 'Content-Type' => 'text/plain') }
5
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'polyphony'
4
+ require_relative '../lib/tipi_ext'
5
+
6
+ i, o = IO.pipe
7
+
8
+ module ::Kernel
9
+ def trace(*args)
10
+ STDOUT.orig_write(format_trace(args))
11
+ end
12
+
13
+ def format_trace(args)
14
+ if args.first.is_a?(String)
15
+ if args.size > 1
16
+ format("%s: %p\n", args.shift, args)
17
+ else
18
+ format("%s\n", args.first)
19
+ end
20
+ else
21
+ format("%p\n", args.size == 1 ? args.first : args)
22
+ end
23
+ end
24
+ end
25
+
26
+ f = spin do
27
+ parser = Tipi::HTTP1Parser.new(i)
28
+ while true
29
+ trace '*' * 40
30
+ headers = parser.parse_headers
31
+ break unless headers
32
+ trace headers
33
+
34
+ body = parser.read_body
35
+ trace "body: #{body ? body.bytesize : 0} bytes"
36
+ trace body if body && body.bytesize < 80
37
+ end
38
+ end
39
+
40
+ o << "GET /a HTTP/1.1\r\n\r\n"
41
+
42
+ # o << "GET /a HTTP/1.1\r\nContent-Length: 0\r\n\r\n"
43
+
44
+ # o << "GET / HTTP/1.1\r\nHost: localhost:10080\r\nUser-Agent: curl/7.74.0\r\nAccept: */*\r\n\r\n"
45
+
46
+ o << "post /?q=time&blah=blah HTTP/1\r\nTransfer-Encoding: chunked\r\n\r\na\r\nabcdefghij\r\n0\r\n\r\n"
47
+
48
+ data = " " * 4000000
49
+ o << "get /?q=time HTTP/1.1\r\nContent-Length: #{data.bytesize}\r\n\r\n#{data}"
50
+
51
+ o << "get /?q=time HTTP/1.1\r\nCookie: foo\r\nCookie: bar\r\n\r\n"
52
+
53
+ o.close
54
+
55
+ f.await
@@ -12,7 +12,7 @@ const server = http.createServer((req, res) => {
12
12
  // request_url: req.url,
13
13
  // headers: req.headers
14
14
  // };
15
-
15
+
16
16
  // res.writeHead(200, { 'Content-Type': 'application/json' });
17
17
  // res.end(JSON.stringify(requestCopy));
18
18
 
@@ -9,10 +9,19 @@ opts = {
9
9
  }
10
10
 
11
11
  puts "pid: #{Process.pid}"
12
- puts 'Listening on port 4411...'
12
+ puts 'Listening on port 10080...'
13
+
14
+ # GC.disable
15
+ # Thread.current.backend.idle_gc_period = 60
16
+
17
+ spin_loop(interval: 10) { p Thread.backend.stats }
18
+
19
+ spin_loop(interval: 10) do
20
+ GC.compact
21
+ end
13
22
 
14
23
  spin do
15
- Tipi.serve('0.0.0.0', 4411, opts) do |req|
24
+ Tipi.serve('0.0.0.0', 10080, opts) do |req|
16
25
  if req.path == '/stream'
17
26
  req.send_headers('Foo' => 'Bar')
18
27
  sleep 1
@@ -20,10 +29,13 @@ spin do
20
29
  sleep 1
21
30
  req.send_chunk("bar\n")
22
31
  req.finish
32
+ elsif req.path == '/upload'
33
+ body = req.read
34
+ req.respond("Body: #{body.inspect} (#{body.bytesize} bytes)")
23
35
  else
24
36
  req.respond("Hello world!\n")
25
37
  end
26
- p req.transfer_counts
38
+ # p req.transfer_counts
27
39
  end
28
40
  p 'done...'
29
41
  end.await
@@ -24,4 +24,4 @@ puts "pid: #{Process.pid}"
24
24
  puts 'Send HUP to stop gracefully'
25
25
  puts 'Listening on port 1234...'
26
26
 
27
- suspend
27
+ suspend
@@ -16,23 +16,11 @@ root_path = FileUtils.pwd
16
16
 
17
17
  trap('INT') { exit! }
18
18
 
19
- app = Tipi.route do |req|
20
- req.on('normal') do
21
- path = File.join(root_path, req.route_relative_path)
22
- if File.file?(path)
23
- req.serve_file(path)
24
- else
25
- req.respond(nil, ':status' => Qeweney::Status::NOT_FOUND)
26
- end
27
- end
28
- req.on('spliced') do
29
- path = File.join(root_path, req.route_relative_path)
30
- if File.file?(path)
31
- req.serve_file(path, respond_from_io: true)
32
- else
33
- req.respond(nil, ':status' => Qeweney::Status::NOT_FOUND)
34
- end
19
+ Tipi.serve('0.0.0.0', 4411, opts) do |req|
20
+ path = File.join(root_path, req.path)
21
+ if File.file?(path)
22
+ req.serve_file(path)
23
+ else
24
+ req.respond(nil, ':status' => Qeweney::Status::NOT_FOUND)
35
25
  end
36
26
  end
37
-
38
- Tipi.serve('0.0.0.0', 4411, opts, &app)
@@ -10,24 +10,50 @@ authority = Localhost::Authority.fetch
10
10
  opts = {
11
11
  reuse_addr: true,
12
12
  dont_linger: true,
13
- secure_context: authority.server_context
14
13
  }
15
14
 
16
15
  puts "pid: #{Process.pid}"
17
16
  puts 'Listening on port 1234...'
18
- Tipi.serve('0.0.0.0', 1234, opts) do |req|
19
- p path: req.path
20
- if req.path == '/stream'
21
- req.send_headers('Foo' => 'Bar')
22
- sleep 0.5
23
- req.send_chunk("foo\n")
24
- sleep 0.5
25
- req.send_chunk("bar\n", done: true)
26
- else
27
- req.respond("Hello world!\n")
17
+
18
+ ctx = authority.server_context
19
+ server = Polyphony::Net.tcp_listen('0.0.0.0', 1234, opts)
20
+ loop do
21
+ socket = server.accept
22
+ client = OpenSSL::SSL::SSLSocket.new(socket, ctx)
23
+ client.sync_close = true
24
+ spin do
25
+ state = {}
26
+ accept_thread = Thread.new do
27
+ puts "call client accept"
28
+ client.accept
29
+ state[:result] = :ok
30
+ rescue Exception => e
31
+ puts error: e
32
+ state[:result] = e
33
+ end
34
+ "wait for accept thread"
35
+ accept_thread.join
36
+ "accept thread done"
37
+ if state[:result].is_a?(Exception)
38
+ puts "Exception in SSL handshake: #{state[:result].inspect}"
39
+ next
40
+ end
41
+ Tipi.client_loop(client, opts) do |req|
42
+ p path: req.path
43
+ if req.path == '/stream'
44
+ req.send_headers('Foo' => 'Bar')
45
+ sleep 0.5
46
+ req.send_chunk("foo\n")
47
+ sleep 0.5
48
+ req.send_chunk("bar\n", done: true)
49
+ elsif req.path == '/upload'
50
+ body = req.read
51
+ req.respond("Body: #{body.inspect} (#{body.bytesize} bytes)")
52
+ else
53
+ req.respond("Hello world!\n")
54
+ end
55
+ end
56
+ ensure
57
+ client ? client.close : socket.close
28
58
  end
29
- # req.send_headers
30
- # req.send_chunk("Method: #{req.method}\n")
31
- # req.send_chunk("Path: #{req.path}\n")
32
- # req.send_chunk("Query: #{req.query.inspect}\n", done: true)
33
59
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'tipi'
5
+
6
+ app_path = ARGV.first || File.expand_path('./config.ru', __dir__)
7
+ unless File.file?(app_path)
8
+ STDERR.puts "Please provide rack config file (there are some in the examples directory.)"
9
+ exit!
10
+ end
11
+
12
+ app = Tipi::RackAdapter.load(app_path)
13
+ opts = { reuse_addr: true, dont_linger: true }
14
+
15
+ server = Tipi.listen('0.0.0.0', 1234, opts)
16
+ puts 'listening on port 1234'
17
+
18
+ child_pids = []
19
+ 4.times do
20
+ child_pids << Polyphony.fork do
21
+ puts "forked pid: #{Process.pid}"
22
+ server.each(&app)
23
+ end
24
+ end
25
+
26
+ child_pids.each { |pid| Thread.current.backend.waitpid(pid) }
@@ -25,4 +25,4 @@ child_pids = []
25
25
  end
26
26
  end
27
27
 
28
- child_pids.each { |pid| Thread.current.backend.waitpid(pid) }
28
+ child_pids.each { |pid| Thread.current.backend.waitpid(pid) }
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+ require 'fiber'
5
+
6
+ ctx = OpenSSL::SSL::SSLContext.new
7
+
8
+ f = Fiber.new { |peer| loop { p peer: peer; _name, peer = peer.transfer nil } }
9
+ ctx.servername_cb = proc { |_socket, name|
10
+ p servername_cb: name
11
+ f.transfer([name, Fiber.current]).tap { |r| p result: r }
12
+ }
13
+
14
+ socket = Socket.new(:INET, :STREAM).tap do |s|
15
+ s.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1)
16
+ s.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEPORT, 1)
17
+ s.bind(Socket.sockaddr_in(12345, '0.0.0.0'))
18
+ s.listen(Socket::SOMAXCONN)
19
+ end
20
+ server = OpenSSL::SSL::SSLServer.new(socket, ctx)
21
+
22
+ Thread.new do
23
+ sleep 0.5
24
+ socket = TCPSocket.new('127.0.0.1', 12345)
25
+ client = OpenSSL::SSL::SSLSocket.new(socket)
26
+ client.hostname = 'example.com'
27
+ p client: client
28
+ client.connect
29
+ rescue => e
30
+ p client_error: e
31
+ end
32
+
33
+ while true
34
+ conn = server.accept
35
+ p accepted: conn
36
+ break
37
+ end
@@ -26,7 +26,7 @@ class WebsocketClient
26
26
  def receive
27
27
  parsed = @reader.next
28
28
  return parsed if parsed
29
-
29
+
30
30
  @socket.read_loop do |data|
31
31
  @reader << data
32
32
  parsed = @reader.next