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
data/df/server_utils.rb CHANGED
@@ -7,24 +7,7 @@ require 'tipi/digital_fabric/executive'
7
7
  require 'json'
8
8
  require 'fileutils'
9
9
  require 'time'
10
-
11
- module ::Kernel
12
- def trace(*args)
13
- STDOUT.orig_write(format_trace(args))
14
- end
15
-
16
- def format_trace(args)
17
- if args.first.is_a?(String)
18
- if args.size > 1
19
- format("%s: %p\n", args.shift, args)
20
- else
21
- format("%s\n", args.first)
22
- end
23
- else
24
- format("%p\n", args.size == 1 ? args.first : args)
25
- end
26
- end
27
- end
10
+ require 'polyphony/extensions/debug'
28
11
 
29
12
  FileUtils.cd(__dir__)
30
13
 
@@ -65,8 +48,10 @@ def listen_http
65
48
  # log("Done with HTTP connection", client: client)
66
49
  @service.decr_connection_count
67
50
  end
68
- rescue => e
69
- log("HTTP accept loop error", error: e, backtrace: e.backtrace)
51
+ rescue Polyphony::BaseException
52
+ raise
53
+ rescue Exception => e
54
+ log 'HTTP accept (unknown) error', error: e, backtrace: e.backtrace
70
55
  end
71
56
  end
72
57
  end
@@ -74,15 +59,19 @@ end
74
59
  CERTIFICATE_REGEXP = /(-----BEGIN CERTIFICATE-----\n[^-]+-----END CERTIFICATE-----\n)/.freeze
75
60
 
76
61
  def listen_https
77
- spin('https_listener') do
62
+ spin(:https_listener) do
78
63
  private_key = OpenSSL::PKey::RSA.new IO.read('../../reality/ssl/privkey.pem')
79
64
  c = IO.read('../../reality/ssl/cacert.pem')
80
65
  certificates = c.scan(CERTIFICATE_REGEXP).map { |p| OpenSSL::X509::Certificate.new(p.first) }
81
66
  ctx = OpenSSL::SSL::SSLContext.new
67
+ ctx.security_level = 0
82
68
  cert = certificates.shift
83
69
  log "SSL Certificate expires: #{cert.not_after.inspect}"
84
70
  ctx.add_certificate(cert, private_key, certificates)
85
- ctx.ciphers = 'ECDH+aRSA'
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
86
75
 
87
76
  # TODO: further limit ciphers
88
77
  # ref: https://github.com/socketry/falcon/blob/3ec805b3ceda0a764a2c5eb68cde33897b6a35ff/lib/falcon/environments/tls.rb
@@ -99,7 +88,9 @@ def listen_https
99
88
  server = Polyphony::Net.tcp_listen('0.0.0.0', 10443, opts)
100
89
  id = 0
101
90
  loop do
102
- client = server.accept
91
+ client = server.accept rescue nil
92
+ next unless client
93
+
103
94
  # log('Accept HTTPS client connection', client: client)
104
95
  spin("https#{id += 1}") do
105
96
  @service.incr_connection_count
@@ -110,10 +101,12 @@ def listen_https
110
101
  # log("Done with HTTP connection", client: client)
111
102
  @service.decr_connection_count
112
103
  end
113
- rescue OpenSSL::SSL::SSLError, SystemCallError, TypeError => e
114
- # log('HTTPS accept error', error: e, backtrace: e.backtrace)
115
- rescue => e
116
- log('HTTPS accept (unknown) error', error: e, backtrace: e.backtrace)
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
117
110
  end
118
111
  end
119
112
  end
@@ -126,13 +119,16 @@ def listen_unix
126
119
  socket = UNIXServer.new(UNIX_SOCKET_PATH)
127
120
 
128
121
  id = 0
129
- socket.accept_loop do |client|
122
+ loop do
123
+ client = socket.accept
130
124
  # log('Accept Unix connection', client: client)
131
125
  spin("unix#{id += 1}") do
132
126
  Tipi.client_loop(client, {}) { |req| @service.http_request(req, true) }
133
127
  end
134
- rescue OpenSSL::SSL::SSLError
135
- # disregard
128
+ rescue Polyphony::BaseException
129
+ raise
130
+ rescue Exception => e
131
+ log 'Unix accept error', error: e, backtrace: e.backtrace
136
132
  end
137
133
  end
138
134
  end
@@ -141,33 +137,39 @@ def listen_df
141
137
  spin(:df_listener) do
142
138
  opts = {
143
139
  reuse_addr: true,
140
+ reuse_port: true,
144
141
  dont_linger: true,
145
142
  }
146
143
  log('Listening for DF connections on localhost:4321')
147
144
  server = Polyphony::Net.tcp_listen('0.0.0.0', 4321, opts)
148
145
 
149
146
  id = 0
150
- server.accept_loop do |client|
147
+ loop do
148
+ client = server.accept
151
149
  # log('Accept DF connection', client: client)
152
150
  spin("df#{id += 1}") do
153
151
  Tipi.client_loop(client, {}) { |req| @service.http_request(req, true) }
154
152
  end
155
- rescue OpenSSL::SSL::SSLError
156
- # disregard
153
+ rescue Polyphony::BaseException
154
+ raise
155
+ rescue Exception => e
156
+ log 'DF accept (unknown) error', error: e, backtrace: e.backtrace
157
157
  end
158
158
  end
159
159
  end
160
160
 
161
- # Thread.backend.trace_proc = proc do |event, fiber, value, pri|
162
- # fiber_id = fiber.tag || fiber.inspect
163
- # case event
164
- # when :fiber_schedule
165
- # log format("=> %s %s %s %s", event, fiber_id, value.inspect, pri ? '' : '(priority)')
166
- # when :fiber_run
167
- # log format("=> %s %s %s", event, fiber_id, value.inspect)
168
- # when :fiber_create, :fiber_terminate
169
- # log format("=> %s %s", event, fiber_id)
170
- # else
171
- # log format("=> %s", event)
172
- # end
173
- # end
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
+ run { |req|
4
+ req.respond('Hello, world!')
5
+ }
data/examples/hello.ru CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  run lambda { |env|
4
4
  [
5
- 200,
6
- {"Content-Type" => "text/plain"},
7
- ["Hello, world!"]
5
+ 200,
6
+ {"Content-Type" => "text/plain"},
7
+ ["Hello, world!"]
8
8
  ]
9
9
  }
@@ -31,23 +31,25 @@ f = spin do
31
31
  break unless headers
32
32
  trace headers
33
33
 
34
- body = parser.read_body(headers)
34
+ body = parser.read_body
35
35
  trace "body: #{body ? body.bytesize : 0} bytes"
36
+ trace body if body && body.bytesize < 80
36
37
  end
37
38
  end
38
39
 
39
40
  o << "GET /a HTTP/1.1\r\n\r\n"
40
- o << "GET /b HTTP/1.1\r\n\r\n"
41
+
42
+ # o << "GET /a HTTP/1.1\r\nContent-Length: 0\r\n\r\n"
41
43
 
42
44
  # o << "GET / HTTP/1.1\r\nHost: localhost:10080\r\nUser-Agent: curl/7.74.0\r\nAccept: */*\r\n\r\n"
43
45
 
44
- # o << "post /?q=time&blah=blah HTTP/1\r\nHost: dev.realiteq.net\r\n\r\n"
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}"
45
50
 
46
- # data = " " * 4000000
47
- # o << "get /?q=time HTTP/1.1\r\nContent-Length: #{data.bytesize}\r\n\r\n#{data}"
51
+ o << "get /?q=time HTTP/1.1\r\nCookie: foo\r\nCookie: bar\r\n\r\n"
48
52
 
49
- # o << "get /?q=time HTTP/1.1\r\nCookie: foo\r\nCookie: bar\r\n\r\n"
53
+ o.close
50
54
 
51
- # o.close
52
-
53
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
 
@@ -14,7 +14,7 @@ puts 'Listening on port 10080...'
14
14
  # GC.disable
15
15
  # Thread.current.backend.idle_gc_period = 60
16
16
 
17
- spin_loop(interval: 10) { p Thread.current.fiber_scheduling_stats }
17
+ spin_loop(interval: 10) { p Thread.backend.stats }
18
18
 
19
19
  spin_loop(interval: 10) do
20
20
  GC.compact
@@ -29,6 +29,9 @@ spin do
29
29
  sleep 1
30
30
  req.send_chunk("bar\n")
31
31
  req.finish
32
+ elsif req.path == '/upload'
33
+ body = req.read
34
+ req.respond("Body: #{body.inspect} (#{body.bytesize} bytes)")
32
35
  else
33
36
  req.respond("Hello world!\n")
34
37
  end
@@ -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
@@ -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