tipi 0.39 → 0.43

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +4 -0
  3. data/.gitignore +5 -1
  4. data/CHANGELOG.md +30 -0
  5. data/Gemfile +5 -1
  6. data/Gemfile.lock +62 -25
  7. data/Rakefile +7 -3
  8. data/benchmarks/bm_http1_parser.rb +85 -0
  9. data/bin/benchmark +37 -0
  10. data/bin/h1pd +6 -0
  11. data/bin/tipi +3 -21
  12. data/df/server.rb +16 -87
  13. data/df/server_utils.rb +175 -0
  14. data/examples/full_service.rb +13 -0
  15. data/examples/http1_parser.rb +55 -0
  16. data/examples/http_server.rb +15 -3
  17. data/examples/http_server_forked.rb +3 -1
  18. data/examples/http_server_routes.rb +29 -0
  19. data/examples/http_server_static.rb +26 -0
  20. data/examples/https_server.rb +3 -0
  21. data/examples/servername_cb.rb +37 -0
  22. data/examples/websocket_demo.rb +2 -8
  23. data/examples/ws_page.html +2 -2
  24. data/lib/tipi.rb +89 -1
  25. data/lib/tipi/acme.rb +308 -0
  26. data/lib/tipi/cli.rb +30 -0
  27. data/lib/tipi/digital_fabric/agent.rb +7 -5
  28. data/lib/tipi/digital_fabric/agent_proxy.rb +16 -8
  29. data/lib/tipi/digital_fabric/executive.rb +6 -2
  30. data/lib/tipi/digital_fabric/protocol.rb +18 -3
  31. data/lib/tipi/digital_fabric/request_adapter.rb +0 -4
  32. data/lib/tipi/digital_fabric/service.rb +77 -49
  33. data/lib/tipi/http1_adapter.rb +91 -100
  34. data/lib/tipi/http2_adapter.rb +21 -6
  35. data/lib/tipi/http2_stream.rb +54 -44
  36. data/lib/tipi/rack_adapter.rb +2 -53
  37. data/lib/tipi/response_extensions.rb +17 -0
  38. data/lib/tipi/version.rb +1 -1
  39. data/test/helper.rb +60 -12
  40. data/test/test_http_server.rb +0 -27
  41. data/test/test_request.rb +2 -29
  42. data/tipi.gemspec +11 -7
  43. metadata +79 -26
  44. data/e +0 -0
@@ -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!') }
@@ -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
@@ -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
@@ -11,11 +11,13 @@ opts = {
11
11
  dont_linger: true
12
12
  }
13
13
 
14
+ server = Tipi.listen('0.0.0.0', 1234, opts)
15
+
14
16
  child_pids = []
15
17
  8.times do
16
18
  pid = Polyphony.fork do
17
19
  puts "forked pid: #{Process.pid}"
18
- Tipi.serve('0.0.0.0', 1234, opts) do |req|
20
+ server.each do |req|
19
21
  req.respond("Hello world! from pid: #{Process.pid}\n")
20
22
  end
21
23
  rescue Interrupt
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'tipi'
5
+
6
+ opts = {
7
+ reuse_addr: true,
8
+ dont_linger: true
9
+ }
10
+
11
+ puts "pid: #{Process.pid}"
12
+ puts 'Listening on port 4411...'
13
+
14
+ app = Tipi.route do |req|
15
+ req.on 'stream' do
16
+ req.send_headers('Foo' => 'Bar')
17
+ sleep 1
18
+ req.send_chunk("foo\n")
19
+ sleep 1
20
+ req.send_chunk("bar\n")
21
+ req.finish
22
+ end
23
+ req.default do
24
+ req.respond("Hello world!\n")
25
+ end
26
+ end
27
+
28
+ trap('INT') { exit! }
29
+ Tipi.serve('0.0.0.0', 4411, opts, &app)
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'tipi'
5
+ require 'fileutils'
6
+
7
+ opts = {
8
+ reuse_addr: true,
9
+ dont_linger: true
10
+ }
11
+
12
+ puts "pid: #{Process.pid}"
13
+ puts 'Listening on port 4411...'
14
+
15
+ root_path = FileUtils.pwd
16
+
17
+ trap('INT') { exit! }
18
+
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)
25
+ end
26
+ end
@@ -23,6 +23,9 @@ Tipi.serve('0.0.0.0', 1234, opts) do |req|
23
23
  req.send_chunk("foo\n")
24
24
  sleep 0.5
25
25
  req.send_chunk("bar\n", done: true)
26
+ elsif req.path == '/upload'
27
+ body = req.read
28
+ req.respond("Body: #{body.inspect} (#{body.bytesize} bytes)")
26
29
  else
27
30
  req.respond("Hello world!\n")
28
31
  end
@@ -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
@@ -1,13 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'bundler/inline'
4
-
5
- gemfile do
6
- source 'https://rubygems.org'
7
- gem 'polyphony', '~> 0.44'
8
- gem 'tipi', '~> 0.31'
9
- end
10
-
3
+ require 'bundler/setup'
4
+ require 'tipi'
11
5
  require 'tipi/websocket'
12
6
 
13
7
  class WebsocketClient
@@ -6,7 +6,7 @@
6
6
  <body>
7
7
  <script>
8
8
  var connect = function () {
9
- var exampleSocket = new WebSocket("wss://dev.realiteq.net/");
9
+ var exampleSocket = new WebSocket("ws://localhost:4411/");
10
10
 
11
11
  exampleSocket.onopen = function (event) {
12
12
  document.querySelector('#status').innerText = 'connected';
@@ -30,4 +30,4 @@
30
30
  <h1 id="status">disconnected</h1>
31
31
  <h1 id="msg"></h1>
32
32
  </body>
33
- </html>
33
+ </html>
data/lib/tipi.rb CHANGED
@@ -1,9 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'polyphony'
4
+
4
5
  require_relative './tipi/http1_adapter'
5
6
  require_relative './tipi/http2_adapter'
6
7
  require_relative './tipi/configuration'
8
+ require_relative './tipi/response_extensions'
9
+ require_relative './tipi/acme'
10
+
11
+ require 'qeweney/request'
12
+
13
+ class Qeweney::Request
14
+ include Tipi::ResponseExtensions
15
+ end
7
16
 
8
17
  module Tipi
9
18
  ALPN_PROTOCOLS = %w[h2 http/1.1].freeze
@@ -42,7 +51,7 @@ module Tipi
42
51
  ensure
43
52
  client.close rescue nil
44
53
  end
45
-
54
+
46
55
  def protocol_adapter(socket, opts)
47
56
  use_http2 = socket.respond_to?(:alpn_protocol) &&
48
57
  socket.alpn_protocol == H2_PROTOCOL
@@ -53,5 +62,84 @@ module Tipi
53
62
  def route(&block)
54
63
  proc { |req| req.route(&block) }
55
64
  end
65
+
66
+ CERTIFICATE_STORE_DEFAULT_DIR = File.expand_path('~/.tipi')
67
+ CERTIFICATE_STORE_DEFAULT_DB_PATH = File.join(
68
+ CERTIFICATE_STORE_DEFAULT_DIR, 'certificates.db'
69
+ )
70
+
71
+ def default_certificate_store
72
+ FileUtils.mkdir(CERTIFICATE_STORE_DEFAULT_DIR) rescue nil
73
+ Tipi::ACME::SQLiteCertificateStore.new(CERTIFICATE_STORE_DEFAULT_DB_PATH)
74
+ end
75
+
76
+ def full_service(
77
+ http_port: 10080,
78
+ https_port: 10443,
79
+ certificate_store: default_certificate_store,
80
+ app: nil, &block
81
+ )
82
+ app ||= block
83
+ raise "No app given" unless app
84
+
85
+ http_handler = ->(r) { r.redirect("https://#{r.host}#{r.path}") }
86
+
87
+ ctx = OpenSSL::SSL::SSLContext.new
88
+ # ctx.ciphers = 'ECDH+aRSA'
89
+ Polyphony::Net.setup_alpn(ctx, Tipi::ALPN_PROTOCOLS)
90
+
91
+ challenge_handler = Tipi::ACME::HTTPChallengeHandler.new
92
+ certificate_manager = Tipi::ACME::CertificateManager.new(
93
+ master_ctx: ctx,
94
+ store: certificate_store,
95
+ challenge_handler: challenge_handler
96
+ )
97
+
98
+ http_listener = spin do
99
+ opts = {
100
+ reuse_addr: true,
101
+ reuse_port: true,
102
+ dont_linger: true,
103
+ }
104
+ puts "Listening for HTTP on localhost:#{http_port}"
105
+ server = Polyphony::Net.tcp_listen('0.0.0.0', http_port, opts)
106
+ wrapped_handler = certificate_manager.challenge_routing_app(http_handler)
107
+ server.accept_loop do |client|
108
+ spin do
109
+ Tipi.client_loop(client, opts, &wrapped_handler)
110
+ rescue => e
111
+ puts "Uncaught error in HTTP listener: #{e.inspect}"
112
+ end
113
+ end
114
+ ensure
115
+ server.close
116
+ end
117
+
118
+ https_listener = spin do
119
+ opts = {
120
+ reuse_addr: true,
121
+ reuse_port: true,
122
+ dont_linger: true,
123
+ secure_context: ctx,
124
+ }
125
+
126
+ puts "Listening for HTTPS on localhost:#{https_port}"
127
+ server = Polyphony::Net.tcp_listen('0.0.0.0', https_port, opts)
128
+ loop do
129
+ client = server.accept
130
+ spin do
131
+ Tipi.client_loop(client, opts, &app)
132
+ rescue => e
133
+ puts "Uncaught error in HTTPS listener: #{e.inspect}"
134
+ end
135
+ rescue OpenSSL::SSL::SSLError, SystemCallError, TypeError
136
+ # ignore
137
+ end
138
+ ensure
139
+ server.close
140
+ end
141
+
142
+ Fiber.await(http_listener, https_listener)
143
+ end
56
144
  end
57
145
  end