tipi 0.40 → 0.45

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