tipi 0.41 → 0.46

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